Merge "Replace biometric dependency from tip of tree" into androidx-main
diff --git a/.github/actions/build-single-project/action.yml b/.github/actions/build-single-project/action.yml
index 8c7af75..ecedb70 100644
--- a/.github/actions/build-single-project/action.yml
+++ b/.github/actions/build-single-project/action.yml
@@ -32,8 +32,10 @@
shell: bash
run: echo "yes" | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "cmake;3.22.1"
- name: "Install NDK"
+ working-directory: ${{ github.workspace }}
shell: bash
run: |
+ set -x
NDK_VERSION=$(grep "ndkVersion" settings.gradle | awk -F "=" '{gsub(/"| /, ""); print $2}')
echo "yes" | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "ndk;$NDK_VERSION"
- name: "Install Android SDK Build-Tools"
diff --git a/OWNERS b/OWNERS
index 0bbe0a2..63cb0b9 100644
--- a/OWNERS
+++ b/OWNERS
@@ -39,6 +39,8 @@
per-file *libraryversions.toml = [email protected]
# Glance
per-file *libraryversions.toml = [email protected]
+# AppSearch
+per-file *libraryversions.toml = [email protected], [email protected], [email protected]
# Copybara can self-approve CLs within synced docs.
per-file docs/** = [email protected]
\ No newline at end of file
diff --git a/activity/activity-compose/api/1.10.0-beta01.txt b/activity/activity-compose/api/1.10.0-beta01.txt
deleted file mode 100644
index df9fcaf..0000000
--- a/activity/activity-compose/api/1.10.0-beta01.txt
+++ /dev/null
@@ -1,55 +0,0 @@
-// Signature format: 4.0
-package androidx.activity.compose {
-
- public final class ActivityResultRegistryKt {
- method @androidx.compose.runtime.Composable public static <I, O> androidx.activity.compose.ManagedActivityResultLauncher<I,O> rememberLauncherForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> onResult);
- }
-
- public final class BackHandlerKt {
- method @androidx.compose.runtime.Composable public static void BackHandler(optional boolean enabled, kotlin.jvm.functions.Function0<kotlin.Unit> onBack);
- }
-
- public final class ComponentActivityKt {
- method public static void setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionContext? parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- }
-
- public final class LocalActivityResultRegistryOwner {
- method @androidx.compose.runtime.Composable public androidx.activity.result.ActivityResultRegistryOwner? getCurrent();
- method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.result.ActivityResultRegistryOwner?> provides(androidx.activity.result.ActivityResultRegistryOwner registryOwner);
- property @androidx.compose.runtime.Composable public final androidx.activity.result.ActivityResultRegistryOwner? current;
- field public static final androidx.activity.compose.LocalActivityResultRegistryOwner INSTANCE;
- }
-
- public final class LocalFullyDrawnReporterOwner {
- method @androidx.compose.runtime.Composable public androidx.activity.FullyDrawnReporterOwner? getCurrent();
- method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.FullyDrawnReporterOwner?> provides(androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
- property @androidx.compose.runtime.Composable public final androidx.activity.FullyDrawnReporterOwner? current;
- field public static final androidx.activity.compose.LocalFullyDrawnReporterOwner INSTANCE;
- }
-
- public final class LocalOnBackPressedDispatcherOwner {
- method @androidx.compose.runtime.Composable public androidx.activity.OnBackPressedDispatcherOwner? getCurrent();
- method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.OnBackPressedDispatcherOwner?> provides(androidx.activity.OnBackPressedDispatcherOwner dispatcherOwner);
- property @androidx.compose.runtime.Composable public final androidx.activity.OnBackPressedDispatcherOwner? current;
- field public static final androidx.activity.compose.LocalOnBackPressedDispatcherOwner INSTANCE;
- }
-
- public final class ManagedActivityResultLauncher<I, O> extends androidx.activity.result.ActivityResultLauncher<I> {
- method public androidx.activity.result.contract.ActivityResultContract<I,O> getContract();
- method public void launch(I input, androidx.core.app.ActivityOptionsCompat? options);
- method @Deprecated public void unregister();
- property public androidx.activity.result.contract.ActivityResultContract<I,O> contract;
- }
-
- public final class PredictiveBackHandlerKt {
- method @androidx.compose.runtime.Composable public static void PredictiveBackHandler(optional boolean enabled, kotlin.jvm.functions.Function2<kotlinx.coroutines.flow.Flow<androidx.activity.BackEventCompat>,? super kotlin.coroutines.Continuation<kotlin.Unit>,? extends java.lang.Object?> onBack);
- }
-
- public final class ReportDrawnKt {
- method @androidx.compose.runtime.Composable public static void ReportDrawn();
- method @androidx.compose.runtime.Composable public static void ReportDrawnAfter(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- method @androidx.compose.runtime.Composable public static void ReportDrawnWhen(kotlin.jvm.functions.Function0<java.lang.Boolean> predicate);
- }
-
-}
-
diff --git a/activity/activity-compose/api/restricted_1.10.0-beta01.txt b/activity/activity-compose/api/restricted_1.10.0-beta01.txt
deleted file mode 100644
index df9fcaf..0000000
--- a/activity/activity-compose/api/restricted_1.10.0-beta01.txt
+++ /dev/null
@@ -1,55 +0,0 @@
-// Signature format: 4.0
-package androidx.activity.compose {
-
- public final class ActivityResultRegistryKt {
- method @androidx.compose.runtime.Composable public static <I, O> androidx.activity.compose.ManagedActivityResultLauncher<I,O> rememberLauncherForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, kotlin.jvm.functions.Function1<? super O,kotlin.Unit> onResult);
- }
-
- public final class BackHandlerKt {
- method @androidx.compose.runtime.Composable public static void BackHandler(optional boolean enabled, kotlin.jvm.functions.Function0<kotlin.Unit> onBack);
- }
-
- public final class ComponentActivityKt {
- method public static void setContent(androidx.activity.ComponentActivity, optional androidx.compose.runtime.CompositionContext? parent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- }
-
- public final class LocalActivityResultRegistryOwner {
- method @androidx.compose.runtime.Composable public androidx.activity.result.ActivityResultRegistryOwner? getCurrent();
- method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.result.ActivityResultRegistryOwner?> provides(androidx.activity.result.ActivityResultRegistryOwner registryOwner);
- property @androidx.compose.runtime.Composable public final androidx.activity.result.ActivityResultRegistryOwner? current;
- field public static final androidx.activity.compose.LocalActivityResultRegistryOwner INSTANCE;
- }
-
- public final class LocalFullyDrawnReporterOwner {
- method @androidx.compose.runtime.Composable public androidx.activity.FullyDrawnReporterOwner? getCurrent();
- method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.FullyDrawnReporterOwner?> provides(androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
- property @androidx.compose.runtime.Composable public final androidx.activity.FullyDrawnReporterOwner? current;
- field public static final androidx.activity.compose.LocalFullyDrawnReporterOwner INSTANCE;
- }
-
- public final class LocalOnBackPressedDispatcherOwner {
- method @androidx.compose.runtime.Composable public androidx.activity.OnBackPressedDispatcherOwner? getCurrent();
- method public infix androidx.compose.runtime.ProvidedValue<androidx.activity.OnBackPressedDispatcherOwner?> provides(androidx.activity.OnBackPressedDispatcherOwner dispatcherOwner);
- property @androidx.compose.runtime.Composable public final androidx.activity.OnBackPressedDispatcherOwner? current;
- field public static final androidx.activity.compose.LocalOnBackPressedDispatcherOwner INSTANCE;
- }
-
- public final class ManagedActivityResultLauncher<I, O> extends androidx.activity.result.ActivityResultLauncher<I> {
- method public androidx.activity.result.contract.ActivityResultContract<I,O> getContract();
- method public void launch(I input, androidx.core.app.ActivityOptionsCompat? options);
- method @Deprecated public void unregister();
- property public androidx.activity.result.contract.ActivityResultContract<I,O> contract;
- }
-
- public final class PredictiveBackHandlerKt {
- method @androidx.compose.runtime.Composable public static void PredictiveBackHandler(optional boolean enabled, kotlin.jvm.functions.Function2<kotlinx.coroutines.flow.Flow<androidx.activity.BackEventCompat>,? super kotlin.coroutines.Continuation<kotlin.Unit>,? extends java.lang.Object?> onBack);
- }
-
- public final class ReportDrawnKt {
- method @androidx.compose.runtime.Composable public static void ReportDrawn();
- method @androidx.compose.runtime.Composable public static void ReportDrawnAfter(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- method @androidx.compose.runtime.Composable public static void ReportDrawnWhen(kotlin.jvm.functions.Function0<java.lang.Boolean> predicate);
- }
-
-}
-
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
index 1ba51b4..a8e1bee 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
@@ -172,6 +172,71 @@
}
}
+ @Test
+ fun testPredictiveBackHandlerDisabledBeforeStart() {
+ val result = mutableListOf<String>()
+ var count by mutableStateOf(2)
+ lateinit var dispatcherOwner: TestOnBackPressedDispatcherOwner
+ lateinit var dispatcher: OnBackPressedDispatcher
+ var started = false
+
+ rule.setContent {
+ dispatcherOwner =
+ TestOnBackPressedDispatcherOwner(LocalLifecycleOwner.current.lifecycle)
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides dispatcherOwner) {
+ PredictiveBackHandler(count > 1) { progress ->
+ if (count <= 1) {
+ started = true
+ }
+ progress.collect()
+ result += "onBack"
+ }
+ dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
+ }
+ }
+
+ // Changing the count right before starting the gesture is received in the
+ // onBackStackStarted callback
+ count = 1
+ dispatcher.startGestureBack()
+
+ rule.runOnIdle { assertThat(started).isTrue() }
+ dispatcher.api34Complete()
+ rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
+ }
+
+ fun testPredictiveBackHandlerDisabledAfterStart() {
+ val result = mutableListOf<String>()
+ var count by mutableStateOf(2)
+ lateinit var dispatcherOwner: TestOnBackPressedDispatcherOwner
+ lateinit var dispatcher: OnBackPressedDispatcher
+ var started = false
+
+ rule.setContent {
+ dispatcherOwner =
+ TestOnBackPressedDispatcherOwner(LocalLifecycleOwner.current.lifecycle)
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides dispatcherOwner) {
+ PredictiveBackHandler(count > 1) { progress ->
+ if (count <= 1) {
+ started = true
+ }
+ progress.collect()
+ result += "onBack"
+ }
+ dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
+ }
+ }
+
+ dispatcher.startGestureBack()
+ // Changing the count right after starting the gesture is not received in the
+ // onBackStackStarted callback
+ count = 1
+
+ rule.runOnIdle { assertThat(started).isFalse() }
+ dispatcher.api34Complete()
+ rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
+ }
+
@Test(expected = IllegalStateException::class)
fun testNoCollection() {
val result = mutableListOf<String>()
diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt
index 1000765..3bdbb96 100644
--- a/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt
+++ b/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt
@@ -76,10 +76,10 @@
// ensure we don't re-register callbacks when onBack changes
val currentOnBack by rememberUpdatedState(onBack)
val onBackScope = rememberCoroutineScope()
+ var onBackInstance: OnBackInstance? = null
val backCallBack = remember {
object : OnBackPressedCallback(enabled) {
- var onBackInstance: OnBackInstance? = null
override fun handleOnBackStarted(backEvent: BackEventCompat) {
super.handleOnBackStarted(backEvent)
@@ -125,7 +125,13 @@
}
}
- LaunchedEffect(enabled) { backCallBack.isEnabled = enabled }
+ LaunchedEffect(enabled) {
+ backCallBack.isEnabled = enabled
+ if (!enabled) {
+ onBackInstance?.close()
+ onBackInstance = null
+ }
+ }
val backDispatcher =
checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
diff --git a/activity/activity-ktx/api/res-1.10.0-beta01.txt b/activity/activity-ktx/api/res-1.10.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/activity/activity-ktx/api/res-1.10.0-beta01.txt
+++ /dev/null
diff --git a/activity/activity-ktx/api/restricted_1.10.0-beta01.txt b/activity/activity-ktx/api/restricted_1.10.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/activity/activity-ktx/api/restricted_1.10.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/activity/activity/api/1.10.0-beta01.txt b/activity/activity/api/1.10.0-beta01.txt
deleted file mode 100644
index be7377b..0000000
--- a/activity/activity/api/1.10.0-beta01.txt
+++ /dev/null
@@ -1,548 +0,0 @@
-// Signature format: 4.0
-package androidx.activity {
-
- public final class ActivityViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<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);
- method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- }
-
- public final class BackEventCompat {
- ctor @RequiresApi(34) public BackEventCompat(android.window.BackEvent backEvent);
- ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, @FloatRange(from=0.0, to=1.0) float progress, int swipeEdge);
- method public float getProgress();
- method public int getSwipeEdge();
- method public float getTouchX();
- method public float getTouchY();
- method @RequiresApi(34) public android.window.BackEvent toBackEvent();
- property public final float progress;
- property public final int swipeEdge;
- property public final float touchX;
- property public final float touchY;
- field public static final androidx.activity.BackEventCompat.Companion Companion;
- field public static final int EDGE_LEFT = 0; // 0x0
- field public static final int EDGE_RIGHT = 1; // 0x1
- }
-
- public static final class BackEventCompat.Companion {
- }
-
- public class ComponentActivity extends android.app.Activity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.activity.FullyDrawnReporterOwner androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnMultiWindowModeChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.app.OnPictureInPictureModeChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.core.app.OnUserLeaveHintProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
- ctor public ComponentActivity();
- ctor @ContentView public ComponentActivity(@LayoutRes int contentLayoutId);
- method public void addMenuProvider(androidx.core.view.MenuProvider provider);
- method public void addMenuProvider(androidx.core.view.MenuProvider provider, androidx.lifecycle.LifecycleOwner owner);
- method public void addMenuProvider(androidx.core.view.MenuProvider provider, androidx.lifecycle.LifecycleOwner owner, androidx.lifecycle.Lifecycle.State state);
- method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration> listener);
- method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public final void addOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo> listener);
- method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent> listener);
- method public final void addOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo> listener);
- method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer> listener);
- method public final void addOnUserLeaveHintListener(Runnable listener);
- method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
- method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
- method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
- method @Deprecated public Object? getLastCustomNonConfigurationInstance();
- method public androidx.lifecycle.Lifecycle getLifecycle();
- method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
- method public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
- method public androidx.lifecycle.ViewModelStore getViewModelStore();
- method @CallSuper public void initializeViewTreeOwners();
- method public void invalidateMenu();
- method @Deprecated @CallSuper protected void onActivityResult(int requestCode, int resultCode, android.content.Intent? data);
- method @Deprecated @CallSuper public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
- method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
- method public final Object? onRetainNonConfigurationInstance();
- method public android.content.Context? peekAvailableContext();
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultRegistry registry, androidx.activity.result.ActivityResultCallback<O> callback);
- method public void removeMenuProvider(androidx.core.view.MenuProvider provider);
- method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration> listener);
- method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public final void removeOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo> listener);
- method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent> listener);
- method public final void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo> listener);
- method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer> listener);
- method public final void removeOnUserLeaveHintListener(Runnable listener);
- method @Deprecated public void startActivityForResult(android.content.Intent intent, int requestCode);
- method @Deprecated public void startActivityForResult(android.content.Intent intent, int requestCode, android.os.Bundle? options);
- method @Deprecated @kotlin.jvm.Throws(exceptionClasses=SendIntentException::class) public void startIntentSenderForResult(android.content.IntentSender intent, int requestCode, android.content.Intent? fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws android.content.IntentSender.SendIntentException;
- method @Deprecated @kotlin.jvm.Throws(exceptionClasses=SendIntentException::class) public void startIntentSenderForResult(android.content.IntentSender intent, int requestCode, android.content.Intent? fillInIntent, int flagsMask, int flagsValues, int extraFlags, android.os.Bundle? options) throws android.content.IntentSender.SendIntentException;
- property public final androidx.activity.result.ActivityResultRegistry activityResultRegistry;
- property @CallSuper public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
- property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
- property public androidx.activity.FullyDrawnReporter fullyDrawnReporter;
- property @Deprecated public Object? lastCustomNonConfigurationInstance;
- property public androidx.lifecycle.Lifecycle lifecycle;
- property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
- property public final androidx.savedstate.SavedStateRegistry savedStateRegistry;
- property public androidx.lifecycle.ViewModelStore viewModelStore;
- }
-
- public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
- ctor public ComponentDialog(android.content.Context context);
- ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
- method public androidx.lifecycle.Lifecycle getLifecycle();
- method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
- method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
- method @CallSuper public void initializeViewTreeOwners();
- method @CallSuper public void onBackPressed();
- property public androidx.lifecycle.Lifecycle lifecycle;
- property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
- property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
- }
-
- public final class EdgeToEdge {
- method public static void enable(androidx.activity.ComponentActivity);
- method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle);
- method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle, optional androidx.activity.SystemBarStyle navigationBarStyle);
- }
-
- public final class FullyDrawnReporter {
- ctor public FullyDrawnReporter(java.util.concurrent.Executor executor, kotlin.jvm.functions.Function0<kotlin.Unit> reportFullyDrawn);
- method public void addOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
- method public void addReporter();
- method public boolean isFullyDrawnReported();
- method public void removeOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
- method public void removeReporter();
- property public final boolean isFullyDrawnReported;
- }
-
- public final class FullyDrawnReporterKt {
- method public static suspend inline Object? reportWhenComplete(androidx.activity.FullyDrawnReporter, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> reporter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- }
-
- public interface FullyDrawnReporterOwner {
- method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
- property public abstract androidx.activity.FullyDrawnReporter fullyDrawnReporter;
- }
-
- public abstract class OnBackPressedCallback {
- ctor public OnBackPressedCallback(boolean enabled);
- method @MainThread public void handleOnBackCancelled();
- method @MainThread public abstract void handleOnBackPressed();
- method @MainThread public void handleOnBackProgressed(androidx.activity.BackEventCompat backEvent);
- method @MainThread public void handleOnBackStarted(androidx.activity.BackEventCompat backEvent);
- method @MainThread public final boolean isEnabled();
- method @MainThread public final void remove();
- method @MainThread public final void setEnabled(boolean);
- property @MainThread public final boolean isEnabled;
- }
-
- public final class OnBackPressedDispatcher {
- ctor public OnBackPressedDispatcher();
- ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
- ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
- method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
- method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
- method @MainThread @VisibleForTesting public void dispatchOnBackCancelled();
- method @MainThread @VisibleForTesting public void dispatchOnBackProgressed(androidx.activity.BackEventCompat backEvent);
- method @MainThread @VisibleForTesting public void dispatchOnBackStarted(androidx.activity.BackEventCompat backEvent);
- method @MainThread public boolean hasEnabledCallbacks();
- method @MainThread public void onBackPressed();
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
- }
-
- 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 {
- method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
- property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
- }
-
- public final class PipHintTrackerKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public static suspend Object? trackPipAnimationHintView(android.app.Activity, android.view.View view, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- }
-
- public final class SystemBarStyle {
- method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
- method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
- method public static androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
- method public static androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
- field public static final androidx.activity.SystemBarStyle.Companion Companion;
- }
-
- public static final class SystemBarStyle.Companion {
- method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
- method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
- method public androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
- method public androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
- }
-
- public final class ViewTreeFullyDrawnReporterOwner {
- method public static androidx.activity.FullyDrawnReporterOwner? get(android.view.View);
- method public static void set(android.view.View, androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
- }
-
- public final class ViewTreeOnBackPressedDispatcherOwner {
- method public static androidx.activity.OnBackPressedDispatcherOwner? get(android.view.View);
- method public static void set(android.view.View, androidx.activity.OnBackPressedDispatcherOwner onBackPressedDispatcherOwner);
- }
-
-}
-
-package androidx.activity.contextaware {
-
- public interface ContextAware {
- method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public android.content.Context? peekAvailableContext();
- method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- }
-
- public final class ContextAwareHelper {
- ctor public ContextAwareHelper();
- method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public void clearAvailableContext();
- method public void dispatchOnContextAvailable(android.content.Context context);
- method public android.content.Context? peekAvailableContext();
- method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- }
-
- public final class ContextAwareKt {
- method public static suspend inline <R> Object? withContextAvailable(androidx.activity.contextaware.ContextAware, kotlin.jvm.functions.Function1<android.content.Context,R> onContextAvailable, kotlin.coroutines.Continuation<R>);
- }
-
- public fun interface OnContextAvailableListener {
- method public void onContextAvailable(android.content.Context context);
- }
-
-}
-
-package androidx.activity.result {
-
- public final class ActivityResult implements android.os.Parcelable {
- ctor public ActivityResult(int resultCode, android.content.Intent? data);
- method public int describeContents();
- method public android.content.Intent? getData();
- method public int getResultCode();
- method public static String resultCodeToString(int resultCode);
- method public void writeToParcel(android.os.Parcel dest, int flags);
- property public final android.content.Intent? data;
- property public final int resultCode;
- field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult> CREATOR;
- field public static final androidx.activity.result.ActivityResult.Companion Companion;
- }
-
- public static final class ActivityResult.Companion {
- method public String resultCodeToString(int resultCode);
- }
-
- public fun interface ActivityResultCallback<O> {
- method public void onActivityResult(O result);
- }
-
- public interface ActivityResultCaller {
- method public <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- method public <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultRegistry registry, androidx.activity.result.ActivityResultCallback<O> callback);
- }
-
- public final class ActivityResultCallerKt {
- method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, androidx.activity.result.ActivityResultRegistry registry, kotlin.jvm.functions.Function1<O,kotlin.Unit> callback);
- method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, kotlin.jvm.functions.Function1<O,kotlin.Unit> callback);
- }
-
- public final class ActivityResultKt {
- method public static operator int component1(androidx.activity.result.ActivityResult);
- method public static operator android.content.Intent? component2(androidx.activity.result.ActivityResult);
- }
-
- public abstract class ActivityResultLauncher<I> {
- ctor public ActivityResultLauncher();
- method public abstract androidx.activity.result.contract.ActivityResultContract<I,? extends java.lang.Object?> getContract();
- method public void launch(I input);
- method public abstract void launch(I input, androidx.core.app.ActivityOptionsCompat? options);
- method @MainThread public abstract void unregister();
- property public abstract androidx.activity.result.contract.ActivityResultContract<I,? extends java.lang.Object?> contract;
- }
-
- public final class ActivityResultLauncherKt {
- method public static void launch(androidx.activity.result.ActivityResultLauncher<java.lang.Void?>, optional androidx.core.app.ActivityOptionsCompat? options);
- method public static void launchUnit(androidx.activity.result.ActivityResultLauncher<kotlin.Unit>, optional androidx.core.app.ActivityOptionsCompat? options);
- }
-
- public abstract class ActivityResultRegistry {
- ctor public ActivityResultRegistry();
- method @MainThread public final boolean dispatchResult(int requestCode, int resultCode, android.content.Intent? data);
- method @MainThread public final <O> boolean dispatchResult(int requestCode, O result);
- method @MainThread public abstract <I, O> void onLaunch(int requestCode, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, androidx.core.app.ActivityOptionsCompat? options);
- method public final void onRestoreInstanceState(android.os.Bundle? savedInstanceState);
- method public final void onSaveInstanceState(android.os.Bundle outState);
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> register(String key, androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> register(String key, androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- }
-
- public interface ActivityResultRegistryOwner {
- method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
- property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
- }
-
- public final class IntentSenderRequest implements android.os.Parcelable {
- method public int describeContents();
- method public android.content.Intent? getFillInIntent();
- method public int getFlagsMask();
- method public int getFlagsValues();
- method public android.content.IntentSender getIntentSender();
- method public void writeToParcel(android.os.Parcel dest, int flags);
- property public final android.content.Intent? fillInIntent;
- property public final int flagsMask;
- property public final int flagsValues;
- property public final android.content.IntentSender intentSender;
- field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
- field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
- }
-
- public static final class IntentSenderRequest.Builder {
- ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
- ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
- method public androidx.activity.result.IntentSenderRequest build();
- method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
- method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
- }
-
- public static final class IntentSenderRequest.Companion {
- }
-
- public final class PickVisualMediaRequest {
- method public long getAccentColor();
- method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab getDefaultTab();
- method public int getMaxItems();
- method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
- method public boolean isCustomAccentColorApplied();
- method public boolean isOrderedSelection();
- property public final long accentColor;
- property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab;
- property public final boolean isCustomAccentColorApplied;
- property public final boolean isOrderedSelection;
- property public final int maxItems;
- property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
- }
-
- public static final class PickVisualMediaRequest.Builder {
- ctor public PickVisualMediaRequest.Builder();
- method public androidx.activity.result.PickVisualMediaRequest build();
- method public androidx.activity.result.PickVisualMediaRequest.Builder setAccentColor(long accentColor);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setDefaultTab(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setMaxItems(@IntRange(from=2L) int maxItems);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setOrderedSelection(boolean isOrderedSelection);
- }
-
- public final class PickVisualMediaRequestKt {
- method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
- method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
- method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
- method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(long accentColor, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
- }
-
-}
-
-package androidx.activity.result.contract {
-
- public abstract class ActivityResultContract<I, O> {
- ctor public ActivityResultContract();
- method public abstract android.content.Intent createIntent(android.content.Context context, I input);
- method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<O>? getSynchronousResult(android.content.Context context, I input);
- method public abstract O parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static final class ActivityResultContract.SynchronousResult<T> {
- ctor public ActivityResultContract.SynchronousResult(T value);
- method public T getValue();
- property public final T value;
- }
-
- public final class ActivityResultContracts {
- }
-
- public static class ActivityResultContracts.CaptureVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
- ctor public ActivityResultContracts.CaptureVideo();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
- method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
- ctor @Deprecated public ActivityResultContracts.CreateDocument();
- ctor public ActivityResultContracts.CreateDocument(String mimeType);
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
- ctor public ActivityResultContracts.GetContent();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.GetMultipleContents extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.util.List<android.net.Uri>> {
- ctor public ActivityResultContracts.GetMultipleContents();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String input);
- method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri?> {
- ctor public ActivityResultContracts.OpenDocument();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String[] input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri?,android.net.Uri?> {
- ctor public ActivityResultContracts.OpenDocumentTree();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri? input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, android.net.Uri? input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.OpenMultipleDocuments extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.List<android.net.Uri>> {
- ctor public ActivityResultContracts.OpenMultipleDocuments();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String[] input);
- method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.net.Uri?> {
- ctor public ActivityResultContracts.PickContact();
- method public android.content.Intent createIntent(android.content.Context context, Void? input);
- method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
- ctor public ActivityResultContracts.PickMultipleVisualMedia();
- ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri?> {
- ctor public ActivityResultContracts.PickVisualMedia();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method @Deprecated public static final boolean isPhotoPickerAvailable();
- method public static final boolean isPhotoPickerAvailable(android.content.Context context);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR = "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR";
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER = "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER";
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB = "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB";
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.Companion {
- method @Deprecated public boolean isPhotoPickerAvailable();
- method public boolean isPhotoPickerAvailable(android.content.Context context);
- }
-
- public abstract static class ActivityResultContracts.PickVisualMedia.DefaultTab {
- method public abstract int getValue();
- property public abstract int value;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
- method public int getValue();
- property public int value;
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
- method public int getValue();
- property public int value;
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
- method public String getMimeType();
- property public final String mimeType;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
- }
-
- public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
- }
-
- public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
- ctor public ActivityResultContracts.RequestMultiplePermissions();
- method public android.content.Intent createIntent(android.content.Context context, String[] input);
- method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.Map<java.lang.String,java.lang.Boolean>>? getSynchronousResult(android.content.Context context, String[] input);
- method public java.util.Map<java.lang.String,java.lang.Boolean> parseResult(int resultCode, android.content.Intent? intent);
- field public static final String ACTION_REQUEST_PERMISSIONS = "androidx.activity.result.contract.action.REQUEST_PERMISSIONS";
- field public static final androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion Companion;
- field public static final String EXTRA_PERMISSIONS = "androidx.activity.result.contract.extra.PERMISSIONS";
- field public static final String EXTRA_PERMISSION_GRANT_RESULTS = "androidx.activity.result.contract.extra.PERMISSION_GRANT_RESULTS";
- }
-
- public static final class ActivityResultContracts.RequestMultiplePermissions.Companion {
- }
-
- public static final class ActivityResultContracts.RequestPermission extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.lang.Boolean> {
- ctor public ActivityResultContracts.RequestPermission();
- method public android.content.Intent createIntent(android.content.Context context, String input);
- method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, String input);
- method public Boolean parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static final class ActivityResultContracts.StartActivityForResult extends androidx.activity.result.contract.ActivityResultContract<android.content.Intent,androidx.activity.result.ActivityResult> {
- ctor public ActivityResultContracts.StartActivityForResult();
- method public android.content.Intent createIntent(android.content.Context context, android.content.Intent input);
- method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
- field public static final androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult.Companion Companion;
- field public static final String EXTRA_ACTIVITY_OPTIONS_BUNDLE = "androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE";
- }
-
- public static final class ActivityResultContracts.StartActivityForResult.Companion {
- }
-
- public static final class ActivityResultContracts.StartIntentSenderForResult extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.IntentSenderRequest,androidx.activity.result.ActivityResult> {
- ctor public ActivityResultContracts.StartIntentSenderForResult();
- method public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.IntentSenderRequest input);
- method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
- field public static final String ACTION_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.action.INTENT_SENDER_REQUEST";
- field public static final androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion Companion;
- field public static final String EXTRA_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.extra.INTENT_SENDER_REQUEST";
- field public static final String EXTRA_SEND_INTENT_EXCEPTION = "androidx.activity.result.contract.extra.SEND_INTENT_EXCEPTION";
- }
-
- public static final class ActivityResultContracts.StartIntentSenderForResult.Companion {
- }
-
- public static class ActivityResultContracts.TakePicture extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
- ctor public ActivityResultContracts.TakePicture();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
- method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.graphics.Bitmap?> {
- ctor public ActivityResultContracts.TakePicturePreview();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, Void? input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, Void? input);
- method public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap?> {
- ctor @Deprecated public ActivityResultContracts.TakeVideo();
- method @Deprecated @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
- method @Deprecated public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, android.net.Uri input);
- method @Deprecated public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
- }
-
-}
-
diff --git a/activity/activity/api/restricted_1.10.0-beta01.txt b/activity/activity/api/restricted_1.10.0-beta01.txt
deleted file mode 100644
index 3fc0729..0000000
--- a/activity/activity/api/restricted_1.10.0-beta01.txt
+++ /dev/null
@@ -1,547 +0,0 @@
-// Signature format: 4.0
-package androidx.activity {
-
- public final class ActivityViewModelLazyKt {
- method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<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);
- method @Deprecated @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.activity.ComponentActivity, optional kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer);
- }
-
- public final class BackEventCompat {
- ctor @RequiresApi(34) public BackEventCompat(android.window.BackEvent backEvent);
- ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, @FloatRange(from=0.0, to=1.0) float progress, int swipeEdge);
- method public float getProgress();
- method public int getSwipeEdge();
- method public float getTouchX();
- method public float getTouchY();
- method @RequiresApi(34) public android.window.BackEvent toBackEvent();
- property public final float progress;
- property public final int swipeEdge;
- property public final float touchX;
- property public final float touchY;
- field public static final androidx.activity.BackEventCompat.Companion Companion;
- field public static final int EDGE_LEFT = 0; // 0x0
- field public static final int EDGE_RIGHT = 1; // 0x1
- }
-
- public static final class BackEventCompat.Companion {
- }
-
- public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.activity.FullyDrawnReporterOwner androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnMultiWindowModeChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.app.OnPictureInPictureModeChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.core.app.OnUserLeaveHintProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
- ctor public ComponentActivity();
- ctor @ContentView public ComponentActivity(@LayoutRes int contentLayoutId);
- method public void addMenuProvider(androidx.core.view.MenuProvider provider);
- method public void addMenuProvider(androidx.core.view.MenuProvider provider, androidx.lifecycle.LifecycleOwner owner);
- method public void addMenuProvider(androidx.core.view.MenuProvider provider, androidx.lifecycle.LifecycleOwner owner, androidx.lifecycle.Lifecycle.State state);
- method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration> listener);
- method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public final void addOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo> listener);
- method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent> listener);
- method public final void addOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo> listener);
- method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer> listener);
- method public final void addOnUserLeaveHintListener(Runnable listener);
- method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
- method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
- method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
- method @Deprecated public Object? getLastCustomNonConfigurationInstance();
- method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
- method public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
- method public androidx.lifecycle.ViewModelStore getViewModelStore();
- method @CallSuper public void initializeViewTreeOwners();
- method public void invalidateMenu();
- method @Deprecated @CallSuper protected void onActivityResult(int requestCode, int resultCode, android.content.Intent? data);
- method @Deprecated @CallSuper public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
- method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
- method public final Object? onRetainNonConfigurationInstance();
- method public android.content.Context? peekAvailableContext();
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultRegistry registry, androidx.activity.result.ActivityResultCallback<O> callback);
- method public void removeMenuProvider(androidx.core.view.MenuProvider provider);
- method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration> listener);
- method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public final void removeOnMultiWindowModeChangedListener(androidx.core.util.Consumer<androidx.core.app.MultiWindowModeChangedInfo> listener);
- method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent> listener);
- method public final void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo> listener);
- method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer> listener);
- method public final void removeOnUserLeaveHintListener(Runnable listener);
- method @Deprecated public void startActivityForResult(android.content.Intent intent, int requestCode);
- method @Deprecated public void startActivityForResult(android.content.Intent intent, int requestCode, android.os.Bundle? options);
- method @Deprecated @kotlin.jvm.Throws(exceptionClasses=SendIntentException::class) public void startIntentSenderForResult(android.content.IntentSender intent, int requestCode, android.content.Intent? fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws android.content.IntentSender.SendIntentException;
- method @Deprecated @kotlin.jvm.Throws(exceptionClasses=SendIntentException::class) public void startIntentSenderForResult(android.content.IntentSender intent, int requestCode, android.content.Intent? fillInIntent, int flagsMask, int flagsValues, int extraFlags, android.os.Bundle? options) throws android.content.IntentSender.SendIntentException;
- property public final androidx.activity.result.ActivityResultRegistry activityResultRegistry;
- property @CallSuper public androidx.lifecycle.viewmodel.CreationExtras defaultViewModelCreationExtras;
- property public androidx.lifecycle.ViewModelProvider.Factory defaultViewModelProviderFactory;
- property public androidx.activity.FullyDrawnReporter fullyDrawnReporter;
- property @Deprecated public Object? lastCustomNonConfigurationInstance;
- property public androidx.lifecycle.Lifecycle lifecycle;
- property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
- property public final androidx.savedstate.SavedStateRegistry savedStateRegistry;
- property public androidx.lifecycle.ViewModelStore viewModelStore;
- }
-
- public class ComponentDialog extends android.app.Dialog implements androidx.lifecycle.LifecycleOwner androidx.activity.OnBackPressedDispatcherOwner androidx.savedstate.SavedStateRegistryOwner {
- ctor public ComponentDialog(android.content.Context context);
- ctor public ComponentDialog(android.content.Context context, optional @StyleRes int themeResId);
- method public androidx.lifecycle.Lifecycle getLifecycle();
- method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
- method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
- method @CallSuper public void initializeViewTreeOwners();
- method @CallSuper public void onBackPressed();
- property public androidx.lifecycle.Lifecycle lifecycle;
- property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
- property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
- }
-
- public final class EdgeToEdge {
- method public static void enable(androidx.activity.ComponentActivity);
- method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle);
- method public static void enable(androidx.activity.ComponentActivity, optional androidx.activity.SystemBarStyle statusBarStyle, optional androidx.activity.SystemBarStyle navigationBarStyle);
- }
-
- public final class FullyDrawnReporter {
- ctor public FullyDrawnReporter(java.util.concurrent.Executor executor, kotlin.jvm.functions.Function0<kotlin.Unit> reportFullyDrawn);
- method public void addOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
- method public void addReporter();
- method public boolean isFullyDrawnReported();
- method public void removeOnReportDrawnListener(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
- method public void removeReporter();
- property public final boolean isFullyDrawnReported;
- }
-
- public final class FullyDrawnReporterKt {
- method public static suspend inline Object? reportWhenComplete(androidx.activity.FullyDrawnReporter, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> reporter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- }
-
- public interface FullyDrawnReporterOwner {
- method public androidx.activity.FullyDrawnReporter getFullyDrawnReporter();
- property public abstract androidx.activity.FullyDrawnReporter fullyDrawnReporter;
- }
-
- public abstract class OnBackPressedCallback {
- ctor public OnBackPressedCallback(boolean enabled);
- method @MainThread public void handleOnBackCancelled();
- method @MainThread public abstract void handleOnBackPressed();
- method @MainThread public void handleOnBackProgressed(androidx.activity.BackEventCompat backEvent);
- method @MainThread public void handleOnBackStarted(androidx.activity.BackEventCompat backEvent);
- method @MainThread public final boolean isEnabled();
- method @MainThread public final void remove();
- method @MainThread public final void setEnabled(boolean);
- property @MainThread public final boolean isEnabled;
- }
-
- public final class OnBackPressedDispatcher {
- ctor public OnBackPressedDispatcher();
- ctor public OnBackPressedDispatcher(optional Runnable? fallbackOnBackPressed);
- ctor public OnBackPressedDispatcher(Runnable? fallbackOnBackPressed, androidx.core.util.Consumer<java.lang.Boolean>? onHasEnabledCallbacksChanged);
- method @MainThread public void addCallback(androidx.activity.OnBackPressedCallback onBackPressedCallback);
- method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner owner, androidx.activity.OnBackPressedCallback onBackPressedCallback);
- method @MainThread @VisibleForTesting public void dispatchOnBackCancelled();
- method @MainThread @VisibleForTesting public void dispatchOnBackProgressed(androidx.activity.BackEventCompat backEvent);
- method @MainThread @VisibleForTesting public void dispatchOnBackStarted(androidx.activity.BackEventCompat backEvent);
- method @MainThread public boolean hasEnabledCallbacks();
- method @MainThread public void onBackPressed();
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher invoker);
- }
-
- 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 {
- method public androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
- property public abstract androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
- }
-
- public final class PipHintTrackerKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public static suspend Object? trackPipAnimationHintView(android.app.Activity, android.view.View view, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- }
-
- public final class SystemBarStyle {
- method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
- method public static androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
- method public static androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
- method public static androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
- field public static final androidx.activity.SystemBarStyle.Companion Companion;
- }
-
- public static final class SystemBarStyle.Companion {
- method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim);
- method public androidx.activity.SystemBarStyle auto(@ColorInt int lightScrim, @ColorInt int darkScrim, optional kotlin.jvm.functions.Function1<? super android.content.res.Resources,java.lang.Boolean> detectDarkMode);
- method public androidx.activity.SystemBarStyle dark(@ColorInt int scrim);
- method public androidx.activity.SystemBarStyle light(@ColorInt int scrim, @ColorInt int darkScrim);
- }
-
- public final class ViewTreeFullyDrawnReporterOwner {
- method public static androidx.activity.FullyDrawnReporterOwner? get(android.view.View);
- method public static void set(android.view.View, androidx.activity.FullyDrawnReporterOwner fullyDrawnReporterOwner);
- }
-
- public final class ViewTreeOnBackPressedDispatcherOwner {
- method public static androidx.activity.OnBackPressedDispatcherOwner? get(android.view.View);
- method public static void set(android.view.View, androidx.activity.OnBackPressedDispatcherOwner onBackPressedDispatcherOwner);
- }
-
-}
-
-package androidx.activity.contextaware {
-
- public interface ContextAware {
- method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public android.content.Context? peekAvailableContext();
- method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- }
-
- public final class ContextAwareHelper {
- ctor public ContextAwareHelper();
- method public void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- method public void clearAvailableContext();
- method public void dispatchOnContextAvailable(android.content.Context context);
- method public android.content.Context? peekAvailableContext();
- method public void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener listener);
- }
-
- public final class ContextAwareKt {
- method public static suspend inline <R> Object? withContextAvailable(androidx.activity.contextaware.ContextAware, kotlin.jvm.functions.Function1<android.content.Context,R> onContextAvailable, kotlin.coroutines.Continuation<R>);
- }
-
- public fun interface OnContextAvailableListener {
- method public void onContextAvailable(android.content.Context context);
- }
-
-}
-
-package androidx.activity.result {
-
- public final class ActivityResult implements android.os.Parcelable {
- ctor public ActivityResult(int resultCode, android.content.Intent? data);
- method public int describeContents();
- method public android.content.Intent? getData();
- method public int getResultCode();
- method public static String resultCodeToString(int resultCode);
- method public void writeToParcel(android.os.Parcel dest, int flags);
- property public final android.content.Intent? data;
- property public final int resultCode;
- field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult> CREATOR;
- field public static final androidx.activity.result.ActivityResult.Companion Companion;
- }
-
- public static final class ActivityResult.Companion {
- method public String resultCodeToString(int resultCode);
- }
-
- public fun interface ActivityResultCallback<O> {
- method public void onActivityResult(O result);
- }
-
- public interface ActivityResultCaller {
- method public <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- method public <I, O> androidx.activity.result.ActivityResultLauncher<I> registerForActivityResult(androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultRegistry registry, androidx.activity.result.ActivityResultCallback<O> callback);
- }
-
- public final class ActivityResultCallerKt {
- method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, androidx.activity.result.ActivityResultRegistry registry, kotlin.jvm.functions.Function1<O,kotlin.Unit> callback);
- method public static <I, O> androidx.activity.result.ActivityResultLauncher<kotlin.Unit> registerForActivityResult(androidx.activity.result.ActivityResultCaller, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, kotlin.jvm.functions.Function1<O,kotlin.Unit> callback);
- }
-
- public final class ActivityResultKt {
- method public static operator int component1(androidx.activity.result.ActivityResult);
- method public static operator android.content.Intent? component2(androidx.activity.result.ActivityResult);
- }
-
- public abstract class ActivityResultLauncher<I> {
- ctor public ActivityResultLauncher();
- method public abstract androidx.activity.result.contract.ActivityResultContract<I,? extends java.lang.Object?> getContract();
- method public void launch(I input);
- method public abstract void launch(I input, androidx.core.app.ActivityOptionsCompat? options);
- method @MainThread public abstract void unregister();
- property public abstract androidx.activity.result.contract.ActivityResultContract<I,? extends java.lang.Object?> contract;
- }
-
- public final class ActivityResultLauncherKt {
- method public static void launch(androidx.activity.result.ActivityResultLauncher<java.lang.Void?>, optional androidx.core.app.ActivityOptionsCompat? options);
- method public static void launchUnit(androidx.activity.result.ActivityResultLauncher<kotlin.Unit>, optional androidx.core.app.ActivityOptionsCompat? options);
- }
-
- public abstract class ActivityResultRegistry {
- ctor public ActivityResultRegistry();
- method @MainThread public final boolean dispatchResult(int requestCode, int resultCode, android.content.Intent? data);
- method @MainThread public final <O> boolean dispatchResult(int requestCode, O result);
- method @MainThread public abstract <I, O> void onLaunch(int requestCode, androidx.activity.result.contract.ActivityResultContract<I,O> contract, I input, androidx.core.app.ActivityOptionsCompat? options);
- method public final void onRestoreInstanceState(android.os.Bundle? savedInstanceState);
- method public final void onSaveInstanceState(android.os.Bundle outState);
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> register(String key, androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- method public final <I, O> androidx.activity.result.ActivityResultLauncher<I> register(String key, androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.activity.result.contract.ActivityResultContract<I,O> contract, androidx.activity.result.ActivityResultCallback<O> callback);
- }
-
- public interface ActivityResultRegistryOwner {
- method public androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
- property public abstract androidx.activity.result.ActivityResultRegistry activityResultRegistry;
- }
-
- public final class IntentSenderRequest implements android.os.Parcelable {
- method public int describeContents();
- method public android.content.Intent? getFillInIntent();
- method public int getFlagsMask();
- method public int getFlagsValues();
- method public android.content.IntentSender getIntentSender();
- method public void writeToParcel(android.os.Parcel dest, int flags);
- property public final android.content.Intent? fillInIntent;
- property public final int flagsMask;
- property public final int flagsValues;
- property public final android.content.IntentSender intentSender;
- field public static final android.os.Parcelable.Creator<androidx.activity.result.IntentSenderRequest> CREATOR;
- field public static final androidx.activity.result.IntentSenderRequest.Companion Companion;
- }
-
- public static final class IntentSenderRequest.Builder {
- ctor public IntentSenderRequest.Builder(android.app.PendingIntent pendingIntent);
- ctor public IntentSenderRequest.Builder(android.content.IntentSender intentSender);
- method public androidx.activity.result.IntentSenderRequest build();
- method public androidx.activity.result.IntentSenderRequest.Builder setFillInIntent(android.content.Intent? fillInIntent);
- method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int values, int mask);
- }
-
- public static final class IntentSenderRequest.Companion {
- }
-
- public final class PickVisualMediaRequest {
- method public long getAccentColor();
- method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab getDefaultTab();
- method public int getMaxItems();
- method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
- method public boolean isCustomAccentColorApplied();
- method public boolean isOrderedSelection();
- property public final long accentColor;
- property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab;
- property public final boolean isCustomAccentColorApplied;
- property public final boolean isOrderedSelection;
- property public final int maxItems;
- property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
- }
-
- public static final class PickVisualMediaRequest.Builder {
- ctor public PickVisualMediaRequest.Builder();
- method public androidx.activity.result.PickVisualMediaRequest build();
- method public androidx.activity.result.PickVisualMediaRequest.Builder setAccentColor(long accentColor);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setDefaultTab(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setMaxItems(@IntRange(from=2L) int maxItems);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
- method public androidx.activity.result.PickVisualMediaRequest.Builder setOrderedSelection(boolean isOrderedSelection);
- }
-
- public final class PickVisualMediaRequestKt {
- method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
- method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
- method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
- method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(long accentColor, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
- }
-
-}
-
-package androidx.activity.result.contract {
-
- public abstract class ActivityResultContract<I, O> {
- ctor public ActivityResultContract();
- method public abstract android.content.Intent createIntent(android.content.Context context, I input);
- method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<O>? getSynchronousResult(android.content.Context context, I input);
- method public abstract O parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static final class ActivityResultContract.SynchronousResult<T> {
- ctor public ActivityResultContract.SynchronousResult(T value);
- method public T getValue();
- property public final T value;
- }
-
- public final class ActivityResultContracts {
- }
-
- public static class ActivityResultContracts.CaptureVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
- ctor public ActivityResultContracts.CaptureVideo();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
- method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.CreateDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
- ctor @Deprecated public ActivityResultContracts.CreateDocument();
- ctor public ActivityResultContracts.CreateDocument(String mimeType);
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.GetContent extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,android.net.Uri?> {
- ctor public ActivityResultContracts.GetContent();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.GetMultipleContents extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.util.List<android.net.Uri>> {
- ctor public ActivityResultContracts.GetMultipleContents();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String input);
- method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.OpenDocument extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],android.net.Uri?> {
- ctor public ActivityResultContracts.OpenDocument();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, String[] input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- @RequiresApi(21) public static class ActivityResultContracts.OpenDocumentTree extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri?,android.net.Uri?> {
- ctor public ActivityResultContracts.OpenDocumentTree();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri? input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, android.net.Uri? input);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.OpenMultipleDocuments extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.List<android.net.Uri>> {
- ctor public ActivityResultContracts.OpenMultipleDocuments();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, String[] input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, String[] input);
- method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static final class ActivityResultContracts.PickContact extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.net.Uri?> {
- ctor public ActivityResultContracts.PickContact();
- method public android.content.Intent createIntent(android.content.Context context, Void? input);
- method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
- ctor public ActivityResultContracts.PickMultipleVisualMedia();
- ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri?> {
- ctor public ActivityResultContracts.PickVisualMedia();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri?>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
- method @Deprecated public static final boolean isPhotoPickerAvailable();
- method public static final boolean isPhotoPickerAvailable(android.content.Context context);
- method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
- field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR = "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR";
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER = "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER";
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB = "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB";
- field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.Companion {
- method @Deprecated public boolean isPhotoPickerAvailable();
- method public boolean isPhotoPickerAvailable(android.content.Context context);
- }
-
- public abstract static class ActivityResultContracts.PickVisualMedia.DefaultTab {
- method public abstract int getValue();
- property public abstract int value;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
- method public int getValue();
- property public int value;
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
- method public int getValue();
- property public int value;
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
- method public String getMimeType();
- property public final String mimeType;
- }
-
- public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
- field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
- }
-
- public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
- }
-
- public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
- ctor public ActivityResultContracts.RequestMultiplePermissions();
- method public android.content.Intent createIntent(android.content.Context context, String[] input);
- method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.Map<java.lang.String,java.lang.Boolean>>? getSynchronousResult(android.content.Context context, String[] input);
- method public java.util.Map<java.lang.String,java.lang.Boolean> parseResult(int resultCode, android.content.Intent? intent);
- field public static final String ACTION_REQUEST_PERMISSIONS = "androidx.activity.result.contract.action.REQUEST_PERMISSIONS";
- field public static final androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.Companion Companion;
- field public static final String EXTRA_PERMISSIONS = "androidx.activity.result.contract.extra.PERMISSIONS";
- field public static final String EXTRA_PERMISSION_GRANT_RESULTS = "androidx.activity.result.contract.extra.PERMISSION_GRANT_RESULTS";
- }
-
- public static final class ActivityResultContracts.RequestMultiplePermissions.Companion {
- }
-
- public static final class ActivityResultContracts.RequestPermission extends androidx.activity.result.contract.ActivityResultContract<java.lang.String,java.lang.Boolean> {
- ctor public ActivityResultContracts.RequestPermission();
- method public android.content.Intent createIntent(android.content.Context context, String input);
- method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, String input);
- method public Boolean parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static final class ActivityResultContracts.StartActivityForResult extends androidx.activity.result.contract.ActivityResultContract<android.content.Intent,androidx.activity.result.ActivityResult> {
- ctor public ActivityResultContracts.StartActivityForResult();
- method public android.content.Intent createIntent(android.content.Context context, android.content.Intent input);
- method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
- field public static final androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult.Companion Companion;
- field public static final String EXTRA_ACTIVITY_OPTIONS_BUNDLE = "androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE";
- }
-
- public static final class ActivityResultContracts.StartActivityForResult.Companion {
- }
-
- public static final class ActivityResultContracts.StartIntentSenderForResult extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.IntentSenderRequest,androidx.activity.result.ActivityResult> {
- ctor public ActivityResultContracts.StartIntentSenderForResult();
- method public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.IntentSenderRequest input);
- method public androidx.activity.result.ActivityResult parseResult(int resultCode, android.content.Intent? intent);
- field public static final String ACTION_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.action.INTENT_SENDER_REQUEST";
- field public static final androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion Companion;
- field public static final String EXTRA_INTENT_SENDER_REQUEST = "androidx.activity.result.contract.extra.INTENT_SENDER_REQUEST";
- field public static final String EXTRA_SEND_INTENT_EXCEPTION = "androidx.activity.result.contract.extra.SEND_INTENT_EXCEPTION";
- }
-
- public static final class ActivityResultContracts.StartIntentSenderForResult.Companion {
- }
-
- public static class ActivityResultContracts.TakePicture extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,java.lang.Boolean> {
- ctor public ActivityResultContracts.TakePicture();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, android.net.Uri input);
- method public final Boolean parseResult(int resultCode, android.content.Intent? intent);
- }
-
- public static class ActivityResultContracts.TakePicturePreview extends androidx.activity.result.contract.ActivityResultContract<java.lang.Void?,android.graphics.Bitmap?> {
- ctor public ActivityResultContracts.TakePicturePreview();
- method @CallSuper public android.content.Intent createIntent(android.content.Context context, Void? input);
- method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, Void? input);
- method public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
- }
-
- @Deprecated public static class ActivityResultContracts.TakeVideo extends androidx.activity.result.contract.ActivityResultContract<android.net.Uri,android.graphics.Bitmap?> {
- ctor @Deprecated public ActivityResultContracts.TakeVideo();
- method @Deprecated @CallSuper public android.content.Intent createIntent(android.content.Context context, android.net.Uri input);
- method @Deprecated public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.graphics.Bitmap?>? getSynchronousResult(android.content.Context context, android.net.Uri input);
- method @Deprecated public final android.graphics.Bitmap? parseResult(int resultCode, android.content.Intent? intent);
- }
-
-}
-
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionCtsTestBase.java
index abd0ce4..2c9bf17 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionCtsTestBase.java
@@ -25,13 +25,21 @@
import androidx.appsearch.app.EnterpriseGlobalSearchSession;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
public abstract class EnterpriseGlobalSearchSessionCtsTestBase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
protected EnterpriseGlobalSearchSession mEnterpriseGlobalSearchSession;
protected abstract ListenableFuture<EnterpriseGlobalSearchSession>
@@ -42,6 +50,7 @@
mEnterpriseGlobalSearchSession = createEnterpriseGlobalSearchSessionAsync().get();
}
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX)
@Test
public void testGetByDocumentId_returnsNotFoundResults() throws Exception {
// The batch result may be empty instead of containing NOT_FOUND errors if the enterprise
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionPlatformCtsTest.java
index 968ce31..97f487a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/EnterpriseGlobalSearchSessionPlatformCtsTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+// @exportToFramework:skipFile()
package androidx.appsearch.cts.app;
import android.content.Context;
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
index b666e4d..74a03e5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
@@ -20,12 +20,16 @@
import static org.junit.Assert.assertThrows;
+import android.os.Build;
+import android.os.Parcel;
+
import androidx.appsearch.app.EmbeddingVector;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.flags.CheckFlagsRule;
import androidx.appsearch.flags.DeviceFlagsValueProvider;
import androidx.appsearch.flags.Flags;
import androidx.appsearch.flags.RequiresFlagsEnabled;
+import androidx.test.filters.SdkSuppress;
import org.junit.Rule;
import org.junit.Test;
@@ -1219,4 +1223,39 @@
() -> new EmbeddingVector(new float[]{}, "my_model"));
assertThat(exception).hasMessageThat().contains("Embedding values cannot be empty.");
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public void testWriteToParcel() {
+ GenericDocument inDoc =
+ new GenericDocument.Builder<>("namespace", "id1", "schema1")
+ .setScore(42)
+ .setPropertyString("propString", "Hello")
+ .setPropertyBytes("propBytes", new byte[][] {{1, 2}})
+ .setPropertyDocument(
+ "propDocument",
+ new GenericDocument.Builder<>("namespace", "id2", "schema2")
+ .setPropertyString("propString", "Goodbye")
+ .setPropertyBytes("propBytes", new byte[][] {{3, 4}})
+ .build())
+ .build();
+
+ // Serialize the document
+ Parcel parcel = Parcel.obtain();
+ inDoc.writeToParcel(parcel, /* flags= */ 0);
+
+ // Deserialize the document
+ parcel.setDataPosition(0);
+ GenericDocument document = GenericDocument.createFromParcel(parcel);
+ parcel.recycle();
+
+ // Compare results
+ assertThat(document.getPropertyString("propString")).isEqualTo("Hello");
+ assertThat(document.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][] {{1, 2}});
+ assertThat(document.getPropertyDocument("propDocument").getPropertyString("propString"))
+ .isEqualTo("Goodbye");
+ assertThat(document.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+ .isEqualTo(new byte[][] {{3, 4}});
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
index 958b2a7..d4856d7 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -113,7 +113,7 @@
public void testFlagValue_enableSearchSpecSearchStringParameters() {
assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS)
.isEqualTo(
- "com.android.appsearch.flags.enable_search_spec_search_spec_strings");
+ "com.android.appsearch.flags.enable_search_spec_search_string_parameters");
}
@Test
@@ -133,4 +133,10 @@
assertThat(Flags.FLAG_ENABLE_BLOB_STORE)
.isEqualTo("com.android.appsearch.flags.enable_blob_store");
}
+
+ @Test
+ public void testFlagValue_enableEnterpriseEmptyBatchResultFix() {
+ assertThat(Flags.FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX)
+ .isEqualTo("com.android.appsearch.flags.enable_enterprise_empty_batch_result_fix");
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/CurrentTimeMillisLong.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/CurrentTimeMillisLong.java
new file mode 100644
index 0000000..2b1d3f4
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/CurrentTimeMillisLong.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.annotation;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc Value is a non-negative timestamp measured as the number of
+ * milliseconds since 1970-01-01T00:00:00Z.
+ * @paramDoc Value is a non-negative timestamp measured as the number of
+ * milliseconds since 1970-01-01T00:00:00Z.
+ * @returnDoc Value is a non-negative timestamp measured as the number of
+ * milliseconds since 1970-01-01T00:00:00Z.
+ */
+@Retention(SOURCE)
+@Target({METHOD, PARAMETER, FIELD})
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public @interface CurrentTimeMillisLong {
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/SystemApi.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/SystemApi.java
new file mode 100644
index 0000000..678d085
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/SystemApi.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates an API is exposed for use by bundled system applications.
+ *
+ * <p>These APIs are not guaranteed to remain consistent release-to-release,
+ * and are not for use by apps linking against the Android SDK.
+ *
+ * <p>This annotation should only appear on API that is already marked @<pre>hide</pre>.
+ */
+@Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE})
+@Retention(RetentionPolicy.RUNTIME)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public @interface SystemApi {
+ enum Client {
+ /**
+ * Specifies that the intended clients of a SystemApi are privileged apps.
+ * This is the default value for {@link #client}.
+ */
+ PRIVILEGED_APPS,
+
+ /**
+ * Specifies that the intended clients of a SystemApi are used by classes in
+ * <pre>BOOTCLASSPATH</pre> in mainline modules. Mainline modules can also expose
+ * this type of system APIs too when they're used only by the non-updatable
+ * platform code.
+ */
+ MODULE_LIBRARIES,
+
+ /**
+ * Specifies that the system API is available only in the system server process.
+ * Use this to expose APIs from code loaded by the system server process <em>but</em>
+ * not in <pre>BOOTCLASSPATH</pre>.
+ */
+ SYSTEM_SERVER
+ }
+
+ /**
+ * The intended client of this SystemAPI.
+ */
+ Client client() default Client.PRIVILEGED_APPS;
+
+ /**
+ * Container for {@link SystemApi} that allows it to be applied repeatedly to types.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(TYPE)
+ @interface Container {
+ SystemApi[] value();
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBlobHandle.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBlobHandle.java
index d6ed1c2..2d6501d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBlobHandle.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBlobHandle.java
@@ -71,7 +71,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Constructor
- private AppSearchBlobHandle(
+ AppSearchBlobHandle(
@Param(id = 1) @NonNull byte[] sha256Digest,
@Param(id = 2) @NonNull String label) {
mSha256Digest = Preconditions.checkNotNull(sha256Digest);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 6de5448..006d119 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -17,14 +17,19 @@
package androidx.appsearch.app;
import android.annotation.SuppressLint;
+import android.os.Build;
+import android.os.Parcel;
import android.util.Log;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.CurrentTimeMillisLong;
import androidx.appsearch.annotation.Document;
+import androidx.appsearch.annotation.SystemApi;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.flags.FlaggedApi;
import androidx.appsearch.flags.Flags;
@@ -152,6 +157,45 @@
}
/**
+ * Writes the {@link GenericDocument} to the given {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to write to.
+ * @param flags The flags to use for parceling.
+ * @exportToFramework:hide
+ */
+ // GenericDocument is an open class that can be extended, whereas parcelable classes must be
+ // final in those methods. Thus, we make this a system api to avoid 3p apps depending on it
+ // and getting confused by the inheritability.
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public final void writeToParcel(@NonNull Parcel dest, int flags) {
+ Objects.requireNonNull(dest);
+ dest.writeParcelable(mDocumentParcel, flags);
+ }
+
+ /**
+ * Creates a {@link GenericDocument} from a {@link Parcel}.
+ *
+ * @param parcel The {@link Parcel} to read from.
+ * @exportToFramework:hide
+ */
+ // GenericDocument is an open class that can be extended, whereas parcelable classes must be
+ // final in those methods. Thus, we make this a system api to avoid 3p apps depending on it
+ // and getting confused by the inheritability.
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static GenericDocument createFromParcel(@NonNull Parcel parcel) {
+ Objects.requireNonNull(parcel);
+ return new GenericDocument(
+ parcel.readParcelable(
+ GenericDocumentParcel.class.getClassLoader(), GenericDocumentParcel.class));
+ }
+
+ /**
* Returns the {@link GenericDocumentParcel} holding the values for this
* {@link GenericDocument}.
*
@@ -202,7 +246,7 @@
*
* <p>The value is in the {@link System#currentTimeMillis} time base.
*/
- /*@exportToFramework:CurrentTimeMillisLong*/
+ @CurrentTimeMillisLong
public long getCreationTimestampMillis() {
return mDocumentParcel.getCreationTimestampMillis();
}
@@ -1358,7 +1402,7 @@
@CanIgnoreReturnValue
@NonNull
public BuilderType setCreationTimestampMillis(
- /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
+ @CurrentTimeMillisLong long creationTimestampMillis) {
mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis);
return mBuilderTypeInstance;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
index 67550eb..00957811 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.CurrentTimeMillisLong;
import androidx.core.util.Preconditions;
/**
@@ -79,7 +80,7 @@
*
* <p>The value is in the {@link System#currentTimeMillis} time base.
*/
- /*@exportToFramework:CurrentTimeMillisLong*/
+ @CurrentTimeMillisLong
public long getUsageTimestampMillis() {
return mUsageTimestampMillis;
}
@@ -127,7 +128,7 @@
@CanIgnoreReturnValue
@NonNull
public ReportSystemUsageRequest.Builder setUsageTimestampMillis(
- /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+ @CurrentTimeMillisLong long usageTimestampMillis) {
mUsageTimestampMillis = usageTimestampMillis;
return this;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
index aafcc61..b911919 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.CurrentTimeMillisLong;
import androidx.appsearch.flags.FlaggedApi;
import androidx.appsearch.flags.Flags;
import androidx.appsearch.safeparcel.AbstractSafeParcelable;
@@ -84,7 +85,7 @@
*
* <p>The value is in the {@link System#currentTimeMillis} time base.
*/
- /*@exportToFramework:CurrentTimeMillisLong*/
+ @CurrentTimeMillisLong
public long getUsageTimestampMillis() {
return mUsageTimestampMillis;
}
@@ -127,7 +128,7 @@
@CanIgnoreReturnValue
@NonNull
public ReportUsageRequest.Builder setUsageTimestampMillis(
- /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+ @CurrentTimeMillisLong long usageTimestampMillis) {
mUsageTimestampMillis = usageTimestampMillis;
return this;
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
index 3f98aaf..93d8138 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -80,7 +80,7 @@
* methods.
*/
public static final String FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS =
- FLAG_PREFIX + "enable_search_spec_search_spec_strings";
+ FLAG_PREFIX + "enable_search_spec_search_string_parameters";
/** Enable addTakenActions API in PutDocumentsRequest. */
public static final String FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS =
@@ -142,6 +142,14 @@
public static final String FLAG_ENABLE_BLOB_STORE =
FLAG_PREFIX + "enable_blob_store";
+ /** Enable {@link androidx.appsearch.app.GenericDocument#writeToParcel}. */
+ public static final String FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC =
+ FLAG_PREFIX + "enable_generic_document_over_ipc";
+
+ /** Enable empty batch result fix for enterprise GetDocuments. */
+ public static final String FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX =
+ FLAG_PREFIX + "enable_enterprise_empty_batch_result_fix";
+
// Whether the features should be enabled.
//
// In Jetpack, those should always return true.
@@ -263,4 +271,9 @@
public static boolean enableBlobStore() {
return true;
}
+
+ /** Whether empty batch result fix for enterprise GetDocuments should be enabled. */
+ public static boolean enableEnterpriseEmptyBatchResultFix() {
+ return true;
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
index 1d407fa..bbfb803 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
@@ -24,6 +24,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.CurrentTimeMillisLong;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.AppSearchSession;
import androidx.appsearch.app.EmbeddingVector;
@@ -193,7 +194,7 @@
}
/** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
- /*@exportToFramework:CurrentTimeMillisLong*/
+ @CurrentTimeMillisLong
public long getCreationTimestampMillis() {
return mCreationTimestampMillis;
}
@@ -393,7 +394,7 @@
@CanIgnoreReturnValue
@NonNull
public Builder setCreationTimestampMillis(
- /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
+ @CurrentTimeMillisLong long creationTimestampMillis) {
mCreationTimestampMillis = creationTimestampMillis;
return this;
}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java
index f422d6d..8520b09 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/LongPropertyAnnotation.java
@@ -28,6 +28,7 @@
import com.google.auto.value.AutoValue;
import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.TypeName;
import java.util.Map;
@@ -69,7 +70,8 @@
String name = (String) annotationParams.get("name");
SerializerClass customSerializer = null;
TypeMirror serializerInAnnotation = (TypeMirror) annotationParams.get("serializer");
- if (!serializerInAnnotation.toString().equals(DEFAULT_SERIALIZER_CLASS.canonicalName())) {
+ String typeName = TypeName.get(serializerInAnnotation).toString();
+ if (!typeName.equals(DEFAULT_SERIALIZER_CLASS.canonicalName())) {
customSerializer = SerializerClass.create(
(TypeElement) asElement(serializerInAnnotation),
SerializerClass.Kind.LONG_SERIALIZER);
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java
index c949c4c..3f086cb 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/StringPropertyAnnotation.java
@@ -28,6 +28,7 @@
import com.google.auto.value.AutoValue;
import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.TypeName;
import java.util.Map;
@@ -69,7 +70,8 @@
String name = (String) annotationParams.get("name");
SerializerClass customSerializer = null;
TypeMirror serializerInAnnotation = (TypeMirror) annotationParams.get("serializer");
- if (!serializerInAnnotation.toString().equals(DEFAULT_SERIALIZER_CLASS.canonicalName())) {
+ String typeName = TypeName.get(serializerInAnnotation).toString();
+ if (!typeName.equals(DEFAULT_SERIALIZER_CLASS.canonicalName())) {
customSerializer = SerializerClass.create(
(TypeElement) asElement(serializerInAnnotation),
SerializerClass.Kind.STRING_SERIALIZER);
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index b483b72..373cd50 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -32,9 +32,6 @@
# Replaced with @hide:
# <!--@exportToFramework:hide-->
#
-# Replaced with @CurrentTimeMillisLong:
-# /*@exportToFramework:CurrentTimeMillisLong*/
-#
# Removes the text appearing between ifJetpack() and else(), and causes the text appearing between
# else() and --> to become uncommented, to support framework-only Javadocs:
# <!--@exportToFramework:ifJetpack()-->
@@ -143,10 +140,6 @@
# Add additional imports if required
imports_to_add = []
- if '@exportToFramework:CurrentTimeMillisLong' in contents:
- imports_to_add.append('android.annotation.CurrentTimeMillisLong')
- if '@exportToFramework:UnsupportedAppUsage' in contents:
- imports_to_add.append('android.compat.annotation.UnsupportedAppUsage')
for import_to_add in imports_to_add:
contents = re.sub(
r'^(\s*package [^;]+;\s*)$', r'\1\nimport %s;\n' % import_to_add, contents,
@@ -167,6 +160,12 @@
'com.android.server.appsearch.external.localstorage.')
.replace('androidx.appsearch.flags.FlaggedApi', 'android.annotation.FlaggedApi')
.replace('androidx.appsearch.flags.Flags', 'com.android.appsearch.flags.Flags')
+ .replace(
+ 'androidx.appsearch.annotation.CurrentTimeMillis',
+ 'android.annotation.CurrentTimeMillis')
+ .replace(
+ 'androidx.appsearch.annotation.SystemApi',
+ 'android.annotation.SystemApi')
.replace('androidx.appsearch', 'android.app.appsearch')
.replace(
'androidx.annotation.GuardedBy',
@@ -190,9 +189,6 @@
.replace('@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)', '')
.replace('Preconditions.checkNotNull(', 'Objects.requireNonNull(')
.replace('ObjectsCompat.', 'Objects.')
-
- .replace('/*@exportToFramework:CurrentTimeMillisLong*/', '@CurrentTimeMillisLong')
- .replace('/*@exportToFramework:UnsupportedAppUsage*/', '@UnsupportedAppUsage')
.replace('<!--@exportToFramework:hide-->', '@hide')
.replace('@exportToFramework:hide', '@hide')
.replace('// @exportToFramework:skipFile()', '')
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerExtension.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerExtension.kt
index 404aad4..571621c 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerExtension.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerExtension.kt
@@ -61,6 +61,13 @@
*/
var useConnectedDevices = true
+ /**
+ * Whether tests with Macrobenchmark rule should be skipped when running on emulator. Note that
+ * when `automaticGenerationDuringBuild` is `true` and managed devices are used benchmark will
+ * always run on emulator, causing an exception if this flag is not enabled.
+ */
+ var skipBenchmarksOnEmulator = true
+
/** Enables the emulator display for GMD devices. This is not a stable api. */
@Incubating var enableEmulatorDisplay = false
}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index c683663..239eff6 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -33,6 +33,7 @@
import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES
import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES_BASELINE_PROFILE
import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK
+import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_SKIP_ON_EMULATOR
import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_TARGET_PACKAGE_NAME
import androidx.baselineprofile.gradle.utils.InstrumentationTestRunnerArgumentsAgp82
import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
@@ -253,17 +254,26 @@
// If this is a benchmark variant sets the instrumentation runner argument to run only
// tests with MacroBenchmark rules.
if (
- addEnabledRulesInstrumentationArgument &&
- enabledRulesNotSet &&
- variant.buildType in benchmarkExtendedToOriginalTypeMap.keys
+ variant.buildType in benchmarkExtendedToOriginalTypeMap.keys &&
+ supportsFeature(TEST_VARIANT_SUPPORTS_INSTRUMENTATION_RUNNER_ARGUMENTS)
) {
- if (supportsFeature(TEST_VARIANT_SUPPORTS_INSTRUMENTATION_RUNNER_ARGUMENTS)) {
+
+ InstrumentationTestRunnerArgumentsAgp82.set(
+ variant = variant,
+ arguments =
+ listOf(
+ INSTRUMENTATION_ARG_SKIP_ON_EMULATOR to
+ baselineProfileExtension.skipBenchmarksOnEmulator.toString()
+ )
+ )
+
+ if (addEnabledRulesInstrumentationArgument && enabledRulesNotSet) {
InstrumentationTestRunnerArgumentsAgp82.set(
variant = variant,
arguments =
listOf(
INSTRUMENTATION_ARG_ENABLED_RULES to
- INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK
+ INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK,
)
)
}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
index eaddaf0..c367bd7 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
@@ -53,6 +53,7 @@
// Instrumentation runner arguments
internal const val INSTRUMENTATION_ARG_ENABLED_RULES = "androidx.benchmark.enabledRules"
+internal const val INSTRUMENTATION_ARG_SKIP_ON_EMULATOR = "androidx.benchmark.skipOnEmulator"
internal const val INSTRUMENTATION_ARG_ENABLED_RULES_BASELINE_PROFILE = "baselineprofile"
internal const val INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK = "macrobenchmark"
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPluginTest.kt
index 5ab636c..ab13a81 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPluginTest.kt
@@ -304,12 +304,14 @@
arrayOf(
AssertData("benchmarkReleaseArguments", false) {
contains("androidx.benchmark.enabledRules=macrobenchmark")
+ contains("androidx.benchmark.skipOnEmulator=true")
},
AssertData("nonMinifiedReleaseArguments", false) {
contains("androidx.benchmark.enabledRules=baselineprofile")
},
AssertData("benchmarkReleaseArguments", true) {
doesNotContain("androidx.benchmark.enabledRules=macrobenchmark")
+ contains("androidx.benchmark.skipOnEmulator=true")
},
AssertData("nonMinifiedReleaseArguments", true) {
doesNotContain("androidx.benchmark.enabledRules=baselineprofile")
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/AssertsAssert.kt
similarity index 61%
rename from tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
rename to benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/AssertsAssert.kt
index aeb0122..81d31ba 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/AssertsAssert.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,15 @@
* limitations under the License.
*/
-package androidx.tv.foundation.lazy.list
+package androidx.benchmark
-/** DSL marker used to distinguish between lazy layout scope and the item scope. */
-@Deprecated("No longer needed as TvLazyRow and TvLazyColumn have been deprecated.")
-@DslMarker
-annotation class TvLazyListScopeMarker
+import kotlin.test.Test
+import org.junit.Assert
+
+class AssertsAssert {
+ // Makes sure that the test APKs are debuggable and asserts are thrown.
+ @Test
+ fun checkAssertsAreEnforced() {
+ Assert.assertThrows(AssertionError::class.java) { assert(false) }
+ }
+}
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/InMemoryTracingTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/InMemoryTracingTest.kt
index 2a471b2..d1dbc7a 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/InMemoryTracingTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/InMemoryTracingTest.kt
@@ -22,6 +22,7 @@
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -81,7 +82,7 @@
// verify events
trace.packet[1].apply {
- assert(timestamp in beforeTime..afterTime)
+ assertTrue(timestamp in beforeTime..afterTime)
assertEquals(
TracePacket(
timestamp = timestamp,
@@ -99,7 +100,7 @@
)
}
trace.packet[2].apply {
- assert(timestamp in beforeTime..afterTime)
+ assertTrue(timestamp in beforeTime..afterTime)
assertEquals(
TracePacket(
timestamp = timestamp,
@@ -115,6 +116,92 @@
)
}
}
+
+ @Test
+ fun traceWithCounters() {
+ val beforeTime = 100L
+ val afterTime = 200L
+
+ // test counter embedded in beginSection
+ InMemoryTracing.beginSection(
+ "test trace section",
+ beforeTime,
+ counterNames = listOf("counterLabel"),
+ counterValues = listOf(0.1)
+ )
+ InMemoryTracing.endSection(afterTime)
+
+ // test counter on its own
+ InMemoryTracing.counter("counterLabel", 1.0, afterTime)
+
+ val trace = InMemoryTracing.commitToTrace("testLabel")
+
+ assertEquals(5, trace.packet.size)
+
+ // verify first track, for slices
+ val sliceDescriptor = trace.packet.first().track_descriptor
+ assertNotNull(sliceDescriptor)
+ assertEquals("testLabel", sliceDescriptor.name)
+ // verify second track, for counters
+ val counterDescriptor = trace.packet[1].track_descriptor
+ assertNotNull(counterDescriptor)
+ assertEquals("counterLabel", counterDescriptor.name)
+
+ // verify events
+ trace.packet[2].apply {
+ assertEquals(timestamp, beforeTime)
+ assertEquals(
+ TracePacket(
+ timestamp = timestamp,
+ timestamp_clock_id = 3,
+ trusted_packet_sequence_id = trusted_packet_sequence_id,
+ track_event =
+ TrackEvent(
+ type = TrackEvent.Type.TYPE_SLICE_BEGIN,
+ track_uuid = sliceDescriptor.uuid,
+ categories = listOf("benchmark"),
+ name = "test trace section",
+ extra_double_counter_track_uuids = listOf(counterDescriptor.uuid!!),
+ extra_double_counter_values = listOf(0.1)
+ )
+ ),
+ this
+ )
+ }
+ trace.packet[3].apply {
+ assertEquals(timestamp, afterTime)
+ assertEquals(
+ TracePacket(
+ timestamp = timestamp,
+ timestamp_clock_id = 3,
+ trusted_packet_sequence_id = trusted_packet_sequence_id,
+ track_event =
+ TrackEvent(
+ type = TrackEvent.Type.TYPE_SLICE_END,
+ track_uuid = sliceDescriptor.uuid,
+ )
+ ),
+ this
+ )
+ }
+ trace.packet[4].apply {
+ assertEquals(timestamp, afterTime)
+ assertEquals(
+ TracePacket(
+ timestamp = timestamp,
+ timestamp_clock_id = 3,
+ trusted_packet_sequence_id = trusted_packet_sequence_id,
+ track_event =
+ TrackEvent(
+ type = TrackEvent.Type.TYPE_COUNTER,
+ track_uuid = counterDescriptor.uuid,
+ double_counter_value = 1.0,
+ )
+ ),
+ this
+ )
+ }
+ }
}
@Suppress("SameParameterValue")
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
index e07f881..6068fa7b 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
@@ -52,9 +52,10 @@
// noop
}
assertNotNull(perfettoTrace)
- assert(perfettoTrace!!.path.matches(Regex(".*/testTrace_[0-9-]+.perfetto-trace"))) {
+ assertTrue(
+ perfettoTrace!!.path.matches(Regex(".*/testTrace_[0-9-]+.perfetto-trace")),
"$perfettoTrace didn't match!"
- }
+ )
}
private fun verifyRecordSuccess(config: PerfettoConfig) {
@@ -68,9 +69,10 @@
// noop
}
assertNotNull(perfettoTrace)
- assert(perfettoTrace!!.path.matches(Regex(".*/${label}_[0-9-]+.perfetto-trace"))) {
+ assertTrue(
+ perfettoTrace!!.path.matches(Regex(".*/${label}_[0-9-]+.perfetto-trace")),
"$perfettoTrace didn't match!"
- }
+ )
}
private fun verifyRecordFails(config: PerfettoConfig) {
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
index 3b38d13..9c359f2 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
@@ -76,7 +76,7 @@
tempFile.writeText(fakeText)
ResultWriter.writeReport(tempFile, listOf(reportA, reportB))
- assert(!tempFile.readText().startsWith(fakeText))
+ assertTrue(!tempFile.readText().startsWith(fakeText))
}
@Test
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index b10d760..be2d9bf2 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -62,6 +62,7 @@
val dryRunMode: Boolean
val dropShadersEnable: Boolean
val dropShadersThrowOnFailure: Boolean
+ val skipBenchmarksOnEmulator: Boolean
// internal properties are microbenchmark only
internal val outputEnable: Boolean
@@ -170,6 +171,9 @@
.filter { it.isNotEmpty() }
.toSet()
+ skipBenchmarksOnEmulator =
+ arguments.getBenchmarkArgument("skipBenchmarksOnEmulator")?.toBoolean() ?: false
+
enabledRules =
arguments
.getBenchmarkArgument(
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/DeviceInfo.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/DeviceInfo.kt
index 65e62cd..c783adc 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/DeviceInfo.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/DeviceInfo.kt
@@ -38,6 +38,7 @@
Build.FINGERPRINT.startsWith("unknown") ||
Build.FINGERPRINT.contains("emulator") ||
Build.MODEL.contains("google_sdk") ||
+ Build.MODEL.startsWith("sdk_") ||
Build.MODEL.contains("sdk_gphone64") ||
Build.MODEL.contains("Emulator") ||
Build.MODEL.contains("Android SDK built for") ||
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InMemoryTracing.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InMemoryTracing.kt
index a68a4e7..b3e2c64 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InMemoryTracing.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InMemoryTracing.kt
@@ -19,6 +19,7 @@
import android.os.Process
import androidx.annotation.RestrictTo
import androidx.benchmark.InMemoryTracing.commitToTrace
+import perfetto.protos.CounterDescriptor
import perfetto.protos.ThreadDescriptor
import perfetto.protos.Trace
import perfetto.protos.TracePacket
@@ -65,18 +66,41 @@
private val TRACK_EVENT_CATEGORIES = listOf("benchmark")
/**
- * For perf/simplicity, this isn't protected by a lock - it should only every be accessed by the
+ * For perf/simplicity, this isn't protected by a lock - it should only ever be accessed by the
* test thread, and dumped/reset between tests.
*/
val events = mutableListOf<TracePacket>()
+ /** Map of counter name to UUID, populated by [counterNameToTrackUuid] */
+ private val counterTracks = mutableMapOf<String, Long>()
+
+ private fun counterNameToTrackUuid(name: String): Long {
+ return counterTracks.getOrPut(name) { UUID + 1 + counterTracks.size }
+ }
+
fun clearEvents() {
events.clear()
+ counterTracks.clear()
}
/** Capture trace state, and return as a Trace(), which can be appended to a trace file. */
fun commitToTrace(label: String): Trace {
val capturedEvents = events.toList()
+ val capturedCounterDescriptors =
+ counterTracks.map { (name, uuid) ->
+ TracePacket(
+ timestamp_clock_id = CLOCK_ID,
+ incremental_state_cleared = true,
+ track_descriptor =
+ TrackDescriptor(
+ uuid = uuid,
+ parent_uuid = UUID,
+ name = name,
+ counter = CounterDescriptor()
+ )
+ )
+ }
+
clearEvents()
return Trace(
listOf(
@@ -94,11 +118,17 @@
disallow_merging_with_system_tracks = true
)
)
- ) + capturedEvents
+ ) + capturedCounterDescriptors + capturedEvents
)
}
- fun beginSection(label: String, nanoTime: Long = System.nanoTime()) {
+ fun beginSection(
+ label: String,
+ nanoTime: Long = System.nanoTime(),
+ counterNames: List<String> = emptyList(),
+ counterValues: List<Double> = emptyList()
+ ) {
+ require(counterNames.size == counterValues.size)
events.add(
TracePacket(
timestamp = nanoTime,
@@ -109,7 +139,10 @@
type = TrackEvent.Type.TYPE_SLICE_BEGIN,
track_uuid = UUID,
categories = TRACK_EVENT_CATEGORIES,
- name = label
+ name = label,
+ extra_double_counter_values = counterValues,
+ extra_double_counter_track_uuids =
+ counterNames.map { counterNameToTrackUuid(it) },
)
)
)
@@ -129,6 +162,23 @@
)
)
}
+
+ fun counter(name: String, value: Double, nanoTime: Long = System.nanoTime()) {
+ events.add(
+ TracePacket(
+ timestamp = nanoTime,
+ timestamp_clock_id = CLOCK_ID,
+ trusted_packet_sequence_id = TRUSTED_PACKET_SEQUENCE_ID,
+ track_event =
+ TrackEvent(
+ type = TrackEvent.Type.TYPE_COUNTER,
+ double_counter_value = value,
+ track_uuid = counterNameToTrackUuid(name),
+ // track_uuid = UUID
+ )
+ )
+ )
+ }
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricsContainer.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricsContainer.kt
index 710ef11..56a5ffe 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricsContainer.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricsContainer.kt
@@ -136,30 +136,42 @@
* Call exactly once at the end of a benchmark.
*/
fun captureFinished(maxIterations: Int): List<MetricResult> {
+ val results =
+ names.mapIndexed { index, name ->
+ val metricData =
+ List(repeatCount) {
+ // convert to floats and divide by iter count here for efficiency
+ data[it][index] / maxIterations.toDouble()
+ }
+ metricData.chunked(10).forEachIndexed { chunkNum, chunk ->
+ Log.d(
+ BenchmarkState.TAG,
+ name +
+ "[%2d:%2d]: %s"
+ .format(
+ chunkNum * 10,
+ (chunkNum + 1) * 10,
+ chunk.joinToString(" ") { it.toLong().toString() }
+ )
+ )
+ }
+ MetricResult(name, metricData)
+ }
+
+ val metricTraceLabels = names.map { "metric: $it" }
for (i in 0..repeatTiming.lastIndex step 2) {
- InMemoryTracing.beginSection("measurement ${i / 2}", nanoTime = repeatTiming[i])
+ val measurementIndex = i / 2
+ InMemoryTracing.beginSection(
+ "measurement $measurementIndex",
+ nanoTime = repeatTiming[i],
+ counterNames = metricTraceLabels,
+ counterValues = results.map { it.data[measurementIndex] }
+ )
InMemoryTracing.endSection(nanoTime = repeatTiming[i + 1])
}
+ // to clarify when measurement ends, reset metrics to 0
+ metricTraceLabels.forEach { InMemoryTracing.counter(it, 0.0, repeatTiming.last()) }
- return names.mapIndexed { index, name ->
- val metricData =
- List(repeatCount) {
- // convert to floats and divide by iter count here for efficiency
- data[it][index] / maxIterations.toDouble()
- }
- metricData.chunked(10).forEachIndexed { chunkNum, chunk ->
- Log.d(
- BenchmarkState.TAG,
- name +
- "[%2d:%2d]: %s"
- .format(
- chunkNum * 10,
- (chunkNum + 1) * 10,
- chunk.joinToString(" ") { it.toLong().toString() }
- )
- )
- }
- MetricResult(name, metricData)
- }
+ return results
}
}
diff --git a/benchmark/benchmark-common/src/main/proto/perfetto_trace.proto b/benchmark/benchmark-common/src/main/proto/perfetto_trace.proto
index 12b204f..5372b59 100644
--- a/benchmark/benchmark-common/src/main/proto/perfetto_trace.proto
+++ b/benchmark/benchmark-common/src/main/proto/perfetto_trace.proto
@@ -94,9 +94,18 @@
enum Type {
TYPE_SLICE_BEGIN = 1;
TYPE_SLICE_END = 2;
+ TYPE_COUNTER = 4;
}
optional Type type = 9;
optional uint64 track_uuid = 11;
+ oneof counter_value_field {
+ int64 counter_value = 30;
+ double double_counter_value = 44;
+ }
+ repeated uint64 extra_counter_track_uuids = 31;
+ repeated int64 extra_counter_values = 12;
+ repeated uint64 extra_double_counter_track_uuids = 45;
+ repeated double extra_double_counter_values = 46;
}
message ThreadDescriptor {
@@ -104,10 +113,15 @@
optional int32 tid = 2;
}
+message CounterDescriptor {
+}
+
message TrackDescriptor {
optional uint64 uuid = 1;
+ optional uint64 parent_uuid = 5;
optional string name = 2;
optional ThreadDescriptor thread = 4;
+ optional CounterDescriptor counter = 8;
optional bool disallow_merging_with_system_tracks = 9;
}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 7b7fc7f..d586cd8 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -41,6 +41,7 @@
import java.util.concurrent.FutureTask
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@@ -200,7 +201,18 @@
private fun applyInternal(base: Statement, description: Description) = Statement {
applied = true
+
assumeTrue(Arguments.RuleType.Microbenchmark in Arguments.enabledRules)
+
+ // When running on emulator and argument `skipOnEmulator` is passed,
+ // the test is skipped.
+ if (Arguments.skipBenchmarksOnEmulator) {
+ assumeFalse(
+ "Skipping test because it's running on emulator and `skipOnEmulator` is enabled",
+ DeviceInfo.isEmulator
+ )
+ }
+
var invokeMethodName = description.methodName
Log.d(TAG, "-- Running ${description.className}#$invokeMethodName --")
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 113312a..23786dd 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -75,6 +75,7 @@
implementation("androidx.test:core:1.5.0")
implementation(libs.testUiautomator)
implementation(libs.wireRuntime)
+ implementation(libs.testExtJunit)
androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(project(":internal-testutils-ktx"))
@@ -165,7 +166,7 @@
def installTask = tasks.findByPath(
":benchmark:integration-tests:macrobenchmark-target:installRelease")
if (installTask != null) {
- tasks.getByPath(":benchmark:benchmark-macro:connectedDebugAndroidTest")
+ tasks.getByPath(":benchmark:benchmark-macro:connectedReleaseAndroidTest")
.dependsOn(installTask)
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
index 35dab5c..6f8298f 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
@@ -29,6 +29,10 @@
createTempFileFromAsset(prefix = "api24_startup_cold", suffix = ".perfetto-trace")
.absolutePath
+ private val api31ColdStart =
+ createTempFileFromAsset(prefix = "api31_startup_cold", suffix = ".perfetto-trace")
+ .absolutePath
+
private val commasInSliceNames =
createTempFileFromAsset(prefix = "api24_commas_in_slice_names", suffix = ".perfetto-trace")
.absolutePath
@@ -107,6 +111,20 @@
targetPackageOnly = false,
)
+ @Test
+ fun filterNonTerminatingSlices() =
+ verifyFirstSum(
+ tracePath = api31ColdStart, // arbitrary trace which includes non-termination slices
+ packageName = Packages.TARGET, // ignored
+ sectionName = "wait",
+ expectedFirstMs = 0.00724,
+ expectedMinMs = 0.001615, // filtered out non-terminating -1 duration
+ expectedMaxMs = 357.761234,
+ expectedSumMs = 811.865025,
+ expectedSumCount = 226, // filtered out single case where dur = -1
+ targetPackageOnly = false,
+ )
+
companion object {
private fun verifyMetric(
tracePath: String,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index ad06709..1bd63b6 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -40,6 +40,7 @@
import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assume.assumeFalse
/** Get package ApplicationInfo, throw if not found. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -217,6 +218,14 @@
"Empty list of metrics passed to metrics param, must pass at least one Metric"
}
+ // When running on emulator and argument `skipOnEmulator` is passed, the test is skipped.
+ if (Arguments.skipBenchmarksOnEmulator) {
+ assumeFalse(
+ "Skipping test because it's running on emulator and `skipOnEmulator` is enabled",
+ DeviceInfo.isEmulator
+ )
+ }
+
val suppressionState = checkErrors(packageName)
var warningMessage = suppressionState?.warningMessage ?: ""
// skip benchmark if not supported by vm settings
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 8a2cb2f..519b60e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -457,6 +457,9 @@
* )
* ```
*
+ * Note that non-terminating slices in the trace (where duration = -1) are always ignored by this
+ * metric.
+ *
* @see androidx.tracing.Trace.beginSection
* @see androidx.tracing.Trace.endSection
* @see androidx.tracing.trace
@@ -553,10 +556,12 @@
traceSession: PerfettoTraceProcessor.Session
): List<Measurement> {
val slices =
- traceSession.querySlices(
- sectionName,
- packageName = if (targetPackageOnly) captureInfo.targetPackageName else null
- )
+ traceSession
+ .querySlices(
+ sectionName,
+ packageName = if (targetPackageOnly) captureInfo.targetPackageName else null
+ )
+ .filter { it.dur != -1L } // filter out non-terminating slices
return when (mode) {
Mode.First -> {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt
index c78134b..9e03d06 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt
@@ -64,8 +64,8 @@
expectedSamples.zip(observedSamples).forEachIndexed { index, pair ->
if (abs(pair.first - pair.second) > threshold) {
errorString +=
- "$name sample $index observed ${pair.first}" +
- " more than $threshold from expected ${pair.second}\n"
+ "$name sample $index observed ${pair.second}, which is" +
+ " more than $threshold from expected ${pair.first}\n"
}
}
}
diff --git a/benchmark/benchmark/build.gradle b/benchmark/benchmark/build.gradle
index cb95e6d..3c10910 100644
--- a/benchmark/benchmark/build.gradle
+++ b/benchmark/benchmark/build.gradle
@@ -40,6 +40,7 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.kotlinTest)
}
android {
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkConfigTest.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkConfigTest.kt
new file mode 100644
index 0000000..a350a82
--- /dev/null
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/BenchmarkConfigTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.benchmark.benchmark
+
+import android.content.pm.ApplicationInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * These tests validate build time properties of benchmarks.
+ *
+ * These tests are enforced in presubmit, even if dryRunMode=true is passed. Standard
+ * microbenchmarks will not perform these checks when dryRunMode=false.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class BenchmarkConfigTest {
+ private val arguments = InstrumentationRegistry.getArguments()
+ private val contextTest = InstrumentationRegistry.getInstrumentation().context
+ private val contextTarget = InstrumentationRegistry.getInstrumentation().targetContext
+
+ @Test
+ fun coverageDisabled() {
+ assertNotEquals(
+ illegal = "true",
+ actual = arguments.getString("coverage"),
+ message = "Coverage must not be enabled in microbench instrumentation args"
+ )
+ }
+
+ @Test
+ fun selfInstrumenting() {
+ assertEquals(
+ expected = contextTest.packageName,
+ actual = contextTarget.packageName,
+ message =
+ "Microbenchmark must be self-instrumenting," +
+ " test pkg=${contextTest.packageName}," +
+ " target pkg=${contextTarget.packageName}"
+ )
+ }
+
+ @Test
+ fun debuggableFalse() {
+ val debuggable = contextTest.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
+ assertFalse(debuggable, "Microbenchmark must not be debuggable")
+ }
+}
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh b/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh
new file mode 100755
index 0000000..858a968
--- /dev/null
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# ADB push intro copied from lockClocks.sh
+if [ "`command -v getprop`" == "" ]; then
+ if [ -n "`command -v adb`" ]; then
+ echo ""
+ echo "Pushing $0 and running it on device..."
+ dest=/data/local/tmp/`basename $0`
+ adb push $0 ${dest}
+ adb shell ${dest} $@
+ adb shell rm ${dest}
+ exit
+ else
+ echo "Could not find adb. Options are:"
+ echo " 1. Ensure adb is on your \$PATH"
+ echo " 2. Use './gradlew lockClocks'"
+ echo " 3. Manually adb push this script to your device, and run it there"
+ exit -1
+ fi
+fi
+
+echo ""
+
+# require root
+if [[ `id` != "uid=0"* ]]; then
+ echo "Not running as root, cannot disable jit, aborting"
+ exit -1
+fi
+
+setprop dalvik.vm.extra-opts "-Xusejit:false"
+stop
+start
+
+DEVICE=`getprop ro.product.device`
+echo "JIT compilation has been disabled on $DEVICE!"
+echo "Performance will be terrible for almost everything! (except e.g. AOT benchmarks)"
+echo "To reenable it (strongly recommended after benchmarking!!!), reboot or run resetDevice.sh"
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh b/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh
new file mode 100755
index 0000000..060f075
--- /dev/null
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# ADB push intro copied from lockClocks.sh
+if [ "`command -v getprop`" == "" ]; then
+ if [ -n "`command -v adb`" ]; then
+ echo ""
+ echo "Pushing $0 and running it on device..."
+ dest=/data/local/tmp/`basename $0`
+ adb push $0 ${dest}
+ adb shell ${dest} $@
+ # adb shell rm ${dest} # will fail, not very important
+ exit
+ else
+ echo "Could not find adb. Options are:"
+ echo " 1. Ensure adb is on your \$PATH"
+ echo " 2. Use './gradlew lockClocks'"
+ echo " 3. Manually adb push this script to your device, and run it there"
+ exit -1
+ fi
+fi
+
+DEVICE=`getprop ro.product.device`
+echo ""
+echo "Rebooting $DEVICE, and resetting animation scales!"
+echo "This will re-lock clocks, reenable JIT, and reset animation scale to 1.0"
+
+settings put global window_animation_scale 1.0
+settings put global transition_animation_scale 1.0
+settings put global animator_duration_scale 1.0
+
+reboot # required to relock clocks, and handles reenabling jit since the property won't persist
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
index eb4f9aa..76ce1a1 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
@@ -45,15 +45,12 @@
* .setContentView(
* new PromptContentViewWithMoreOptionsButton.Builder()
* .setDescription("test description")
- * .setMoreOptionsButtonListener(executor, listener)
* .build()
* )
* .build();
* </pre>
*/
public final class PromptContentViewWithMoreOptionsButton implements PromptContentView {
- static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
-
private final String mDescription;
private PromptContentViewWithMoreOptionsButton(@NonNull String description) {
@@ -89,10 +86,6 @@
@RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
@NonNull
public Builder setDescription(@NonNull String description) {
- if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
- throw new IllegalArgumentException("The character number of description exceeds "
- + MAX_DESCRIPTION_CHARACTER_NUMBER);
- }
mDescription = description;
return this;
}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
index 58d7be2..b464d05 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
@@ -42,10 +42,6 @@
* </pre>
*/
public final class PromptVerticalListContentView implements PromptContentView {
- static final int MAX_ITEM_NUMBER = 20;
- static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
- static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
-
private final List<PromptContentItem> mContentList;
private final String mDescription;
@@ -93,10 +89,6 @@
*/
@NonNull
public Builder setDescription(@NonNull String description) {
- if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
- throw new IllegalArgumentException("The character number of description exceeds "
- + MAX_DESCRIPTION_CHARACTER_NUMBER);
- }
mDescription = description;
return this;
}
@@ -112,7 +104,6 @@
@NonNull
public Builder addListItem(@NonNull PromptContentItem listItem) {
mContentList.add(listItem);
- checkItemLimits(listItem);
return this;
}
@@ -128,34 +119,9 @@
@NonNull
public Builder addListItem(@NonNull PromptContentItem listItem, int index) {
mContentList.add(index, listItem);
- checkItemLimits(listItem);
return this;
}
- private void checkItemLimits(@NonNull PromptContentItem listItem) {
- if (doesListItemExceedsCharLimit(listItem)) {
- throw new IllegalArgumentException(
- "The character number of list item exceeds "
- + MAX_EACH_ITEM_CHARACTER_NUMBER);
- }
- if (mContentList.size() > MAX_ITEM_NUMBER) {
- throw new IllegalArgumentException(
- "The number of list items exceeds " + MAX_ITEM_NUMBER);
- }
- }
-
- private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) {
- if (listItem instanceof PromptContentItemPlainText) {
- return ((PromptContentItemPlainText) listItem).getText().length()
- > MAX_EACH_ITEM_CHARACTER_NUMBER;
- } else if (listItem instanceof PromptContentItemBulletedText) {
- return ((PromptContentItemBulletedText) listItem).getText().length()
- > MAX_EACH_ITEM_CHARACTER_NUMBER;
- } else {
- return false;
- }
- }
-
/**
* Creates a {@link PromptVerticalListContentView}.
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 9771220..06d7941 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -140,6 +140,7 @@
method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityWidthPx(android.content.Intent);
method public static int getMaxToolbarItems();
+ method public static android.net.Network? getNetwork(android.content.Intent);
method public static android.app.PendingIntent? getSecondaryToolbarSwipeUpGesture(android.content.Intent);
method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
method public static java.util.Locale? getTranslateLocale(android.content.Intent);
@@ -193,6 +194,7 @@
field public static final String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
field public static final String EXTRA_NAVIGATION_BAR_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_COLOR";
field public static final String EXTRA_NAVIGATION_BAR_DIVIDER_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_DIVIDER_COLOR";
+ field public static final String EXTRA_NETWORK = "androidx.browser.customtabs.extra.NETWORK";
field public static final String EXTRA_REMOTEVIEWS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS";
field public static final String EXTRA_REMOTEVIEWS_CLICKED_ID = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_CLICKED_ID";
field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
@@ -254,6 +256,7 @@
method public androidx.browser.customtabs.CustomTabsIntent.Builder setInstantAppsEnabled(boolean);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
+ method public androidx.browser.customtabs.CustomTabsIntent.Builder setNetwork(android.net.Network);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.customtabs.CustomTabsIntent.Builder setPendingSession(androidx.browser.customtabs.CustomTabsSession.PendingSession);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarSwipeUpGesture(android.app.PendingIntent?);
@@ -290,6 +293,7 @@
field public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
field public static final String CATEGORY_COLOR_SCHEME_CUSTOMIZATION = "androidx.browser.customtabs.category.ColorSchemeCustomization";
field public static final String CATEGORY_NAVBAR_COLOR_CUSTOMIZATION = "androidx.browser.customtabs.category.NavBarColorCustomization";
+ field public static final String CATEGORY_SET_NETWORK = "androidx.browser.customtabs.category.SetNetwork";
field public static final String CATEGORY_TRUSTED_WEB_ACTIVITY_IMMERSIVE_MODE = "androidx.browser.trusted.category.ImmersiveMode";
field public static final String CATEGORY_WEB_SHARE_TARGET_V2 = "androidx.browser.trusted.category.WebShareTargetV2";
field public static final int FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE = 1; // 0x1
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index 985731c..b39f101 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -151,6 +151,7 @@
method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityWidthPx(android.content.Intent);
method public static int getMaxToolbarItems();
+ method public static android.net.Network? getNetwork(android.content.Intent);
method public static android.app.PendingIntent? getSecondaryToolbarSwipeUpGesture(android.content.Intent);
method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
method public static java.util.Locale? getTranslateLocale(android.content.Intent);
@@ -204,6 +205,7 @@
field public static final String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
field public static final String EXTRA_NAVIGATION_BAR_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_COLOR";
field public static final String EXTRA_NAVIGATION_BAR_DIVIDER_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_DIVIDER_COLOR";
+ field public static final String EXTRA_NETWORK = "androidx.browser.customtabs.extra.NETWORK";
field public static final String EXTRA_REMOTEVIEWS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS";
field public static final String EXTRA_REMOTEVIEWS_CLICKED_ID = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_CLICKED_ID";
field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
@@ -265,6 +267,7 @@
method public androidx.browser.customtabs.CustomTabsIntent.Builder setInstantAppsEnabled(boolean);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
+ method public androidx.browser.customtabs.CustomTabsIntent.Builder setNetwork(android.net.Network);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.customtabs.CustomTabsIntent.Builder setPendingSession(androidx.browser.customtabs.CustomTabsSession.PendingSession);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarSwipeUpGesture(android.app.PendingIntent?);
@@ -301,6 +304,7 @@
field public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
field public static final String CATEGORY_COLOR_SCHEME_CUSTOMIZATION = "androidx.browser.customtabs.category.ColorSchemeCustomization";
field public static final String CATEGORY_NAVBAR_COLOR_CUSTOMIZATION = "androidx.browser.customtabs.category.NavBarColorCustomization";
+ field public static final String CATEGORY_SET_NETWORK = "androidx.browser.customtabs.category.SetNetwork";
field public static final String CATEGORY_TRUSTED_WEB_ACTIVITY_IMMERSIVE_MODE = "androidx.browser.trusted.category.ImmersiveMode";
field public static final String CATEGORY_WEB_SHARE_TARGET_V2 = "androidx.browser.trusted.category.WebShareTargetV2";
field public static final int FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE = 1; // 0x1
diff --git a/browser/browser/build.gradle b/browser/browser/build.gradle
index 6c1f70b..9af6f1b 100644
--- a/browser/browser/build.gradle
+++ b/browser/browser/build.gradle
@@ -27,7 +27,7 @@
}
dependencies {
- api("androidx.core:core:1.1.0")
+ api("androidx.core:core:1.10.0")
api("androidx.annotation:annotation:1.8.1")
api("androidx.annotation:annotation-experimental:1.4.1")
api(libs.guavaListenableFuture)
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index 71bbefaf..cf8f78f 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -26,6 +26,7 @@
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
+import android.net.Network;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -47,6 +48,7 @@
import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
+import androidx.core.content.IntentCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -616,6 +618,13 @@
"androidx.browser.customtabs.extra.NAVIGATION_BAR_DIVIDER_COLOR";
/**
+ * Extra that specifies the {@link Network} to be bound when launching a Custom Tab or using
+ * mayLaunchUrl.
+ * See {@link Builder#setNetwork}.
+ */
+ public static final String EXTRA_NETWORK = "androidx.browser.customtabs.extra.NETWORK";
+
+ /**
* Key that specifies the unique ID for an action button. To make a button to show on the
* toolbar, use {@link #TOOLBAR_ACTION_BUTTON_ID} as its ID.
*/
@@ -1436,6 +1445,23 @@
}
/**
+ * Sets the target network {@link Network} to bind when launching a custom tab.
+ *
+ * This API allows the caller to specify the target network to bind when launching a URL
+ * via Custom Tabs, e.g. may want to open a custom tab over a Wi-Fi network, while the
+ * default network is a cellular connection. All URLRequests created in the future via this
+ * tab will be bound to {@link Network}.
+ *
+ * @param network {@link Network} the target network to be bound.
+ * @see CustomTabsIntent#EXTRA_NETWORK
+ */
+ @NonNull
+ public Builder setNetwork(@NonNull Network network) {
+ mIntent.putExtra(EXTRA_NETWORK, network);
+ return this;
+ }
+
+ /**
* Combines all the options that have been set and returns a new {@link CustomTabsIntent}
* object.
*/
@@ -1767,6 +1793,17 @@
}
/**
+ * Gets the target network that the custom tab is currently bound to if any.
+ *
+ * @return The target {@link Network} is bound to.
+ * @see CustomTabsIntent#EXTRA_NETWORK
+ */
+ @Nullable
+ public static Network getNetwork(@NonNull Intent intent) {
+ return IntentCompat.getParcelableExtra(intent, EXTRA_NETWORK, Network.class);
+ }
+
+ /**
* @return Whether the background interaction is enabled.
* @see CustomTabsIntent#EXTRA_DISABLE_BACKGROUND_INTERACTION
*/
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index b3360be..4ec3c2a 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -68,6 +68,13 @@
"androidx.browser.customtabs.category.ColorSchemeCustomization";
/**
+ * An Intent filter category to signify that the Custom Tabs provider supports multi-network,
+ * bind a custom tab to a particular network via {@link CustomTabsIntent.Builder#setNetwork}.
+ */
+ public static final String CATEGORY_SET_NETWORK =
+ "androidx.browser.customtabs.category.SetNetwork";
+
+ /**
* An Intent filter category to signify that the Custom Tabs provider supports Trusted Web
* Activities (see {@link TrustedWebUtils} for more details).
*/
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/PrefetchOptions.java b/browser/browser/src/main/java/androidx/browser/customtabs/PrefetchOptions.java
index a7699dc..fde9b6f 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/PrefetchOptions.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/PrefetchOptions.java
@@ -41,7 +41,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
PrefetchOptions(
- @NonNull boolean requiresAnonymousIpWhenCrossOrigin, @Nullable Uri sourceOrigin) {
+ boolean requiresAnonymousIpWhenCrossOrigin, @Nullable Uri sourceOrigin) {
this.requiresAnonymousIpWhenCrossOrigin = requiresAnonymousIpWhenCrossOrigin;
this.sourceOrigin = sourceOrigin;
}
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index f6d2a04..604bedc 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -26,8 +26,11 @@
import static org.junit.Assert.fail;
import android.app.PendingIntent;
+import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
+import android.net.ConnectivityManager;
+import android.net.Network;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
@@ -35,6 +38,7 @@
import androidx.annotation.ColorRes;
import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -839,4 +843,23 @@
assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION));
assertNull(intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M /* getActiveNetwork requires api>=23 */)
+ public void testBindNetworkToCustomTabs() {
+ ConnectivityManager cm =
+ (ConnectivityManager) ApplicationProvider.getApplicationContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ Network network = cm.getActiveNetwork();
+ long expectedNetworkHandle = network.getNetworkHandle();
+ Intent intent = new CustomTabsIntent.Builder()
+ .setNetwork(network)
+ .build()
+ .intent;
+ assertNotNull(intent);
+ assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_NETWORK));
+ assertNotNull(CustomTabsIntent.getNetwork(intent));
+ assertEquals(expectedNetworkHandle,
+ CustomTabsIntent.getNetwork(intent).getNetworkHandle());
+ }
}
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index c2a6cc7..cdf3a164 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -22,6 +22,11 @@
repos.addMavenRepositories(repositories)
+project.tasks.withType(Jar).configureEach { task ->
+ task.reproducibleFileOrder = true
+ task.preserveFileTimestamps = false
+}
+
dependencies {
api(project("plugins"))
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 95d88c4..e178e58 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -143,13 +143,16 @@
* If true, enable the ArrayNullnessMigration lint check to transition to type-use nullness
* annotations. Defaults to false.
*/
-const val MIGRATE_ARRAY_ANNOTATIONS = "androidx.migrateArrayAnnotations"
+const val USE_JSPECIFY_ANNOTATIONS = "androidx.useJSpecifyAnnotations"
/** If true, yarn dependencies are fetched from an offline mirror */
const val YARN_OFFLINE_MODE = "androidx.yarnOfflineMode"
const val FORCE_KOTLIN_2_0_TARGET = "androidx.forceKotlin20Target"
+/** Defined by AndroidX Benchmark Plugin, may be used for local experiments with compilation */
+const val FORCE_BENCHMARK_AOT_COMPILATION = "androidx.benchmark.forceaotcompilation"
+
val ALL_ANDROIDX_PROPERTIES =
setOf(
ADD_GROUP_CONSTRAINTS,
@@ -183,9 +186,10 @@
FilteredAnchorTask.PROP_TASK_NAME,
FilteredAnchorTask.PROP_PATH_PREFIX,
INCLUDE_OPTIONAL_PROJECTS,
- MIGRATE_ARRAY_ANNOTATIONS,
+ USE_JSPECIFY_ANNOTATIONS,
YARN_OFFLINE_MODE,
FORCE_KOTLIN_2_0_TARGET,
+ FORCE_BENCHMARK_AOT_COMPILATION,
) + AndroidConfigImpl.GRADLE_PROPERTIES
fun Project.shouldForceKotlin20Target() =
@@ -285,10 +289,10 @@
findBooleanProperty(ALLOW_CUSTOM_COMPILE_SDK) ?: true
/**
- * Whether to enable the ArrayNullnessMigration lint check for moving nullness annotations on arrays
- * when switching a project to type-use nullness annotations.
+ * Whether to enable the JSpecifyNullnessMigration lint check for moving nullness annotations when
+ * switching a project to the JSpecify type-use nullness annotations.
*/
-fun Project.migrateArrayAnnotations() = findBooleanProperty(MIGRATE_ARRAY_ANNOTATIONS) ?: false
+fun Project.useJSpecifyAnnotations() = findBooleanProperty(USE_JSPECIFY_ANNOTATIONS) ?: false
fun Project.findBooleanProperty(propName: String) = booleanPropertyProvider(propName).get()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index cdbd64e..f875c2f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -48,6 +48,7 @@
import com.android.build.api.dsl.KotlinMultiplatformAndroidTestOnJvmCompilation
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.dsl.PrivacySandboxSdkExtension
+import com.android.build.api.dsl.TestBuildType
import com.android.build.api.dsl.TestExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
@@ -822,11 +823,11 @@
}
private fun configureWithLibraryPlugin(project: Project, androidXExtension: AndroidXExtension) {
+ val buildTypeForTests = "release"
project.extensions.getByType<LibraryExtension>().apply {
publishing { singleVariant(DEFAULT_PUBLISH_CONFIG) }
configureAndroidBaseOptions(project, androidXExtension)
-
val debugSigningConfig = signingConfigs.getByName("debug")
// Use a local debug keystore to avoid build server issues.
debugSigningConfig.storeFile = project.getKeystore()
@@ -834,6 +835,7 @@
// Sign all the builds (including release) with debug key
buildType.signingConfig = debugSigningConfig
}
+ testBuildType = buildTypeForTests
project.configureTestConfigGeneration(this)
project.addAppApkToTestConfigGeneration(androidXExtension)
}
@@ -853,10 +855,13 @@
it.defaultConfig.aarMetadata.minCompileSdk = it.compileSdk
it.lint.targetSdk = project.defaultAndroidConfig.targetSdk
it.testOptions.targetSdk = project.defaultAndroidConfig.targetSdk
+ // Replace with a public API once available, see b/360392255
+ it.buildTypes.configureEach { buildType ->
+ if (buildType.name == buildTypeForTests && !project.hasBenchmarkPlugin())
+ (buildType as TestBuildType).isDebuggable = true
+ }
}
- beforeVariants(selector().withBuildType("release")) { variant ->
- (variant as HasUnitTestBuilder).enableUnitTest = false
- }
+ beforeVariants(selector().withBuildType("debug")) { variant -> variant.enable = false }
beforeVariants(selector().all()) { variant ->
variant.androidTest.targetSdk = project.defaultAndroidConfig.targetSdk
}
@@ -940,7 +945,6 @@
projectName != null && projectName.contains("desktop") -> VERSION_11
targetName != null && (targetName == "desktop" || targetName == "jvmStubs") ->
VERSION_11
- libraryType == LibraryType.COMPILER_PLUGIN -> VERSION_11
libraryType.compilationTarget == CompilationTarget.HOST -> VERSION_17
else -> VERSION_1_8
}
@@ -1094,9 +1098,12 @@
}
}
- project.configureFtlRunner(
+ val componentsExtension =
project.extensions.getByType(AndroidComponentsExtension::class.java)
- )
+ project.configureFtlRunner(componentsExtension)
+
+ // If a dependency is missing a debug variant, use release instead.
+ buildTypes.getByName("debug").matchingFallbacks.add("release")
// AGP warns if we use project.buildDir (or subdirs) for CMake's generated
// build files (ninja build files, CMakeCache.txt, etc.). Use a staging directory that
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index 43dd805..005f95f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -169,6 +169,9 @@
"-Xep:Finalize:OFF",
"-Xep:AddressSelection:OFF",
"-Xep:StringCharset:OFF",
+ "-Xep:EnumOrdinal:OFF",
+ "-Xep:ClassInitializationDeadlock:OFF",
+ "-Xep:VoidUsed:OFF",
// We allow inter library RestrictTo usage.
"-Xep:RestrictTo:OFF",
@@ -247,6 +250,15 @@
"-Xep:CatchAndPrintStackTrace:ERROR",
"-Xep:MixedMutabilityReturnType:ERROR",
+ // Enforce checks related to nullness annotation usage
+ "-Xep:NullablePrimitiveArray:ERROR",
+ "-Xep:MultipleNullnessAnnotations:ERROR",
+ "-Xep:NullablePrimitive:ERROR",
+ "-Xep:NullableVoid:ERROR",
+ "-Xep:NullableWildcard:ERROR",
+ "-Xep:NullableTypeParameter:ERROR",
+ "-Xep:NullableConstructor:ERROR",
+
// Nullaway
"-XepIgnoreUnknownCheckNames", // https://github.com/uber/NullAway/issues/25
"-Xep:NullAway:ERROR",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt b/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt
index 1b01fa2..202a030 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt
@@ -19,21 +19,20 @@
import androidx.build.logging.TERMINAL_RED
import androidx.build.logging.TERMINAL_RESET
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
-import com.facebook.ktfmt.format.Formatter
-import com.facebook.ktfmt.format.Formatter.format
+import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.file.Paths
import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.runBlocking
import org.gradle.api.DefaultTask
import org.gradle.api.Project
+import org.gradle.api.attributes.java.TargetJvmEnvironment
+import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
@@ -43,21 +42,19 @@
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
-import org.intellij.lang.annotations.Language
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.gradle.kotlin.dsl.named
+import org.gradle.process.ExecOperations
fun Project.configureKtfmt() {
- tasks.register("ktFormat", KtfmtFormatTask::class.java)
+ val ktfmtClasspath = getKtfmtConfiguration()
+ tasks.register("ktFormat", KtfmtFormatTask::class.java) { task ->
+ task.ktfmtClasspath.from(ktfmtClasspath)
+ }
val ktCheckTask =
tasks.register("ktCheck", KtfmtCheckTask::class.java) { task ->
+ task.ktfmtClasspath.from(ktfmtClasspath)
task.cacheEvenIfNoOutputs()
- // Workaround for https://github.com/gradle/gradle/issues/29205
- // Our ktfmt tasks declare "src" as an input, while our KotlinCompile tasks use
- // something like src/main/java as an input
- // Currently Gradle can sometimes get confused when loading a parent and child directory
- // at the same time, so we ask Gradle to avoid running both tasks in parallel
- task.mustRunAfter(project.tasks.withType(KotlinCompile::class.java))
}
// afterEvaluate because Gradle's default "check" task doesn't exist yet
@@ -78,11 +75,27 @@
)
private val ExcludedDirectoryGlobs = ExcludedDirectories.map { "**/$it/**/*.kt" }
+private const val MainClass = "com.facebook.ktfmt.cli.Main"
private const val InputDir = "src"
private const val IncludedFiles = "**/*.kt"
+private fun Project.getKtfmtConfiguration(): FileCollection {
+ val conf = configurations.detachedConfiguration(dependencies.create(getLibraryByName("ktfmt")))
+ conf.attributes {
+ it.attribute(
+ TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
+ project.objects.named(TargetJvmEnvironment.STANDARD_JVM)
+ )
+ }
+ return files(conf)
+}
+
@CacheableTask
abstract class BaseKtfmtTask : DefaultTask() {
+ @get:Inject abstract val execOperations: ExecOperations
+
+ @get:Classpath abstract val ktfmtClasspath: ConfigurableFileCollection
+
@get:Inject abstract val objects: ObjectFactory
@get:Internal val projectPath: String = project.path
@@ -114,54 +127,39 @@
protected fun runKtfmt(format: Boolean) {
if (getInputFiles().files.isEmpty()) return
- runBlocking(Dispatchers.IO) {
- val result = processInputFiles()
- val incorrectlyFormatted = result.filter { !it.isCorrectlyFormatted }
- if (incorrectlyFormatted.isNotEmpty()) {
- if (format) {
- incorrectlyFormatted.forEach { it.input.writeText(it.formattedCode) }
- } else {
- error(
- "Found ${incorrectlyFormatted.size} files that are not correctly " +
- "formatted:\n" +
- incorrectlyFormatted.map { it.input }.joinToString("\n") +
- """
-
- ********************************************************************************
- You can attempt to automatically fix these issues with:
- ./gradlew $projectPath:ktFormat
- ********************************************************************************
- """
- .trimIndent()
- )
- }
- }
+ val outputStream = ByteArrayOutputStream()
+ execOperations.javaexec { javaExecSpec ->
+ javaExecSpec.standardOutput = outputStream
+ javaExecSpec.mainClass.set(MainClass)
+ javaExecSpec.classpath = ktfmtClasspath
+ javaExecSpec.args = getArgsList(format = format)
+ javaExecSpec.jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
+ overrideDirectory?.let { javaExecSpec.workingDir = it }
+ }
+ val output = outputStream.toString()
+ if (output.isNotEmpty()) {
+ processOutput(output)
+ }
+ if (output.isNotEmpty()) {
+ error(processOutput(output))
}
}
- /** Run ktfmt on all the files in [getInputFiles] in parallel. */
- private suspend fun processInputFiles(): List<KtfmtResult> {
- return coroutineScope { getInputFiles().files.map { async { processFile(it) } }.awaitAll() }
- }
+ open fun processOutput(output: String): String =
+ """
+ Failed check for the following files:
+ $output
+ """
+ .trimIndent()
- /** Run ktfmt on the [input] file. */
- private fun processFile(input: File): KtfmtResult {
- val originCode = input.readText()
- val formattedCode = format(Formatter.KOTLINLANG_FORMAT, originCode)
- return KtfmtResult(
- input = input,
- isCorrectlyFormatted = originCode == formattedCode,
- formattedCode = formattedCode
- )
+ private fun getArgsList(format: Boolean): List<String> {
+ val arguments = mutableListOf("--kotlinlang-style")
+ if (!format) arguments.add("--dry-run")
+ arguments.addAll(getInputFiles().files.map { it.absolutePath })
+ return arguments
}
}
-internal data class KtfmtResult(
- val input: File,
- val isCorrectlyFormatted: Boolean,
- @Language("kotlin") val formattedCode: String,
-)
-
@CacheableTask
abstract class KtfmtFormatTask : BaseKtfmtTask() {
init {
@@ -189,6 +187,18 @@
fun runCheck() {
runKtfmt(format = false)
}
+
+ override fun processOutput(output: String): String =
+ """
+ Failed check for the following files:
+ $output
+
+ ********************************************************************************
+ ${TERMINAL_RED}You can automatically fix these issues with:
+ ./gradlew $projectPath:ktFormat$TERMINAL_RESET
+ ********************************************************************************
+ """
+ .trimIndent()
}
@CacheableTask
@@ -243,33 +253,35 @@
@TaskAction
fun runCheck() {
- try {
- runKtfmt(format = format)
- } catch (e: IllegalStateException) {
- val kotlinFiles =
- files.filter { file ->
- val isKotlinFile = file.endsWith(".kt") || file.endsWith(".ktx")
- val inExcludedDir =
- Paths.get(file).any { subPath ->
- ExcludedDirectories.contains(subPath.toString())
- }
+ runKtfmt(format = format)
+ }
- isKotlinFile && !inExcludedDir
- }
- error(
- """
+ override fun processOutput(output: String): String {
+ val kotlinFiles =
+ files.filter { file ->
+ val isKotlinFile = file.endsWith(".kt") || file.endsWith(".ktx")
+ val inExcludedDir =
+ Paths.get(file).any { subPath ->
+ ExcludedDirectories.contains(subPath.toString())
+ }
- ********************************************************************************
- ${TERMINAL_RED}You can attempt to automatically fix these issues with:
- ./gradlew :ktCheckFile --format ${kotlinFiles.joinToString(separator = " "){ "--file $it" }}$TERMINAL_RESET
- ********************************************************************************
- """
- .trimIndent()
- )
- }
+ isKotlinFile && !inExcludedDir
+ }
+ return """
+ Failed check for the following files:
+ $output
+
+ ********************************************************************************
+ ${TERMINAL_RED}You can attempt to automatically fix these issues with:
+ ./gradlew :ktCheckFile --format ${kotlinFiles.joinToString(separator = " "){ "--file $it" }}$TERMINAL_RESET
+ ********************************************************************************
+ """
+ .trimIndent()
}
}
fun Project.configureKtfmtCheckFile() {
- tasks.register("ktCheckFile", KtfmtCheckFileTask::class.java)
+ tasks.register("ktCheckFile", KtfmtCheckFileTask::class.java) { task ->
+ task.ktfmtClasspath.from(getKtfmtConfiguration())
+ }
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 563931b..25c421d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -341,10 +341,10 @@
disable.add("IllegalExperimentalApiUsage")
}
- // Only allow the ArrayMigration check to be run when opted-in, since this is meant to be
- // run once per project when switching to type-use nullness annotations.
- if (!project.migrateArrayAnnotations()) {
- disable.add("ArrayMigration")
+ // Only allow the JSpecifyNullness check to be run when opted-in, while migrating projects
+ // to use JSpecify annotations.
+ if (!project.useJSpecifyAnnotations()) {
+ disable.add("JSpecifyNullness")
}
fatal.add("UastImplementation") // go/hide-uast-impl
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index 6efe979..ad04c87 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -222,10 +222,10 @@
/**
* Finds the main compilation for a source set, usually called 'main' but for android we need to
- * search for 'debug' instead.
+ * search for 'release' instead.
*/
private fun KotlinTarget.mainCompilation() =
- compilations.findByName(MAIN_COMPILATION_NAME) ?: compilations.getByName("debug")
+ compilations.findByName(MAIN_COMPILATION_NAME) ?: compilations.getByName("release")
/**
* Writes a metadata file to the given [metadataFile] location for all multiplatform Kotlin source
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
index 938ebaa..94f0d30 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
@@ -32,7 +32,6 @@
import androidx.build.version
import com.android.utils.appendCapitalized
import kotlinx.validation.KlibDumpMetadata
-import kotlinx.validation.KotlinApiCompareTask
import kotlinx.validation.KotlinKlibAbiBuildTask
import kotlinx.validation.KotlinKlibExtractAbiTask
import kotlinx.validation.KotlinKlibMergeAbiTask
@@ -153,10 +152,10 @@
project.tasks
.register(
CHECK_NAME.appendCapitalized(NATIVE_SUFFIX),
- KotlinApiCompareTask::class.java
+ CheckAbiEquivalenceTask::class.java
) {
- it.projectApiFile.set(projectApiFile.map { fileProperty -> fileProperty.get() })
- it.generatedApiFile.set(generatedApiFile.map { fileProperty -> fileProperty.get() })
+ it.checkedInDump = projectApiFile
+ it.builtDump = generatedApiFile
it.group = ABI_GROUP_NAME
}
.also { task -> task.configure { it.cacheEvenIfNoOutputs() } }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckApiEquivalenceTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckApiEquivalenceTask.kt
new file mode 100644
index 0000000..9ff9c13
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckApiEquivalenceTask.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.binarycompatibilityvalidator
+
+import androidx.build.metalava.checkEqual
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+@CacheableTask
+abstract class CheckAbiEquivalenceTask : DefaultTask() {
+
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputFile
+ abstract var checkedInDump: Provider<RegularFileProperty>
+
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputFile
+ abstract var builtDump: Provider<RegularFileProperty>
+
+ @TaskAction
+ fun execute() {
+ checkEqual(checkedInDump.get().asFile.get(), builtDump.get().asFile.get(), "updateAbi")
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index db10029..46af070 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -105,7 +105,8 @@
when (plugin) {
is LibraryPlugin -> {
val libraryExtension = project.extensions.getByType<LibraryExtension>()
- libraryExtension.compileSdk = project.defaultAndroidConfig.compileSdk
+ libraryExtension.compileSdk =
+ project.defaultAndroidConfig.latestStableCompileSdk
libraryExtension.buildToolsVersion =
project.defaultAndroidConfig.buildToolsVersion
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt
index 0e586fb..f227d55 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt
@@ -90,7 +90,7 @@
return diffLines.joinToString("\n")
}
-fun checkEqual(expected: File, actual: File) {
+fun checkEqual(expected: File, actual: File, updateTaskName: String = "updateApi") {
if (!FileUtils.contentEquals(expected, actual)) {
val diff = summarizeDiff(expected, actual)
val message =
@@ -99,7 +99,7 @@
Declared definition is $expected
True definition is $actual
- Please run `./gradlew updateApi` to confirm these changes are
+ Please run `./gradlew ${updateTaskName}` to confirm these changes are
intentional by updating the API definition.
Difference between these files:
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 ffb4cc8..144e37e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -323,11 +323,11 @@
}
}
- // For library modules we only look at the build type debug. The target app project can be
+ // For library modules we only look at the build type release. The target app project can be
// specified through the androidX extension, through: targetAppProjectForInstrumentationTest
// and targetAppProjectVariantForInstrumentationTest.
extensions.findByType(LibraryAndroidComponentsExtension::class.java)?.apply {
- onVariants(selector().withBuildType("debug")) { variant ->
+ onVariants(selector().withBuildType("release")) { variant ->
val targetAppProject =
androidXExtension.deviceTests.targetAppProject ?: return@onVariants
val targetAppProjectVariant = androidXExtension.deviceTests.targetAppVariant
@@ -616,7 +616,7 @@
multiplatformExtension
?.targets
?.filterIsInstance<KotlinAndroidTarget>()
- ?.mapNotNull { it.compilations.find { it.name == "debugAndroidTest" } }
+ ?.mapNotNull { it.compilations.find { it.name == "releaseAndroidTest" } }
?.flatMap { it.allKotlinSourceSets }
?.mapTo(testSourceFileCollections) { it.kotlin.sourceDirectories }
return testSourceFileCollections
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
index 5667601..85f598f 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt
@@ -33,6 +33,12 @@
sdkString.toInt()
}
+ override val latestStableCompileSdk: Int by lazy {
+ val sdkString = project.extraPropertyOrNull(LATEST_STABLE_COMPILE_SDK)?.toString()
+ check(sdkString != null) { "$LATEST_STABLE_COMPILE_SDK is unset" }
+ sdkString.toInt()
+ }
+
override val minSdk: Int = 21
override val targetSdk: Int by lazy {
@@ -41,6 +47,7 @@
companion object {
private const val COMPILE_SDK = "androidx.compileSdk"
+ private const val LATEST_STABLE_COMPILE_SDK = "androidx.latestStableCompileSdk"
private const val TARGET_SDK_VERSION = "androidx.targetSdkVersion"
/**
@@ -50,6 +57,7 @@
val GRADLE_PROPERTIES =
listOf(
COMPILE_SDK,
+ LATEST_STABLE_COMPILE_SDK,
TARGET_SDK_VERSION,
)
}
@@ -70,6 +78,13 @@
*/
val compileSdk: Int
+ /**
+ * The latest stable compile SDK version that is available to use for AndroidX projects.
+ *
+ * This may be specified in `gradle.properties` using `androidx.latestStableCompileSdk`.
+ */
+ val latestStableCompileSdk: Int
+
/** Default minimum SDK version used for AndroidX projects. */
val minSdk: Int
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index cfb12d4..7ca3e2e 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -32,30 +32,31 @@
* LibraryType.checkApi represents whether we enforce API compatibility of the library according to
* our semantic versioning protocol
*
- * The possible values of LibraryType are as follows: PUBLISHED_LIBRARY: a conventional library,
- * published, sourced, documented, and versioned. PUBLISHED_TEST_LIBRARY: PUBLISHED_LIBRARY, but
- * allows calling @VisibleForTesting API. Used for libraries that allow developers to test code that
- * uses your library. Often provides test fakes. INTERNAL_TEST_LIBRARY: unpublished, untracked,
- * undocumented. Used in internal tests. Usually contains integration tests, but is _not_ an app.
- * Runs device tests. INTERNAL_HOST_TEST_LIBRARY: as INTERNAL_TEST_LIBRARY, but runs host tests
- * instead. Avoid mixing host tests and device tests in the same library, for performance /
- * test-result-caching reasons. SAMPLES: a library containing sample code referenced in your
- * library's documentation with @sampled, published as a documentation-related supplement to a
- * conventional library. LINT: a library of lint rules for using a conventional library. Published
- * through lintPublish as part of an AAR, not published standalone. COMPILER_DAEMON: a tool that
- * modifies the kotlin or java compiler. Used only while compiling. Has no API and does not publish
- * source jars, but does release to maven. COMPILER_DAEMON_TEST: a compiler plugin that is not
- * published at all, for internal-only use. COMPILER_PLUGIN: as COMPILER_DAEMON, but is compatible
- * with JDK 11. GRADLE_PLUGIN: a library that is a gradle plugin. ANNOTATION_PROCESSOR: a library
- * consisting of an annotation processor. Used only while compiling. ANNOTATION_PROCESSOR_UTILS:
- * contains reference code for understanding an annotation processor. Publishes source jars, but
- * does not track API. OTHER_CODE_PROCESSOR: a library that algorithmically generates and/or alters
- * code but not through hooking into custom annotations or the kotlin compiler. For example,
- * navigation:safe-args-generator or Jetifier. IDE_PLUGIN: a library that should only ever be
- * downloaded by studio. Unfortunately, we don't yet have a good way to track API for these.
- * b/281843422 UNSET: a library that has not yet been migrated to using LibraryType. Should never be
- * used. APP: an app, such as an example app or integration testsapp. Should never be used; apps
- * should not apply the AndroidX plugin or have an androidx block in their build.gradle files.
+ * The possible values of LibraryType are as follows:
+ * - [PUBLISHED_LIBRARY]: a conventional library published, sourced, documented, and versioned.
+ * - [PUBLISHED_TEST_LIBRARY]: [PUBLISHED_LIBRARY], but allows calling `@VisibleForTesting` API.
+ * Used for libraries that allow developers to test code that uses your library. Often provides
+ * test fakes.
+ * - [INTERNAL_TEST_LIBRARY]: unpublished, untracked, undocumented. Used in internal tests. Usually
+ * contains integration tests, but is _not_ an app. Runs device tests.
+ * - [INTERNAL_HOST_TEST_LIBRARY]: as [INTERNAL_TEST_LIBRARY], but runs host tests instead. Avoid
+ * mixing host tests and device tests in the same library, for performance / test-result-caching
+ * reasons.
+ * - [SAMPLES]: a library containing sample code referenced in your library's documentation with
+ * `@sampled`, published as a documentation-related supplement to a conventional library.
+ * - [LINT]: a library of lint rules for using a conventional library. Published through lintPublish
+ * as part of an AAR, not published standalone.
+ * - [GRADLE_PLUGIN]: a library that is a gradle plugin.
+ * - [ANNOTATION_PROCESSOR]: a library consisting of an annotation processor. Used only while
+ * compiling.
+ * - [ANNOTATION_PROCESSOR_UTILS]: contains reference code for understanding an annotation
+ * processor. Publishes source jars, but does not track API.
+ * - [OTHER_CODE_PROCESSOR]: a library that algorithmically generates and/or alters code but not
+ * through hooking into custom annotations or the kotlin compiler. For example,
+ * navigation:safe-args-generator or Jetifier.
+ * - [IDE_PLUGIN]: a library that should only ever be downloaded by studio. Unfortunately, we don't
+ * yet have a good way to track API for these. b/281843422
+ * - [UNSET]: a library that has not yet been migrated to using LibraryType. Should never be used.
*/
sealed class LibraryType(
val publish: Publish = Publish.NONE,
@@ -69,29 +70,25 @@
get() = javaClass.simpleName
companion object {
- val PUBLISHED_LIBRARY = PublishedLibrary()
+ @JvmStatic val ANNOTATION_PROCESSOR = AnnotationProcessor()
+ @JvmStatic val ANNOTATION_PROCESSOR_UTILS = AnnotationProcessorUtils()
+ @JvmStatic val GRADLE_PLUGIN = GradlePlugin()
+ @JvmStatic val IDE_PLUGIN = IdePlugin()
+ @JvmStatic val INTERNAL_TEST_LIBRARY = InternalTestLibrary()
+ @JvmStatic val INTERNAL_HOST_TEST_LIBRARY = InternalHostTestLibrary()
+ @JvmStatic val LINT = Lint()
+ @JvmStatic val PUBLISHED_LIBRARY = PublishedLibrary()
+ @JvmStatic
val PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
PublishedLibrary(targetsKotlinConsumersOnly = true)
- val PUBLISHED_TEST_LIBRARY = PublishedTestLibrary()
+ @JvmStatic val PUBLISHED_TEST_LIBRARY = PublishedTestLibrary()
+ @JvmStatic
val PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY =
PublishedTestLibrary(targetsKotlinConsumersOnly = true)
- val INTERNAL_TEST_LIBRARY = InternalTestLibrary()
- val INTERNAL_HOST_TEST_LIBRARY = InternalHostTestLibrary()
- val SAMPLES = Samples()
- val LINT = Lint()
- val COMPILER_DAEMON = CompilerDaemon()
- val COMPILER_DAEMON_TEST = CompilerDaemonTest()
- val COMPILER_PLUGIN = CompilerPlugin()
- val GRADLE_PLUGIN = GradlePlugin()
- val ANNOTATION_PROCESSOR = AnnotationProcessor()
- val ANNOTATION_PROCESSOR_UTILS = AnnotationProcessorUtils()
- val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
- val IDE_PLUGIN = IdePlugin()
+ @JvmStatic val SAMPLES = Samples()
+ @JvmStatic val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
val UNSET = Unset()
- @Deprecated("Do not use an androidx block for apps/testapps, only for libraries")
- val APP = UNSET
- @Suppress("DEPRECATION")
private val allTypes =
mapOf(
"PUBLISHED_LIBRARY" to PUBLISHED_LIBRARY,
@@ -103,16 +100,12 @@
"INTERNAL_HOST_TEST_LIBRARY" to INTERNAL_HOST_TEST_LIBRARY,
"SAMPLES" to SAMPLES,
"LINT" to LINT,
- "COMPILER_DAEMON" to COMPILER_DAEMON,
- "COMPILER_DAEMON_TEST" to COMPILER_DAEMON_TEST,
- "COMPILER_PLUGIN" to COMPILER_PLUGIN,
"GRADLE_PLUGIN" to GRADLE_PLUGIN,
"ANNOTATION_PROCESSOR" to ANNOTATION_PROCESSOR,
"ANNOTATION_PROCESSOR_UTILS" to ANNOTATION_PROCESSOR_UTILS,
"OTHER_CODE_PROCESSOR" to OTHER_CODE_PROCESSOR,
"IDE_PLUGIN" to IDE_PLUGIN,
"UNSET" to UNSET,
- "APP" to APP
)
fun valueOf(name: String): LibraryType {
diff --git a/buildSrc/shared-dependencies.gradle b/buildSrc/shared-dependencies.gradle
index 356e5ef..bc37d9f 100644
--- a/buildSrc/shared-dependencies.gradle
+++ b/buildSrc/shared-dependencies.gradle
@@ -39,8 +39,6 @@
}
implementation(libs.xerces)
- implementation(libs.ktfmt)
- implementation(libs.kotlinCoroutinesCore)
implementation(libs.shadow) // used by BundleInsideHelper.kt
api(libs.apacheAnt) // used in AarManifestTransformerTask.kt for unziping
implementation(libs.toml)
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index e9e6568..c615c95 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
-import android.hardware.camera2.CaptureRequest
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraPipe
@@ -36,7 +35,6 @@
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
-import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository
import androidx.camera.camera2.pipe.integration.impl.CapturePipeline
@@ -54,7 +52,6 @@
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.SessionConfig
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -172,38 +169,10 @@
}
}
- override var runningUseCases = useCases.toSet()
-
- override var isPrimary: Boolean = true
- set(value) {
- field = value
- }
-
- override fun <T> setParameterAsync(
- key: CaptureRequest.Key<T>,
- value: T,
- priority: Config.OptionPriority
- ): Deferred<Unit> {
- throw NotImplementedError("Not implemented")
- }
-
- override fun setParametersAsync(
- values: Map<CaptureRequest.Key<*>, Any>,
- priority: Config.OptionPriority
- ): Deferred<Unit> {
- throw NotImplementedError("Not implemented")
- }
-
override fun close(): Job {
return threads.scope.launch {
useCaseCameraGraphConfig.graph.close()
useCaseSurfaceManager.stopAsync().await()
}
}
-
- companion object {
- fun SessionConfig.toCamera2ImplConfig(): Camera2ImplConfig {
- return Camera2ImplConfig(implementationOptions)
- }
- }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ApiCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ApiCompat.kt
index 5657c07..f5f631e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ApiCompat.kt
@@ -17,10 +17,13 @@
package androidx.camera.camera2.pipe.integration.compat
import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.view.Surface
import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.impl.CameraProperties
@RequiresApi(Build.VERSION_CODES.N)
internal object Api24Compat {
@@ -48,4 +51,17 @@
) {
callback.onReadoutStarted(session, request, timestamp, frameNumber)
}
+
+ @JvmStatic
+ fun isZoomOverrideAvailable(cameraProperties: CameraProperties): Boolean =
+ cameraProperties.metadata[CameraCharacteristics.CONTROL_AVAILABLE_SETTINGS_OVERRIDES]
+ ?.contains(CameraMetadata.CONTROL_SETTINGS_OVERRIDE_ZOOM) ?: false
+
+ @JvmStatic
+ fun setSettingsOverrideZoom(parameters: MutableMap<CaptureRequest.Key<*>, Any>) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ parameters[CaptureRequest.CONTROL_SETTINGS_OVERRIDE] =
+ CameraMetadata.CONTROL_SETTINGS_OVERRIDE_ZOOM
+ }
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/Camera2CameraControlCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/Camera2CameraControlCompat.kt
index bf98070..684ed09 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/Camera2CameraControlCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/Camera2CameraControlCompat.kt
@@ -24,7 +24,6 @@
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.impl.containsTag
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
@@ -52,7 +51,7 @@
public fun cancelCurrentTask()
public fun applyAsync(
- camera: UseCaseCamera?,
+ requestControl: UseCaseCameraRequestControl?,
cancelPreviousTask: Boolean = true
): Deferred<Void?>
@@ -106,11 +105,14 @@
?.cancelSignal("The camera control has became inactive.")
}
- override fun applyAsync(camera: UseCaseCamera?, cancelPreviousTask: Boolean): Deferred<Void?> {
+ override fun applyAsync(
+ requestControl: UseCaseCameraRequestControl?,
+ cancelPreviousTask: Boolean
+ ): Deferred<Void?> {
val signal: CompletableDeferred<Void?> = CompletableDeferred()
val config = synchronized(lock) { configBuilder.build() }
synchronized(updateSignalLock) {
- if (camera != null) {
+ if (requestControl != null) {
if (cancelPreviousTask) {
// Cancel the previous request signal if exist.
updateSignal?.cancelSignal()
@@ -122,7 +124,7 @@
}
updateSignal = signal
- camera.requestControl.setConfigAsync(
+ requestControl.setConfigAsync(
type = UseCaseCameraRequestControl.Type.CAMERA2_CAMERA_CONTROL,
config = config,
tags = mapOf(TAG_KEY to signal.hashCode())
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
index 577e4db..83c1aa3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
@@ -18,6 +18,7 @@
import androidx.camera.camera2.pipe.integration.compat.workaround.AutoFlashAEModeDisabler
import androidx.camera.camera2.pipe.integration.compat.workaround.InactiveSurfaceCloser
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage
import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
import androidx.camera.camera2.pipe.integration.compat.workaround.TemplateParamsOverride
import androidx.camera.camera2.pipe.integration.compat.workaround.UseFlashModeTorchFor3aUpdate
@@ -34,6 +35,7 @@
UseFlashModeTorchFor3aUpdate.Bindings::class,
UseTorchAsFlash.Bindings::class,
TemplateParamsOverride.Bindings::class,
+ Lock3ABehaviorWhenCaptureImage.Bindings::class,
],
)
public abstract class CameraCompatModule
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
index 17c69c4..7901a98 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
@@ -30,7 +30,7 @@
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.core.CameraControl
import dagger.Binds
@@ -49,7 +49,7 @@
public fun applyAsync(
evCompIndex: Int,
- camera: UseCaseCamera,
+ requestControl: UseCaseCameraRequestControl,
cancelPreviousTask: Boolean,
): Deferred<Int>
@@ -97,7 +97,7 @@
override fun applyAsync(
evCompIndex: Int,
- camera: UseCaseCamera,
+ requestControl: UseCaseCameraRequestControl,
cancelPreviousTask: Boolean,
): Deferred<Int> {
val signal = CompletableDeferred<Int>()
@@ -122,7 +122,9 @@
updateListener = null
}
- camera.setParameterAsync(CONTROL_AE_EXPOSURE_COMPENSATION, evCompIndex)
+ requestControl.setParametersAsync(
+ values = mapOf(CONTROL_AE_EXPOSURE_COMPENSATION to evCompIndex)
+ )
// Prepare the listener to wait for the exposure value to reach the target.
updateListener =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ZoomCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ZoomCompat.kt
index 43262e0..6b284cb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ZoomCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/ZoomCompat.kt
@@ -25,7 +25,7 @@
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.compat.workaround.getControlZoomRatioRangeSafely
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.internal.ZoomMath.nearZero
import dagger.Module
import dagger.Provides
@@ -35,7 +35,10 @@
public val minZoomRatio: Float
public val maxZoomRatio: Float
- public fun applyAsync(zoomRatio: Float, camera: UseCaseCamera): Deferred<Unit>
+ public fun applyAsync(
+ zoomRatio: Float,
+ requestControl: UseCaseCameraRequestControl
+ ): Deferred<Unit>
/**
* Returns the current crop sensor region which should be used for converting
@@ -84,11 +87,16 @@
private var currentCropRect: Rect? = null
- override fun applyAsync(zoomRatio: Float, camera: UseCaseCamera): Deferred<Unit> {
+ override fun applyAsync(
+ zoomRatio: Float,
+ requestControl: UseCaseCameraRequestControl
+ ): Deferred<Unit> {
val sensorRect =
cameraProperties.metadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]!!
currentCropRect = computeCropRect(sensorRect, zoomRatio)
- return camera.setParameterAsync(CaptureRequest.SCALER_CROP_REGION, currentCropRect)
+ return requestControl.setParametersAsync(
+ values = mapOf(CaptureRequest.SCALER_CROP_REGION to (currentCropRect as Any))
+ )
}
override fun getCropSensorRegion(): Rect =
@@ -119,15 +127,27 @@
private val cameraProperties: CameraProperties,
private val range: Range<Float>,
) : ZoomCompat {
+ private val shouldOverrideZoom =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
+ Api34Compat.isZoomOverrideAvailable(cameraProperties)
+
override val minZoomRatio: Float
get() = range.lower
override val maxZoomRatio: Float
get() = range.upper
- override fun applyAsync(zoomRatio: Float, camera: UseCaseCamera): Deferred<Unit> {
+ override fun applyAsync(
+ zoomRatio: Float,
+ requestControl: UseCaseCameraRequestControl
+ ): Deferred<Unit> {
require(zoomRatio in minZoomRatio..maxZoomRatio)
- return camera.setParameterAsync(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio)
+ val parameters: MutableMap<CaptureRequest.Key<*>, Any> =
+ mutableMapOf(CaptureRequest.CONTROL_ZOOM_RATIO to zoomRatio)
+ if (shouldOverrideZoom && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Api34Compat.setSettingsOverrideZoom(parameters)
+ }
+ return requestControl.setParametersAsync(values = parameters)
}
override fun getCropSensorRegion(): Rect =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
index 7988db32..81e63e1 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -240,6 +240,14 @@
) {
quirks.add(ImageCaptureFailedForVideoSnapshotQuirk())
}
+ if (
+ quirkSettings.shouldEnableQuirk(
+ LockAeAndCaptureImageBreakCameraQuirk::class.java,
+ LockAeAndCaptureImageBreakCameraQuirk.isEnabled(cameraMetadata)
+ )
+ ) {
+ quirks.add(LockAeAndCaptureImageBreakCameraQuirk())
+ }
Quirks(quirks).also {
Logger.d(TAG, "camera2-pipe-integration CameraQuirks = " + Quirks.toString(it))
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/LockAeAndCaptureImageBreakCameraQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/LockAeAndCaptureImageBreakCameraQuirk.kt
new file mode 100644
index 0000000..95ba980
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/LockAeAndCaptureImageBreakCameraQuirk.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics.LENS_FACING
+import android.hardware.camera2.CameraMetadata.LENS_FACING_BACK
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage
+import androidx.camera.core.impl.Quirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: b/360106037
+ * - Description: Quirk indicating that locking AE (Auto Exposure) and taking pictures can lead to
+ * an abnormal camera service state on Pixel 3 back camera. Although the picture is successfully
+ * taken, the camera service becomes unresponsive without any error callbacks. Reopening the
+ * camera can restore its functionality.
+ * - Device(s): Pixel 3.
+ *
+ * @see Lock3ABehaviorWhenCaptureImage
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+public class LockAeAndCaptureImageBreakCameraQuirk : Quirk {
+
+ public companion object {
+ public fun isEnabled(cameraMetadata: CameraMetadata): Boolean {
+ return isPixel3 && cameraMetadata[LENS_FACING] == LENS_FACING_BACK
+ }
+
+ private val isPixel3: Boolean
+ get() = "Pixel 3".equals(Build.MODEL, ignoreCase = true)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt
index 40f7e51..cad386b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt
@@ -24,9 +24,10 @@
/**
* QuirkSummary
- * - Bug Id: 252818931, 261744070, 319913852
- * - Description: On certain devices, the captured image has color issue for reprocessing. We need
- * to disable zero-shutter lag and return false for [CameraInfo.isZslSupported].
+ * - Bug Id: 252818931, 261744070, 319913852, 361328838
+ * - Description: On certain devices, the captured image has color or zoom freezing issue for
+ * reprocessing. We need to disable zero-shutter lag and return false for
+ * [CameraInfo.isZslSupported].
* - Device(s): Samsung Fold4, Samsung s22, Xiaomi Mi 8
*/
@SuppressLint("CameraXQuirksClassDetector")
@@ -34,7 +35,8 @@
public class ZslDisablerQuirk : Quirk {
public companion object {
- private val AFFECTED_SAMSUNG_MODEL = listOf("SM-F936", "SM-S901U", "SM-S908U", "SM-S908U1")
+ private val AFFECTED_SAMSUNG_MODEL =
+ listOf("SM-F936", "SM-S901U", "SM-S908U", "SM-S908U1", "SM-F721U1", "SM-S928U1")
private val AFFECTED_XIAOMI_MODEL = listOf("MI 8")
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/Lock3ABehaviorWhenCaptureImage.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/Lock3ABehaviorWhenCaptureImage.kt
new file mode 100644
index 0000000..f893efd
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/Lock3ABehaviorWhenCaptureImage.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import androidx.camera.camera2.pipe.Lock3ABehavior
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.LockAeAndCaptureImageBreakCameraQuirk
+import dagger.Module
+import dagger.Provides
+
+/**
+ * Provides customized 3A lock behaviors before capturing an image.
+ *
+ * @property hasAeLockBehavior Indicates whether there is a specific AE (Auto Exposure) lock
+ * behavior defined. If true, the [aeLockBehavior] will be used; otherwise, the default AE
+ * behavior will be applied.
+ * @property aeLockBehavior The specific AE lock behavior to apply if [hasAeLockBehavior] is true.
+ * If null and [hasAeLockBehavior] is true, AE lock will be effectively disabled.
+ * @property hasAfLockBehavior Indicates whether there is a specific AF (Auto Focus) lock behavior
+ * defined. If true, the [afLockBehavior] will be used; otherwise, the default AF behavior will be
+ * applied.
+ * @property afLockBehavior The specific AF lock behavior to apply if [hasAfLockBehavior] is true.
+ * If null and [hasAfLockBehavior] is true, AF lock will be effectively disabled.
+ * @property hasAwbLockBehavior Indicates whether there is a specific AWB (Auto White Balance) lock
+ * behavior defined. If true, the [awbLockBehavior] will be used; otherwise, the default AWB
+ * behavior will be applied.
+ * @property awbLockBehavior The specific AWB lock behavior to apply if [hasAwbLockBehavior] is
+ * true. If null and [hasAwbLockBehavior] is true, AWB lock will be effectively disabled.
+ * @see LockAeAndCaptureImageBreakCameraQuirk
+ */
+public class Lock3ABehaviorWhenCaptureImage(
+ private val hasAeLockBehavior: Boolean = false,
+ private val aeLockBehavior: Lock3ABehavior? = null,
+ private val hasAfLockBehavior: Boolean = false,
+ private val afLockBehavior: Lock3ABehavior? = null,
+ private val hasAwbLockBehavior: Boolean = false,
+ private val awbLockBehavior: Lock3ABehavior? = null,
+) {
+
+ /**
+ * Gets customized 3A lock behaviors, using provided defaults if no specific behavior is set.
+ *
+ * This method checks the `has*LockBehavior` properties to determine if a custom behavior is
+ * defined for each 3A lock type (AE, AF, AWB). If a custom behavior is defined, it will be
+ * returned; otherwise, the corresponding `default*Behavior` will be used.
+ *
+ * @param defaultAeBehavior Default AE lock behavior if none is specified.
+ * @param defaultAfBehavior Default AF lock behavior if none is specified.
+ * @param defaultAwbBehavior Default AWB lock behavior if none is specified.
+ * @return A Triple containing the customized AE, AF, and AWB lock behaviors.
+ */
+ public fun getLock3ABehaviors(
+ defaultAeBehavior: Lock3ABehavior? = null,
+ defaultAfBehavior: Lock3ABehavior? = null,
+ defaultAwbBehavior: Lock3ABehavior? = null
+ ): Triple<Lock3ABehavior?, Lock3ABehavior?, Lock3ABehavior?> =
+ Triple(
+ if (hasAeLockBehavior) aeLockBehavior else defaultAeBehavior,
+ if (hasAfLockBehavior) afLockBehavior else defaultAfBehavior,
+ if (hasAwbLockBehavior) awbLockBehavior else defaultAwbBehavior
+ )
+
+ @Module
+ public abstract class Bindings {
+ public companion object {
+ @Provides
+ public fun provideLock3ABehaviorBeforeCaptureImage(
+ cameraQuirks: CameraQuirks
+ ): Lock3ABehaviorWhenCaptureImage =
+ if (
+ cameraQuirks.quirks.contains(LockAeAndCaptureImageBreakCameraQuirk::class.java)
+ ) {
+ doNotLockAe3ABehavior
+ } else {
+ noCustomizedLock3ABehavior
+ }
+ }
+ }
+
+ public companion object {
+ public val noCustomizedLock3ABehavior: Lock3ABehaviorWhenCaptureImage by lazy {
+ Lock3ABehaviorWhenCaptureImage()
+ }
+
+ public val doNotLockAe3ABehavior: Lock3ABehaviorWhenCaptureImage by lazy {
+ Lock3ABehaviorWhenCaptureImage(
+ hasAeLockBehavior = true,
+ aeLockBehavior = null // Explicitly disable AE lock
+ )
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
index 372696c..1d21a90 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
@@ -49,6 +49,7 @@
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.core.Log.info
import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage
import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlash
import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.compat.workaround.shouldStopRepeatingBeforeCapture
@@ -110,6 +111,7 @@
private val threads: UseCaseThreads,
private val requestListener: ComboRequestListener,
private val useTorchAsFlash: UseTorchAsFlash,
+ private val lock3ABehaviorWhenCaptureImage: Lock3ABehaviorWhenCaptureImage,
cameraProperties: CameraProperties,
private val useCaseCameraState: UseCaseCameraState,
useCaseGraphConfig: UseCaseGraphConfig,
@@ -378,10 +380,16 @@
graph
.acquireSession()
.use {
+ val (aeLockBehavior, afLockBehavior, awbLockBehavior) =
+ lock3ABehaviorWhenCaptureImage.getLock3ABehaviors(
+ defaultAeBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+ defaultAfBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+ defaultAwbBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+ )
it.lock3A(
- aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
- afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
- awbLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+ aeLockBehavior = aeLockBehavior,
+ afLockBehavior = afLockBehavior,
+ awbLockBehavior = awbLockBehavior,
convergedTimeLimitNs = convergedTimeLimitNs,
lockedTimeLimitNs = CHECK_3A_TIMEOUT_IN_NS
)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/EvCompControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/EvCompControl.kt
index 23817fa..7a7b198 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/EvCompControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/EvCompControl.kt
@@ -57,11 +57,11 @@
compat.step,
)
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+ private var _requestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
+ _requestControl = value
updateAsync(evCompIndex, cancelPreviousTask = false)
}
@@ -86,9 +86,9 @@
)
}
- return useCaseCamera?.let { camera ->
+ return requestControl?.let { requestControl ->
evCompIndex = exposureIndex
- compat.applyAsync(exposureIndex, camera, cancelPreviousTask)
+ compat.applyAsync(exposureIndex, requestControl, cancelPreviousTask)
}
?: run {
CameraControl.OperationCanceledException("Camera is not active.").let { cancelResult
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
index 1c9f0e7..dbc0d6b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
@@ -53,11 +53,11 @@
private val torchControl: TorchControl,
private val useFlashModeTorchFor3aUpdate: UseFlashModeTorchFor3aUpdate,
) : UseCaseCameraControl {
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+ private var _requestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
+ _requestControl = value
setFlashAsync(_flashMode, false)
}
@@ -98,7 +98,7 @@
): Deferred<Unit> {
val signal = CompletableDeferred<Unit>()
- useCaseCamera?.let {
+ requestControl?.let {
// Update _flashMode immediately so that CameraControlInternal#getFlashMode()
// returns correct value.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 9e69cb2..8bfcd72 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -41,6 +41,7 @@
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.MeteringPoint
import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
import androidx.camera.core.impl.CameraControlInternal
import com.google.common.util.concurrent.ListenableFuture
import dagger.Binds
@@ -66,20 +67,20 @@
private val state3AControl: State3AControl,
private val threads: UseCaseThreads,
private val zoomCompat: ZoomCompat,
-) : UseCaseCameraControl, UseCaseCamera.RunningUseCasesChangeListener {
- private var _useCaseCamera: UseCaseCamera? = null
+) : UseCaseCameraControl, UseCaseManager.RunningUseCasesChangeListener {
+ private var _requestControl: UseCaseCameraRequestControl? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
+ _requestControl = value
}
- override fun onRunningUseCasesChanged() {
+ override fun onRunningUseCasesChanged(runningUseCases: Set<UseCase>) {
// reset to null since preview use case may not be active for current runningUseCases
previewAspectRatio = null
- _useCaseCamera?.runningUseCases?.forEach { useCase ->
+ for (useCase in runningUseCases) {
if (useCase is Preview) {
useCase.attachedSurfaceResolution?.apply {
previewAspectRatio = Rational(width, height)
@@ -119,7 +120,7 @@
): ListenableFuture<FocusMeteringResult> {
val signal = CompletableDeferred<FocusMeteringResult>()
- useCaseCamera?.let { useCaseCamera ->
+ requestControl?.let { requestControl ->
threads.sequentialScope.launch {
focusTimeoutJob?.cancel()
autoCancelJob?.cancel()
@@ -194,7 +195,7 @@
* the CameraGraph and thus may cause extra requests to the camera.
*/
debug { "startFocusAndMetering: updating 3A regions only" }
- useCaseCamera.requestControl.update3aRegions(
+ requestControl.update3aRegions(
aeRegions = aeRegions,
afRegions = afRegions,
awbRegions = awbRegions,
@@ -217,7 +218,7 @@
* If device does support but a region list is empty, it means any previously
* set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
*/
- useCaseCamera.requestControl.startFocusAndMeteringAsync(
+ requestControl.startFocusAndMeteringAsync(
aeRegions = aeRegions,
afRegions = afRegions,
awbRegions = awbRegions,
@@ -243,7 +244,7 @@
triggerFocusTimeout(autoFocusTimeoutMs, signal)
if (action.isAutoCancelEnabled) {
- triggerAutoCancel(action.autoCancelDurationInMillis, signal, useCaseCamera)
+ triggerAutoCancel(action.autoCancelDurationInMillis, signal, requestControl)
}
}
}
@@ -257,7 +258,7 @@
private fun triggerAutoCancel(
delayMillis: Long,
resultToCancel: CompletableDeferred<FocusMeteringResult>,
- useCaseCamera: UseCaseCamera,
+ requestControl: UseCaseCameraRequestControl,
) {
autoCancelJob?.cancel()
@@ -265,7 +266,7 @@
threads.sequentialScope.launch {
delay(delayMillis)
debug { "triggerAutoCancel: auto-canceling after $delayMillis ms" }
- cancelFocusAndMeteringNowAsync(useCaseCamera, resultToCancel)
+ cancelFocusAndMeteringNowAsync(requestControl, resultToCancel)
}
}
@@ -365,13 +366,13 @@
public fun cancelFocusAndMeteringAsync(): Deferred<Result3A?> {
val signal = CompletableDeferred<Result3A?>()
- useCaseCamera?.let { useCaseCamera ->
+ requestControl?.let { requestControl ->
threads.sequentialScope.launch {
focusTimeoutJob?.cancel()
autoCancelJob?.cancel()
cancelSignal?.setCancelException("Cancelled by another cancelFocusAndMetering()")
cancelSignal = signal
- cancelFocusAndMeteringNowAsync(useCaseCamera, updateSignal).propagateTo(signal)
+ cancelFocusAndMeteringNowAsync(requestControl, updateSignal).propagateTo(signal)
}
}
?: run {
@@ -382,12 +383,12 @@
}
private suspend fun cancelFocusAndMeteringNowAsync(
- useCaseCamera: UseCaseCamera,
+ requestControl: UseCaseCameraRequestControl,
signalToCancel: CompletableDeferred<FocusMeteringResult>?,
): Deferred<Result3A> {
signalToCancel?.setCancelException("Cancelled by cancelFocusAndMetering()")
state3AControl.preferredFocusMode = null
- return useCaseCamera.requestControl.cancelFocusAndMeteringAsync()
+ return requestControl.cancelFocusAndMeteringAsync()
}
private fun <T> CompletableDeferred<T>.setCancelException(message: String) {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
index 25456dc..da6fcdb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
@@ -46,12 +46,12 @@
public val cameraProperties: CameraProperties,
private val aeModeDisabler: AutoFlashAEModeDisabler,
private val aeFpsRange: AeFpsRange,
-) : UseCaseCameraControl, UseCaseCamera.RunningUseCasesChangeListener {
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+) : UseCaseCameraControl, UseCaseManager.RunningUseCasesChangeListener {
+ private var _requestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
+ _requestControl = value
value?.let {
val previousSignals =
synchronized(lock) {
@@ -61,15 +61,21 @@
invalidate() // Always apply the settings to the camera.
- synchronized(lock) { updateSignal }
- ?.let { newUpdateSignal ->
- previousSignals.forEach { newUpdateSignal.propagateTo(it) }
- } ?: run { previousSignals.forEach { it.complete(Unit) } }
+ synchronized(lock) { updateSignal }?.propagateToAll(previousSignals)
+ ?: run { for (signals in previousSignals) signals.complete(Unit) }
}
}
- override fun onRunningUseCasesChanged() {
- _useCaseCamera?.runningUseCases?.run { updateTemplate() }
+ override fun onRunningUseCasesChanged(runningUseCases: Set<UseCase>) {
+ if (runningUseCases.isNotEmpty()) {
+ runningUseCases.updateTemplate()
+ }
+ }
+
+ private fun Deferred<Unit>.propagateToAll(previousSignals: List<CompletableDeferred<Unit>>) {
+ for (previousSignal in previousSignals) {
+ propagateTo(previousSignal)
+ }
}
private val lock = Any()
@@ -165,7 +171,7 @@
parameters[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] = it
}
- useCaseCamera?.requestControl?.addParametersAsync(values = parameters)
+ requestControl?.setParametersAsync(values = parameters)
}
?.apply {
toCompletableDeferred().also { signal ->
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
index d09a401..7ffef5d 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
@@ -48,12 +48,12 @@
) : UseCaseCameraControl {
private val mutex = Mutex()
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+ private var _requestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
- _useCaseCamera?.let { submitPendingRequests() }
+ _requestControl = value
+ _requestControl?.let { submitPendingRequests() }
}
public data class CaptureRequest(
@@ -98,8 +98,9 @@
threads.sequentialScope.launch {
val request = CaptureRequest(captureConfigs, captureMode, flashType, signal)
- useCaseCamera?.let { camera ->
- submitRequest(request, camera).propagateResultOrEnqueueRequest(request, camera)
+ requestControl?.let { requestControl ->
+ submitRequest(request, requestControl)
+ .propagateResultOrEnqueueRequest(request, requestControl)
}
?: run {
// UseCaseCamera may become null by the time the coroutine is started
@@ -119,11 +120,11 @@
mutex.withLock {
while (pendingRequests.isNotEmpty()) {
pendingRequests.poll()?.let { request ->
- useCaseCamera?.let { camera ->
- submitRequest(request, camera)
+ requestControl?.let { requestControl ->
+ submitRequest(request, requestControl)
.propagateResultOrEnqueueRequest(
submittedRequest = request,
- requestCamera = camera
+ currentRequestControl = requestControl
)
}
}
@@ -134,9 +135,9 @@
private suspend fun submitRequest(
request: CaptureRequest,
- camera: UseCaseCamera
+ requestControl: UseCaseCameraRequestControl
): Deferred<List<Void?>> {
- debug { "StillCaptureRequestControl: submitting $request at $camera" }
+ debug { "StillCaptureRequestControl: submitting $request at $requestControl" }
val flashMode = flashControl.flashMode
// Prior to submitStillCaptures, wait until the pending flash mode session change is
// completed. On some devices, AE preCapture triggered in submitStillCaptures may not
@@ -145,7 +146,7 @@
flashControl.updateSignal.join()
debug { "StillCaptureRequestControl: Issuing single capture" }
val deferredList =
- camera.requestControl.issueSingleCaptureAsync(
+ requestControl.issueSingleCaptureAsync(
request.captureConfigs,
request.captureMode,
request.flashType,
@@ -164,7 +165,7 @@
private fun Deferred<List<Void?>>.propagateResultOrEnqueueRequest(
submittedRequest: CaptureRequest,
- requestCamera: UseCaseCamera
+ currentRequestControl: UseCaseCameraRequestControl
) {
invokeOnCompletion { cause: Throwable? ->
if (
@@ -174,13 +175,13 @@
threads.sequentialScope.launch {
var isPending = true
- useCaseCamera?.let { latestCamera ->
- if (requestCamera != latestCamera) {
+ requestControl?.let { latestRequestControl ->
+ if (currentRequestControl != latestRequestControl) {
// camera has already been changed, can retry immediately
- submitRequest(submittedRequest, latestCamera)
+ submitRequest(submittedRequest, latestRequestControl)
.propagateResultOrEnqueueRequest(
submittedRequest = submittedRequest,
- requestCamera = latestCamera
+ currentRequestControl = latestRequestControl
)
isPending = false
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
index 2fc55ab3..7d2deb3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
@@ -44,11 +44,11 @@
private val threads: UseCaseThreads,
) : UseCaseCameraControl {
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+ private var _requestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
+ _requestControl = value
setTorchAsync(
torch =
when (torchStateLiveData.value) {
@@ -92,7 +92,7 @@
return signal.createFailureResult(IllegalStateException("No flash unit"))
}
- useCaseCamera?.let { useCaseCamera ->
+ requestControl?.let { requestControl ->
_torchState.setLiveDataValue(torch)
threads.sequentialScope.launch {
@@ -108,7 +108,7 @@
_updateSignal = signal
// TODO(b/209757083), handle the failed result of the setTorchAsync().
- useCaseCamera.requestControl.setTorchAsync(torch).join()
+ requestControl.setTorchAsync(torch).join()
// Hold the internal AE mode to ON while the torch is turned ON.
state3AControl.preferredAeMode =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 6fbcac4..6b3020e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -17,12 +17,10 @@
package androidx.camera.camera2.pipe.integration.impl
import android.hardware.camera2.CameraDevice
-import android.hardware.camera2.CaptureRequest
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.GraphState.GraphStateError
import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
-import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.adapter.RequestProcessorAdapter
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
@@ -30,7 +28,6 @@
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
import androidx.camera.core.UseCase
import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.SessionProcessorSurface
import dagger.Binds
import dagger.Module
@@ -38,7 +35,6 @@
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -49,31 +45,9 @@
@JvmDefaultWithCompatibility
public interface UseCaseCamera {
- // UseCases
- public var runningUseCases: Set<UseCase>
-
- public var isPrimary: Boolean
-
- public interface RunningUseCasesChangeListener {
- /** Invoked when value of [UseCaseCamera.runningUseCases] has been changed. */
- public fun onRunningUseCasesChanged()
- }
-
// RequestControl of the UseCaseCamera
public val requestControl: UseCaseCameraRequestControl
- // Parameters
- public fun <T> setParameterAsync(
- key: CaptureRequest.Key<T>,
- value: T,
- priority: Config.OptionPriority = defaultOptionPriority,
- ): Deferred<Unit>
-
- public fun setParametersAsync(
- values: Map<CaptureRequest.Key<*>, Any>,
- priority: Config.OptionPriority = defaultOptionPriority,
- ): Deferred<Unit>
-
public fun setActiveResumeMode(enabled: Boolean) {}
// Lifecycle
@@ -87,7 +61,6 @@
public class UseCaseCameraImpl
@Inject
constructor(
- private val controls: java.util.Set<UseCaseCameraControl>,
private val useCaseGraphConfig: UseCaseGraphConfig,
private val useCases: java.util.ArrayList<UseCase>,
private val useCaseSurfaceManager: UseCaseSurfaceManager,
@@ -99,34 +72,6 @@
private val debugId = useCaseCameraIds.incrementAndGet()
private val closed = atomic(false)
- override var runningUseCases: Set<UseCase> = setOf<UseCase>()
- set(value) {
- field = value
-
- // Note: This may be called with the same set of values that was previously set. This
- // is used as a signal to indicate the properties of the UseCase may have changed.
- SessionConfigAdapter(value, isPrimary = isPrimary).getValidSessionConfigOrNull()?.let {
- requestControl.setSessionConfigAsync(it)
- }
- ?: run {
- debug { "Unable to reset the session due to invalid config" }
- requestControl.setSessionConfigAsync(
- SessionConfig.Builder().apply { setTemplateType(defaultTemplate) }.build()
- )
- }
-
- controls.forEach { control ->
- if (control is UseCaseCamera.RunningUseCasesChangeListener) {
- control.onRunningUseCasesChanged()
- }
- }
- }
-
- override var isPrimary: Boolean = true
- set(value) {
- field = value
- }
-
init {
debug { "Configured $this for $useCases" }
useCaseGraphConfig.apply { cameraStateAdapter.onGraphUpdated(graph) }
@@ -188,54 +133,10 @@
}
}
- override fun <T> setParameterAsync(
- key: CaptureRequest.Key<T>,
- value: T,
- priority: Config.OptionPriority,
- ): Deferred<Unit> =
- runIfNotClosed { setParametersAsync(mapOf(key to (value as Any)), priority) }
- ?: canceledResult
-
- override fun setParametersAsync(
- values: Map<CaptureRequest.Key<*>, Any>,
- priority: Config.OptionPriority,
- ): Deferred<Unit> =
- runIfNotClosed {
- requestControl.addParametersAsync(values = values, optionPriority = priority)
- } ?: canceledResult
-
override fun setActiveResumeMode(enabled: Boolean) {
useCaseGraphConfig.graph.isForeground = enabled
}
- private fun UseCaseCameraRequestControl.setSessionConfigAsync(
- sessionConfig: SessionConfig
- ): Deferred<Unit> =
- runIfNotClosed {
- setConfigAsync(
- type = UseCaseCameraRequestControl.Type.SESSION_CONFIG,
- config = sessionConfig.implementationOptions,
- tags = sessionConfig.repeatingCaptureConfig.tagBundle.toMap(),
- listeners =
- setOf(
- CameraCallbackMap.createFor(
- sessionConfig.repeatingCameraCaptureCallbacks,
- threads.backgroundExecutor
- )
- ),
- template = RequestTemplate(sessionConfig.repeatingCaptureConfig.templateType),
- streams =
- useCaseGraphConfig.getStreamIdsFromSurfaces(
- sessionConfig.repeatingCaptureConfig.surfaces
- ),
- sessionConfig = sessionConfig,
- )
- } ?: canceledResult
-
- private inline fun <R> runIfNotClosed(crossinline block: () -> R): R? {
- return if (!closed.value) block() else null
- }
-
override fun toString(): String = "UseCaseCamera-$debugId"
@Module
@@ -244,8 +145,4 @@
@Binds
public abstract fun provideUseCaseCamera(useCaseCamera: UseCaseCameraImpl): UseCaseCamera
}
-
- public companion object {
- private val canceledResult = CompletableDeferred<Unit>().apply { cancel() }
- }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraControl.kt
index e08f92a..df794b5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraControl.kt
@@ -17,7 +17,7 @@
package androidx.camera.camera2.pipe.integration.impl
public interface UseCaseCameraControl {
- public var useCaseCamera: UseCaseCamera?
+ public var requestControl: UseCaseCameraRequestControl?
public fun reset()
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 6aa775b..0731bda 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -50,66 +50,64 @@
internal const val DEFAULT_REQUEST_TEMPLATE = CameraDevice.TEMPLATE_PREVIEW
/**
- * The RequestControl provides a couple of APIs to update the config of the camera, it also stores
- * the (repeating) request parameters of the configured [UseCaseCamera]. Once the parameters are
- * updated, it will trigger the update to the [UseCaseCameraState].
+ * Provides methods to update the configuration and parameters of the camera. It also stores the
+ * repeating request parameters associated with the configured [UseCaseCamera]. When parameters are
+ * updated, it triggers changes in the [UseCaseCameraState].
*
- * The parameters can be stored for the different types of config respectively. Each type of the
- * config can be removed or overridden respectively without interfering with the other types.
+ * Parameters can be stored and managed according to different configuration types. Each type can be
+ * modified or overridden independently without affecting other types.
*/
@JvmDefaultWithCompatibility
public interface UseCaseCameraRequestControl {
- /** The declaration order is the ordering to merge. */
+ /** Defines the types or categories of configuration parameters. */
public enum class Type {
+ /** Parameters related to the overall session configuration. */
SESSION_CONFIG,
+ /** General, default parameters. */
DEFAULT,
- CAMERA2_CAMERA_CONTROL,
+ /** Parameters specifically for interoperability with Camera2. */
+ CAMERA2_CAMERA_CONTROL
}
- // Repeating parameters
+ // Repeating Request Parameters
/**
- * Append a new option to update the repeating request.
+ * Asynchronously sets or updates parameters for the repeating capture request.
*
- * This method will: (1) Stores [values], [tags] and [listeners] by [type] respectively. The new
- * inputs above will append to the values that store as the same [type], the existing values
- * that don't conflict with the new inputs will not be cleared. If the [type] isn't set, it will
- * treat the new inputs as the [Type.DEFAULT] (2) Update the repeating request by merging all
- * the [values], [tags] and [listeners] from all the defined types.
+ * New values will overwrite any existing parameters with the same key for the given [type]. If
+ * no [type] is specified, it defaults to [Type.DEFAULT].
*
- * @param type the type of the input parameter, the possible value could be one of the [Type]
- * @param values the new [CaptureRequest.Key] and value will be append to the repeating request
- * @param optionPriority is the priority option that would be used to determine whether the new
- * value can override the existing value or not. This is default to
- * [Config.OptionPriority.OPTIONAL]
- * @param tags the option tag that could be appended to the repeating request, its effect is
- * similar to the [CaptureRequest.Builder.setTag].
- * @param listeners to receive the capture results.
+ * @param type The category of parameters being set (default: [Type.DEFAULT]).
+ * @param values A map of [CaptureRequest.Key] to their new values.
+ * @param optionPriority The priority for resolving conflicts if the same parameter is set
+ * multiple times.
+ * @return A [Deferred] object representing the asynchronous operation.
*/
- public fun addParametersAsync(
+ public fun setParametersAsync(
type: Type = Type.DEFAULT,
values: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
optionPriority: Config.OptionPriority = defaultOptionPriority,
- tags: Map<String, Any> = emptyMap(),
- listeners: Set<Request.Listener> = emptySet()
): Deferred<Unit>
/**
- * Use a new [config] to update the repeating request.
+ * Asynchronously updates the repeating request with a new configuration.
*
- * This method will: (1) Stores [config], [tags] and [listeners] by [type] respectively. The new
- * inputs above will take place of the existing value of the [type]. (2) Update the repeating
- * request by merging all the [config], [tags] and [listeners] from all the defined types.
+ * This method replaces any existing configuration, tags, and listeners associated with the
+ * specified [type]. The repeating request is then rebuilt by merging all configurations, tags,
+ * and listeners from all defined types.
*
- * @param type the type of the input [config]
- * @param config the new config values will be used to update the repeating request.
- * @param tags the option tag that could be appended to the repeating request, its effect is
- * similar to the [CaptureRequest.Builder.setTag].
- * @param streams Specify a list of streams that would be updated. Leave the value in empty will
- * use the [streams] that is previously specified. The update can only be processed after
- * specifying 1 or more valid streams.
- * @param template The [RequestTemplate] will be used for the requests. Leave the value in empty
- * will use the [RequestTemplate] that is previously specified.
- * @param listeners to receive the capture results.
+ * @param type The category of the configuration being updated (e.g., SESSION_CONFIG, DEFAULT).
+ * @param config The new configuration values to apply. If null, the existing configuration for
+ * this type is cleared.
+ * @param tags Optional tags to append to the repeating request, similar to
+ * [CaptureRequest.Builder.setTag].
+ * @param streams The specific streams to update. If empty, all previously specified streams are
+ * updated. The update only proceeds if at least one valid stream is specified.
+ * @param template The [RequestTemplate] to use for the requests. If null, the previously
+ * specified template is used.
+ * @param listeners Listeners to receive capture results.
+ * @param sessionConfig Optional [SessionConfig] to update if applicable to the configuration
+ * type.
+ * @return A [Deferred] representing the asynchronous update operation.
*/
public fun setConfigAsync(
type: Type,
@@ -122,8 +120,29 @@
): Deferred<Unit>
// 3A
+ /**
+ * Asynchronously sets the torch (flashlight) state.
+ *
+ * @param enabled True to enable the torch, false to disable it.
+ * @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
+ */
public suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A>
+ /**
+ * Asynchronously starts a 3A (Auto Exposure, Auto Focus, Auto White Balance) operation with the
+ * specified regions and locking behaviors.
+ *
+ * @param aeRegions The auto-exposure regions.
+ * @param afRegions The auto-focus regions.
+ * @param awbRegions The auto-white balance regions.
+ * @param aeLockBehavior The behavior for locking auto-exposure.
+ * @param afLockBehavior The behavior for locking auto-focus.
+ * @param awbLockBehavior The behavior for locking auto-white balance.
+ * @param afTriggerStartAeMode The AE mode to use when triggering AF.
+ * @param timeLimitNs The time limit for the 3A operation in nanoseconds. Defaults to
+ * [CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS].
+ * @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
+ */
public suspend fun startFocusAndMeteringAsync(
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
@@ -135,9 +154,23 @@
timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A>
+ /**
+ * Asynchronously cancels any ongoing focus and metering operations.
+ *
+ * @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
+ */
public suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A>
// Capture
+ /**
+ * Asynchronously issues a single capture request.
+ *
+ * @param captureSequence A list of [CaptureConfig] objects defining the capture settings.
+ * @param captureMode The capture mode (from [ImageCapture.CaptureMode]).
+ * @param flashType The flash type (from [ImageCapture.FlashType]).
+ * @param flashMode The flash mode (from [ImageCapture.FlashMode]).
+ * @return A list of [Deferred] objects, one for each capture in the sequence.
+ */
public suspend fun issueSingleCaptureAsync(
captureSequence: List<CaptureConfig>,
@ImageCapture.CaptureMode captureMode: Int,
@@ -185,26 +218,18 @@
private val infoBundleMap = mutableMapOf<UseCaseCameraRequestControl.Type, InfoBundle>()
private val lock = Any()
- override fun addParametersAsync(
+ override fun setParametersAsync(
type: UseCaseCameraRequestControl.Type,
values: Map<CaptureRequest.Key<*>, Any>,
optionPriority: Config.OptionPriority,
- tags: Map<String, Any>,
- listeners: Set<Request.Listener>
): Deferred<Unit> =
runIfNotClosed {
synchronized(lock) {
debug { "[$type] Add request option: $values" }
infoBundleMap
.getOrPut(type) { InfoBundle() }
- .let {
- it.options.addAllCaptureRequestOptionsWithPriority(
- values,
- optionPriority
- )
- it.tags.putAll(tags)
- it.listeners.addAll(listeners)
- }
+ .options
+ .addAllCaptureRequestOptionsWithPriority(values, optionPriority)
infoBundleMap.merge()
}
.updateCameraStateAsync()
@@ -329,9 +354,9 @@
runIfNotClosed {
useGraphSessionOrFailed {
it.update3A(
- aeRegions = METERING_REGIONS_DEFAULT.asList(),
- afRegions = METERING_REGIONS_DEFAULT.asList(),
- awbRegions = METERING_REGIONS_DEFAULT.asList()
+ aeRegions = aeRegions ?: METERING_REGIONS_DEFAULT.asList(),
+ afRegions = afRegions ?: METERING_REGIONS_DEFAULT.asList(),
+ awbRegions = awbRegions ?: METERING_REGIONS_DEFAULT.asList()
)
}
} ?: submitFailedResult
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index d064344..54adf25 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -74,10 +74,11 @@
import androidx.camera.core.impl.stabilization.StabilizationMode
import javax.inject.Inject
import javax.inject.Provider
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.runBlocking
+import org.jetbrains.annotations.TestOnly
/**
* This class keeps track of the currently attached and active [UseCase]'s for a specific camera. A
@@ -354,13 +355,57 @@
shouldAddRepeatingUseCase(runningUseCases) -> addRepeatingUseCase()
shouldRemoveRepeatingUseCase(runningUseCases) -> removeRepeatingUseCase()
else -> {
- camera?.isPrimary = isPrimary
- camera?.runningUseCases = runningUseCases
+ camera?.let {
+ it.updateRepeatingRequests(isPrimary, runningUseCases)
+ for (control in allControls) {
+ if (control is RunningUseCasesChangeListener) {
+ control.onRunningUseCasesChanged(runningUseCases)
+ }
+ }
+ }
}
}
}
- @OptIn(ExperimentalCoroutinesApi::class)
+ private fun UseCaseCamera.updateRepeatingRequests(
+ isPrimary: Boolean,
+ runningUseCases: Set<UseCase>
+ ) {
+ // Note: This may be called with the same set of values that was previously set. This
+ // is used as a signal to indicate the properties of the UseCase may have changed.
+ SessionConfigAdapter(runningUseCases, isPrimary = isPrimary)
+ .getValidSessionConfigOrNull()
+ ?.let { requestControl.setSessionConfigAsync(it) }
+ ?: run {
+ Log.debug { "Unable to reset the session due to invalid config" }
+ requestControl.setSessionConfigAsync(
+ SessionConfig.Builder().apply { setTemplateType(defaultTemplate) }.build()
+ )
+ }
+ }
+
+ private fun UseCaseCameraRequestControl.setSessionConfigAsync(
+ sessionConfig: SessionConfig
+ ): Deferred<Unit> =
+ setConfigAsync(
+ type = UseCaseCameraRequestControl.Type.SESSION_CONFIG,
+ config = sessionConfig.implementationOptions,
+ tags = sessionConfig.repeatingCaptureConfig.tagBundle.toMap(),
+ listeners =
+ setOf(
+ CameraCallbackMap.createFor(
+ sessionConfig.repeatingCameraCaptureCallbacks,
+ useCaseThreads.get().backgroundExecutor
+ )
+ ),
+ template = RequestTemplate(sessionConfig.repeatingCaptureConfig.templateType),
+ streams =
+ useCaseGraphConfig?.getStreamIdsFromSurfaces(
+ sessionConfig.repeatingCaptureConfig.surfaces
+ ),
+ sessionConfig = sessionConfig,
+ )
+
@GuardedBy("lock")
private fun refreshAttachedUseCases(newUseCases: Set<UseCase>) {
val useCases = newUseCases.toList()
@@ -392,7 +437,7 @@
// Update list of active useCases
if (useCases.isEmpty()) {
for (control in allControls) {
- control.useCaseCamera = null
+ control.requestControl = null
control.reset()
}
return
@@ -406,7 +451,7 @@
// resume UseCaseManager successfully
// - And/or, the UseCaseManager is ready to be resumed under concurrent camera settings.
for (control in allControls) {
- control.useCaseCamera = null
+ control.requestControl = null
}
}
@@ -503,7 +548,7 @@
.build()
for (control in allControls) {
- control.useCaseCamera = camera
+ control.requestControl = camera?.requestControl
}
camera?.setActiveResumeMode(activeResumeEnabled)
@@ -523,6 +568,13 @@
return attachedUseCases.intersect(activeUseCases)
}
+ @TestOnly
+ @VisibleForTesting
+ public fun getRunningUseCasesForTest(): Set<UseCase> =
+ synchronized(lock) {
+ return getRunningUseCases()
+ }
+
/**
* Adds or removes repeating use case if needed.
*
@@ -689,6 +741,25 @@
cameraControl.setZslDisabledByUserCaseConfig(disableZsl)
}
+ /**
+ * This interface defines a listener that is notified when the set of running UseCases changes.
+ *
+ * A "running" UseCase is one that is both attached and active, meaning it's bound to the
+ * lifecycle and ready to receive camera frames.
+ *
+ * Classes implementing this interface can take action when the active UseCase configuration
+ * changes.
+ */
+ public interface RunningUseCasesChangeListener {
+
+ /**
+ * Invoked when the set of running UseCases has been modified (added, removed, or updated).
+ *
+ * @param runningUseCases The updated set of UseCases that are currently running.
+ */
+ public fun onRunningUseCasesChanged(runningUseCases: Set<UseCase>)
+ }
+
public companion object {
internal data class UseCaseManagerConfig(
val useCases: List<UseCase>,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
index 4d4ad3a..2437ab8 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
@@ -78,11 +78,11 @@
maxZoomRatio = maxZoomRatio
)
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- get() = _useCaseCamera
+ private var _requestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ get() = _requestControl
set(value) {
- _useCaseCamera = value
+ _requestControl = value
applyZoomState(_zoomState.value ?: defaultZoomState, false)
}
@@ -157,7 +157,7 @@
threads.sequentialScope.launch(start = CoroutineStart.UNDISPATCHED) {
setZoomState(zoomState)
- useCaseCamera?.let {
+ _requestControl?.let {
zoomCompat.applyAsync(zoomState.zoomRatio, it).propagateTo(signal)
}
?: signal.completeExceptionally(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
index 4a53c52..63da8d3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
@@ -22,8 +22,8 @@
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.compat.Camera2CameraControlCompat
import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraControl
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.core.CameraControl
import androidx.camera.core.impl.CameraControlInternal
@@ -49,16 +49,16 @@
private constructor(
private val compat: Camera2CameraControlCompat,
private val threads: UseCaseThreads,
- @VisibleForTesting internal val requestListener: ComboRequestListener,
+ @get:VisibleForTesting internal val requestListener: ComboRequestListener,
) : UseCaseCameraControl {
- private var _useCaseCamera: UseCaseCamera? = null
- override var useCaseCamera: UseCaseCamera?
- @RestrictTo(RestrictTo.Scope.LIBRARY) get() = _useCaseCamera
+ private var _useCaseCameraRequestControl: UseCaseCameraRequestControl? = null
+ override var requestControl: UseCaseCameraRequestControl?
+ @RestrictTo(RestrictTo.Scope.LIBRARY) get() = _useCaseCameraRequestControl
@RestrictTo(RestrictTo.Scope.LIBRARY)
set(value) {
- _useCaseCamera = value
- _useCaseCamera?.also {
+ _useCaseCameraRequestControl = value
+ _useCaseCameraRequestControl?.also {
requestListener.removeListener(compat)
requestListener.addListener(compat, threads.sequentialExecutor)
compat.applyAsync(it, false)
@@ -147,7 +147,7 @@
private fun updateAsync(tag: String): ListenableFuture<Void?> =
Futures.nonCancellationPropagating(
threads.sequentialScope
- .async { compat.applyAsync(useCaseCamera).await() }
+ .async { compat.applyAsync(requestControl).await() }
.asListenableFuture(tag)
)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
index af16f8f..355bea9 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
@@ -37,7 +37,7 @@
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.createCameraInfoAdapter
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.useCaseThreads
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
import androidx.camera.camera2.pipe.testing.FakeCameraDevices
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
@@ -167,7 +167,7 @@
cameraInfoAdapter.zoomState.observeForever { currentZoomState = it }
// if useCaseCamera is null, zoom setting operation will be cancelled
- zoomControl.useCaseCamera = FakeUseCaseCamera()
+ zoomControl.requestControl = FakeUseCaseCameraRequestControl()
val expectedZoomState = ZoomValue(3.0f, 1.0f, 10.0f)
zoomControl.applyZoomState(expectedZoomState)[3, TimeUnit.SECONDS]
@@ -183,7 +183,7 @@
cameraInfoAdapter.zoomState.observeForever { currentZoomState = it }
// if useCaseCamera is null, zoom setting operation will be cancelled
- zoomControl.useCaseCamera = FakeUseCaseCamera()
+ zoomControl.requestControl = FakeUseCaseCameraRequestControl()
zoomControl.reset()
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 fce6cf6..60a1e30 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
@@ -40,7 +40,6 @@
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.FocusMeteringControl
import androidx.camera.camera2.pipe.integration.impl.State3AControl
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
@@ -73,10 +72,8 @@
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
@@ -158,6 +155,7 @@
private val cameraPropertiesMap = mutableMapOf<String, CameraProperties>()
private val fakeRequestControl = FakeUseCaseCameraRequestControl(testScope)
+ private val runningUseCases = mutableSetOf<UseCase>()
@Before
fun setUp() {
@@ -171,9 +169,9 @@
fun tearDown() {
// CoroutineScope#cancel can throw exception if the scope has no job left
try {
- fakeUseCaseCamera.runningUseCases.forEach {
- it.onStateDetached()
- it.onUnbind()
+ for (useCase in runningUseCases) {
+ useCase.onStateDetached()
+ useCase.onUnbind()
}
// fakeUseCaseThreads may still be using Main dispatcher which sometimes
// causes Dispatchers.resetMain() to throw an exception:
@@ -666,8 +664,7 @@
CAMERA_ID_0,
useCases = setOf(createPreview(Size(1920, 1080))),
)
- fakeUseCaseCamera.runningUseCases = emptySet()
- focusMeteringControl.onRunningUseCasesChanged()
+ focusMeteringControl.onRunningUseCasesChanged(emptySet())
startFocusMeteringAndAwait(FocusMeteringAction.Builder(point1).build())
@@ -1545,46 +1542,15 @@
focusMeteringResultCallback.await()
}
- private val fakeUseCaseCamera =
- object : UseCaseCamera {
- override var runningUseCases = setOf<UseCase>()
-
- override val requestControl: UseCaseCameraRequestControl
- get() = fakeRequestControl
-
- override var isPrimary: Boolean = true
- set(value) {
- field = value
- }
-
- override fun <T> setParameterAsync(
- key: CaptureRequest.Key<T>,
- value: T,
- priority: androidx.camera.core.impl.Config.OptionPriority
- ): Deferred<Unit> {
- TODO("Not yet implemented")
- }
-
- override fun setParametersAsync(
- values: Map<CaptureRequest.Key<*>, Any>,
- priority: androidx.camera.core.impl.Config.OptionPriority
- ): Deferred<Unit> {
- TODO("Not yet implemented")
- }
-
- override fun close(): Job {
- TODO("Not yet implemented")
- }
- }
-
private fun initFocusMeteringControl(
cameraId: String,
useCases: Set<UseCase> = emptySet(),
useCaseThreads: UseCaseThreads = fakeUseCaseThreads,
state3AControl: State3AControl = createState3AControl(cameraId),
zoomCompat: ZoomCompat = FakeZoomCompat()
- ) =
- FocusMeteringControl(
+ ): FocusMeteringControl {
+ runningUseCases.addAll(useCases)
+ return FocusMeteringControl(
cameraPropertiesMap[cameraId]!!,
MeteringRegionCorrection.Bindings.provideMeteringRegionCorrection(
CameraQuirks(
@@ -1603,10 +1569,10 @@
zoomCompat
)
.apply {
- fakeUseCaseCamera.runningUseCases = useCases
- useCaseCamera = fakeUseCaseCamera
- onRunningUseCasesChanged()
+ requestControl = fakeRequestControl
+ onRunningUseCasesChanged(useCases)
}
+ }
private fun initCameraProperties(
cameraIdStr: String,
@@ -1738,8 +1704,8 @@
private fun createState3AControl(
cameraId: String = CAMERA_ID_0,
properties: CameraProperties = cameraPropertiesMap[cameraId]!!,
- useCaseCamera: UseCaseCamera = fakeUseCaseCamera,
- ) = FakeState3AControlCreator.createState3AControl(properties, useCaseCamera)
+ requestControl: UseCaseCameraRequestControl = fakeRequestControl,
+ ) = FakeState3AControlCreator.createState3AControl(properties, requestControl)
private fun FocusMeteringControl.startFocusAndMeteringAndAdvanceTestScope(
testScope: TestScope,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/Lock3ABehaviorWhenCaptureImageTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/Lock3ABehaviorWhenCaptureImageTest.kt
new file mode 100644
index 0000000..206bca8
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/Lock3ABehaviorWhenCaptureImageTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import androidx.camera.camera2.pipe.Lock3ABehavior
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+class Lock3ABehaviorWhenCaptureImageTest {
+
+ @Test
+ fun getLock3ABehaviors_noCustomBehaviors_returnsDefaults() {
+ val lock3ABehavior = Lock3ABehaviorWhenCaptureImage()
+ val defaultAeBehavior = null
+ val defaultAfBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+ val defaultAwbBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+
+ val (ae, af, awb) =
+ lock3ABehavior.getLock3ABehaviors(
+ defaultAeBehavior,
+ defaultAfBehavior,
+ defaultAwbBehavior
+ )
+
+ assertThat(ae).isEqualTo(defaultAeBehavior)
+ assertThat(af).isEqualTo(defaultAfBehavior)
+ assertThat(awb).isEqualTo(defaultAwbBehavior)
+ }
+
+ @Test
+ fun getLock3ABehaviors_withCustomBehaviors_returnsCustom() {
+ val customAeBehavior = null
+ val customAfBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+ val customAwbBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+ val lock3ABehavior =
+ Lock3ABehaviorWhenCaptureImage(
+ hasAeLockBehavior = true,
+ aeLockBehavior = customAeBehavior,
+ hasAfLockBehavior = true,
+ afLockBehavior = customAfBehavior,
+ hasAwbLockBehavior = true,
+ awbLockBehavior = customAwbBehavior
+ )
+
+ val (ae, af, awb) = lock3ABehavior.getLock3ABehaviors() // No defaults needed
+
+ assertThat(ae).isEqualTo(customAeBehavior)
+ assertThat(af).isEqualTo(customAfBehavior)
+ assertThat(awb).isEqualTo(customAwbBehavior)
+ }
+
+ @Test
+ fun getLock3ABehaviors_mixedBehaviors_returnsCorrectly() {
+ val customAfBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+ val lock3ABehavior =
+ Lock3ABehaviorWhenCaptureImage(
+ hasAfLockBehavior = true,
+ afLockBehavior = customAfBehavior
+ )
+ val defaultAeBehavior = null
+ val defaultAwbBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+
+ val (ae, af, awb) =
+ lock3ABehavior.getLock3ABehaviors(
+ defaultAeBehavior,
+ defaultAwbBehavior = defaultAwbBehavior
+ )
+
+ assertThat(ae).isEqualTo(defaultAeBehavior) // Default used
+ assertThat(af).isEqualTo(customAfBehavior) // Custom used
+ assertThat(awb).isEqualTo(defaultAwbBehavior) // Default used
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index e23b168..f968130 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -48,11 +48,15 @@
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
import androidx.camera.camera2.pipe.integration.compat.workaround.CapturePipelineTorchCorrection
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage.Companion.doNotLockAe3ABehavior
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage.Companion.noCustomizedLock3ABehavior
import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpTemplateParamsOverride
import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseFlashModeTorchFor3aUpdate
import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseTorchAsFlash
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlash
import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlashImpl
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
@@ -60,7 +64,6 @@
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraph
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
@@ -154,6 +157,9 @@
var waitForAwbAtLock3AForCapture: Boolean = false
var cancelAfAtUnlock3AForCapture: Boolean = false
+ var aeLockBehavior: Lock3ABehavior? = null
+ var afLockBehavior: Lock3ABehavior? = null
+ var awbLockBehavior: Lock3ABehavior? = null
override suspend fun lock3A(
aeMode: AeMode?,
@@ -172,6 +178,9 @@
convergedTimeLimitNs: Long,
lockedTimeLimitNs: Long
): Deferred<Result3A> {
+ this.aeLockBehavior = aeLockBehavior
+ this.afLockBehavior = afLockBehavior
+ this.awbLockBehavior = awbLockBehavior
lock3ASemaphore.release()
return CompletableDeferred(Result3A(Result3A.Status.OK))
}
@@ -309,8 +318,6 @@
@Before
fun setUp() {
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
-
state3AControl =
State3AControl(
fakeCameraProperties,
@@ -328,7 +335,7 @@
)
),
)
- .apply { useCaseCamera = fakeUseCaseCamera }
+ .apply { requestControl = fakeRequestControl }
torchControl =
TorchControl(
@@ -337,7 +344,7 @@
fakeUseCaseThreads,
)
.also {
- it.useCaseCamera = fakeUseCaseCamera
+ it.requestControl = fakeRequestControl
// Ensure the control is updated after the UseCaseCamera been set.
assertThat(fakeRequestControl.setTorchSemaphore.tryAcquire(testScope)).isTrue()
@@ -362,19 +369,7 @@
templateParamsOverride = NoOpTemplateParamsOverride,
)
- capturePipeline =
- CapturePipelineImpl(
- configAdapter = fakeCaptureConfigAdapter,
- cameraProperties = fakeCameraProperties,
- requestListener = comboRequestListener,
- threads = fakeUseCaseThreads,
- torchControl = torchControl,
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- useCaseCameraState = fakeUseCaseCameraState,
- useTorchAsFlash = NotUseTorchAsFlash,
- sessionProcessorManager = null,
- flashControl = flashControl,
- )
+ capturePipeline = createCapturePipeline()
}
@After
@@ -478,19 +473,7 @@
private suspend fun TestScope.withTorchAsFlashQuirk_shouldOpenTorch(imageCaptureMode: Int) {
// Arrange.
- capturePipeline =
- CapturePipelineImpl(
- configAdapter = fakeCaptureConfigAdapter,
- cameraProperties = fakeCameraProperties,
- requestListener = comboRequestListener,
- threads = fakeUseCaseThreads,
- torchControl = torchControl,
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- useCaseCameraState = fakeUseCaseCameraState,
- useTorchAsFlash = UseTorchAsFlashImpl,
- sessionProcessorManager = null,
- flashControl = flashControl,
- )
+ capturePipeline = createCapturePipeline(useTorchAsFlash = UseTorchAsFlashImpl)
val requestList = mutableListOf<Request>()
fakeCameraGraphSession.requestHandler = { requests -> requestList.addAll(requests) }
@@ -605,22 +588,69 @@
@Test
fun miniLatency_flashRequired_withFlashTypeTorch_shouldLock3A(): Unit = runTest {
withFlashTypeTorch_shouldLock3A(
+ capturePipeline,
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
- ImageCapture.FLASH_MODE_ON
+ ImageCapture.FLASH_MODE_ON,
+ expectedLock3ABehaviors =
+ Triple(
+ Lock3ABehavior.AFTER_CURRENT_SCAN,
+ Lock3ABehavior.AFTER_CURRENT_SCAN,
+ Lock3ABehavior.AFTER_CURRENT_SCAN
+ )
)
}
@Test
+ fun miniLatency_flashRequired_withFlashTypeTorch_doNotLockAe3ABehavior_shouldLock3A(): Unit =
+ runTest {
+ val capturePipeline =
+ createCapturePipeline(lock3ABehaviorWhenCaptureImage = doNotLockAe3ABehavior)
+ withFlashTypeTorch_shouldLock3A(
+ capturePipeline,
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_MODE_ON,
+ expectedLock3ABehaviors =
+ Triple(
+ null,
+ Lock3ABehavior.AFTER_CURRENT_SCAN,
+ Lock3ABehavior.AFTER_CURRENT_SCAN
+ )
+ )
+ }
+
+ @Test
fun maxQuality_withFlashTypeTorch_shouldLock3A(): Unit = runTest {
withFlashTypeTorch_shouldLock3A(
+ capturePipeline,
ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
- ImageCapture.FLASH_MODE_OFF
+ ImageCapture.FLASH_MODE_OFF,
+ expectedLock3ABehaviors =
+ Triple(
+ Lock3ABehavior.AFTER_CURRENT_SCAN,
+ Lock3ABehavior.AFTER_CURRENT_SCAN,
+ Lock3ABehavior.AFTER_CURRENT_SCAN
+ )
+ )
+ }
+
+ @Test
+ fun maxQuality_withFlashTypeTorch_doNotLockAe3ABehavior_shouldLock3A(): Unit = runTest {
+ val capturePipeline =
+ createCapturePipeline(lock3ABehaviorWhenCaptureImage = doNotLockAe3ABehavior)
+ withFlashTypeTorch_shouldLock3A(
+ capturePipeline,
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+ ImageCapture.FLASH_MODE_OFF,
+ expectedLock3ABehaviors =
+ Triple(null, Lock3ABehavior.AFTER_CURRENT_SCAN, Lock3ABehavior.AFTER_CURRENT_SCAN)
)
}
private suspend fun TestScope.withFlashTypeTorch_shouldLock3A(
+ capturePipeline: CapturePipeline,
imageCaptureMode: Int,
- flashMode: Int
+ flashMode: Int,
+ expectedLock3ABehaviors: Triple<Lock3ABehavior?, Lock3ABehavior?, Lock3ABehavior?>,
) {
// Arrange.
val requestList = mutableListOf<Request>()
@@ -639,6 +669,10 @@
// Assert 1, should call lock3A, but not call unlock3A (before capturing is finished).
assertThat(fakeCameraGraphSession.lock3ASemaphore.tryAcquire(this)).isTrue()
assertThat(fakeCameraGraphSession.unlock3ASemaphore.tryAcquire(this)).isFalse()
+ // Ensure correct Lock3ABehaviors are set.
+ assertThat(fakeCameraGraphSession.aeLockBehavior).isEqualTo(expectedLock3ABehaviors.first)
+ assertThat(fakeCameraGraphSession.afLockBehavior).isEqualTo(expectedLock3ABehaviors.second)
+ assertThat(fakeCameraGraphSession.awbLockBehavior).isEqualTo(expectedLock3ABehaviors.third)
// Complete the capture request.
assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue()
@@ -1224,6 +1258,24 @@
assertThat(screenFlash.awaitClear(3000)).isTrue()
}
+ private fun createCapturePipeline(
+ useTorchAsFlash: UseTorchAsFlash = NotUseTorchAsFlash,
+ lock3ABehaviorWhenCaptureImage: Lock3ABehaviorWhenCaptureImage = noCustomizedLock3ABehavior
+ ) =
+ CapturePipelineImpl(
+ configAdapter = fakeCaptureConfigAdapter,
+ cameraProperties = fakeCameraProperties,
+ requestListener = comboRequestListener,
+ threads = fakeUseCaseThreads,
+ torchControl = torchControl,
+ useCaseGraphConfig = fakeUseCaseGraphConfig,
+ useCaseCameraState = fakeUseCaseCameraState,
+ useTorchAsFlash = useTorchAsFlash,
+ lock3ABehaviorWhenCaptureImage = lock3ABehaviorWhenCaptureImage,
+ sessionProcessorManager = null,
+ flashControl = flashControl,
+ )
+
// TODO(wenhungteng@): Porting overrideAeModeForStillCapture_quirkAbsent_notOverride,
// overrideAeModeForStillCapture_aePrecaptureStarted_override,
// overrideAeModeForStillCapture_aePrecaptureFinish_notOverride,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/EvCompControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/EvCompControlTest.kt
index 4567d6b..2e84d00 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/EvCompControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/EvCompControlTest.kt
@@ -27,7 +27,7 @@
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.compat.EvCompImpl
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
@@ -80,7 +80,7 @@
EvCompControl(
EvCompImpl(FakeCameraProperties(metadata), fakeUseCaseThreads, comboRequestListener)
)
- exposureControl.useCaseCamera = FakeUseCaseCamera()
+ exposureControl.requestControl = FakeUseCaseCameraRequestControl()
}
@Test
@@ -112,7 +112,7 @@
val deferred = exposureControl.updateAsync(1)
// Act. Simulate control inactive by set useCaseCamera to null & call reset().
- exposureControl.useCaseCamera = null
+ exposureControl.requestControl = null
exposureControl.reset()
// Assert. The exposure control has been set to inactive. It should throw an exception.
@@ -136,7 +136,7 @@
comboRequestListener
)
exposureControl = EvCompControl(evCompCompat)
- exposureControl.useCaseCamera = FakeUseCaseCamera()
+ exposureControl.requestControl = FakeUseCaseCameraRequestControl()
// Act.
val deferred = exposureControl.updateAsync(1)
@@ -151,7 +151,7 @@
val deferred = exposureControl.updateAsync(targetEv)
// Act. Simulate the UseCaseCamera is recreated.
- exposureControl.useCaseCamera = FakeUseCaseCamera()
+ exposureControl.requestControl = FakeUseCaseCameraRequestControl()
comboRequestListener.simulateAeConverge(exposureValue = targetEv)
// Assert. The setEV task should be completed.
@@ -164,7 +164,7 @@
val deferred = exposureControl.updateAsync(1)
// Act. Simulate the UseCaseCamera is recreated,
- exposureControl.useCaseCamera = FakeUseCaseCamera()
+ exposureControl.requestControl = FakeUseCaseCameraRequestControl()
// Act. Submits a new EV value.
val deferred2 = exposureControl.updateAsync(targetEv)
comboRequestListener.simulateAeConverge(exposureValue = targetEv)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
index 61efe36..ba24b3c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
@@ -29,7 +29,6 @@
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.camera2.pipe.integration.compat.workaround.UseFlashModeTorchFor3aUpdateImpl
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.core.CameraControl
@@ -82,7 +81,6 @@
)
}
private val fakeRequestControl = FakeUseCaseCameraRequestControl()
- private val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
private val aeFpsRange =
AeFpsRange(
CameraQuirks(
@@ -139,11 +137,11 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera }
+ .apply { requestControl = fakeRequestControl }
torchControl =
TorchControl(cameraProperties, state3AControl, fakeUseCaseThreads).apply {
- useCaseCamera = fakeUseCaseCamera
+ requestControl = fakeRequestControl
}
flashControl =
@@ -159,13 +157,12 @@
NotUseFlashModeTorchFor3aUpdate
},
)
- flashControl.useCaseCamera = fakeUseCaseCamera
+ flashControl.requestControl = fakeRequestControl
flashControl.setScreenFlash(screenFlash)
}
@Test
fun setFlash_whenInactive(): Unit = runBlocking {
- val fakeUseCaseCamera = FakeUseCaseCamera()
val fakeCameraProperties = FakeCameraProperties()
val flashControl =
@@ -176,7 +173,7 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeRequestControl },
fakeUseCaseThreads,
TorchControl(fakeCameraProperties, state3AControl, fakeUseCaseThreads),
NotUseFlashModeTorchFor3aUpdate
@@ -267,7 +264,7 @@
// Act. call reset & clear the UseCaseCamera.
flashControl.setFlashAsync(ImageCapture.FLASH_MODE_ON)
flashControl.reset()
- flashControl.useCaseCamera = null
+ flashControl.requestControl = null
assertThrows<CameraControl.OperationCanceledException> { deferred.awaitWithTimeout() }
}
@@ -280,11 +277,10 @@
val deferred = flashControl.setFlashAsync(ImageCapture.FLASH_MODE_ON)
val fakeRequestControl =
FakeUseCaseCameraRequestControl().apply { addParameterResult = CompletableDeferred() }
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
// Act. Simulate the UseCaseCamera is recreated.
- flashControl.useCaseCamera = fakeUseCaseCamera
- state3AControl.useCaseCamera = fakeUseCaseCamera
+ flashControl.requestControl = fakeRequestControl
+ state3AControl.requestControl = fakeRequestControl
// Simulate setFlash is completed on the recreated UseCaseCamera
fakeRequestControl.addParameterResult.complete(Unit)
@@ -301,11 +297,10 @@
val deferred = flashControl.setFlashAsync(ImageCapture.FLASH_MODE_ON)
val fakeRequestControl =
FakeUseCaseCameraRequestControl().apply { addParameterResult = CompletableDeferred() }
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
// Act. Simulate the UseCaseCamera is recreated.
- flashControl.useCaseCamera = fakeUseCaseCamera
- state3AControl.useCaseCamera = fakeUseCaseCamera
+ flashControl.requestControl = fakeRequestControl
+ state3AControl.requestControl = fakeRequestControl
// Act. Submits a new Flash mode.
val deferred2 = flashControl.setFlashAsync(ImageCapture.FLASH_MODE_AUTO)
// Simulate setFlash is completed on the recreated UseCaseCamera
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index eb3c8e5..5698804 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -23,6 +23,7 @@
import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
+import androidx.camera.camera2.pipe.integration.compat.workaround.Lock3ABehaviorWhenCaptureImage.Companion.noCustomizedLock3ABehavior
import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpTemplateParamsOverride
import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseFlashModeTorchFor3aUpdate
import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseTorchAsFlash
@@ -32,7 +33,7 @@
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
import androidx.camera.camera2.pipe.integration.testing.FakeState3AControlCreator
import androidx.camera.camera2.pipe.integration.testing.FakeSurface
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
import androidx.camera.camera2.pipe.testing.FakeRequestFailure
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -83,26 +84,23 @@
private lateinit var fakeUseCaseCameraState: UseCaseCameraState
private val fakeState3AControl: State3AControl =
- FakeState3AControlCreator.createState3AControl(useCaseCamera = FakeUseCaseCamera())
-
- private lateinit var requestControl: UseCaseCameraRequestControl
-
- private lateinit var fakeUseCaseCamera: UseCaseCamera
-
- private val torchControl =
- TorchControl(fakeCameraProperties, fakeState3AControl, fakeUseCaseThreads)
-
- private val flashControl =
- FlashControl(
- fakeCameraProperties,
- fakeState3AControl,
- fakeUseCaseThreads,
- torchControl,
- NotUseFlashModeTorchFor3aUpdate,
+ FakeState3AControlCreator.createState3AControl(
+ requestControl = FakeUseCaseCameraRequestControl()
)
+ private lateinit var useCaseCameraRequestControl: UseCaseCameraRequestControl
+
private val stillCaptureRequestControl =
- StillCaptureRequestControl(flashControl, fakeUseCaseThreads)
+ StillCaptureRequestControl(
+ FlashControl(
+ fakeCameraProperties,
+ fakeState3AControl,
+ fakeUseCaseThreads,
+ TorchControl(fakeCameraProperties, fakeState3AControl, fakeUseCaseThreads),
+ NotUseFlashModeTorchFor3aUpdate,
+ ),
+ fakeUseCaseThreads
+ )
private val captureConfigList =
listOf(
@@ -112,7 +110,7 @@
@Before
fun setUp() {
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
}
@After
@@ -133,7 +131,7 @@
@Test
fun captureRequestsNotSubmitted_whenCameraIsNull() =
runTest(testDispatcher) {
- stillCaptureRequestControl.useCaseCamera = null
+ stillCaptureRequestControl.requestControl = null
stillCaptureRequestControl.issueCaptureRequests()
@@ -144,13 +142,13 @@
@Test
fun captureRequestsSubmittedAfterCameraIsAvailable_whenCameraIsNull() =
runTest(testDispatcher) {
- stillCaptureRequestControl.useCaseCamera = null
+ stillCaptureRequestControl.requestControl = null
stillCaptureRequestControl.issueCaptureRequests()
advanceUntilIdle()
// new camera is attached
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
// the previous request should be submitted in the new camera
advanceUntilIdle()
@@ -241,7 +239,7 @@
}
// new camera is attached
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
// the previous request should be submitted again in the new camera
advanceUntilIdle()
@@ -282,7 +280,7 @@
// making sure issuing is attempted before new camera is not attached
advanceUntilIdle()
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
// the previous request should be submitted in the new camera
advanceUntilIdle()
@@ -297,7 +295,7 @@
advanceUntilIdle()
// simulates previous camera closing and new camera being set
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
advanceUntilIdle()
assertThat(fakeCameraGraphSession.submittedRequests.size).isEqualTo(0)
@@ -317,13 +315,13 @@
}
// new camera is attached
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
// the previous request should be submitted again in the new camera
advanceUntilIdle()
// new camera is attached again
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
// since the previous request was successful, it should not be submitted again
assertThat(fakeCameraGraphSession.submittedRequests.size).isEqualTo(0)
@@ -333,7 +331,7 @@
fun noPendingRequestRemaining_whenReset() =
runTest(testDispatcher) {
// simulate adding to pending list
- stillCaptureRequestControl.useCaseCamera = null
+ stillCaptureRequestControl.requestControl = null
stillCaptureRequestControl.issueCaptureRequests()
stillCaptureRequestControl.issueCaptureRequests()
@@ -342,7 +340,7 @@
stillCaptureRequestControl.reset()
// new camera is attached
- stillCaptureRequestControl.setNewUseCaseCamera()
+ stillCaptureRequestControl.setNewRequestControl()
// if no new request submitted, it should imply all pending requests were cleared
advanceUntilIdle()
@@ -353,7 +351,7 @@
fun allPendingRequestsAreCancelled_whenReset() =
runTest(testDispatcher) {
// simulate adding to pending list
- stillCaptureRequestControl.useCaseCamera = null
+ stillCaptureRequestControl.requestControl = null
val requestFutures =
listOf(
stillCaptureRequestControl.issueCaptureRequests(),
@@ -426,7 +424,7 @@
)
val torchControl =
TorchControl(fakeCameraProperties, fakeState3AControl, fakeUseCaseThreads)
- requestControl =
+ useCaseCameraRequestControl =
UseCaseCameraRequestControlImpl(
capturePipeline =
CapturePipelineImpl(
@@ -438,6 +436,7 @@
useCaseGraphConfig = fakeUseCaseGraphConfig,
useCaseCameraState = fakeUseCaseCameraState,
useTorchAsFlash = NotUseTorchAsFlash,
+ lock3ABehaviorWhenCaptureImage = noCustomizedLock3ABehavior,
sessionProcessorManager = null,
flashControl =
FlashControl(
@@ -451,14 +450,10 @@
state = fakeUseCaseCameraState,
useCaseGraphConfig = fakeUseCaseGraphConfig,
)
- fakeUseCaseCamera =
- FakeUseCaseCamera(
- requestControl = requestControl,
- )
}
- private fun StillCaptureRequestControl.setNewUseCaseCamera() {
+ private fun StillCaptureRequestControl.setNewRequestControl() {
initUseCaseCameraScopeObjects()
- useCaseCamera = fakeUseCaseCamera
+ requestControl = useCaseCameraRequestControl
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
index 9e21126..1c201dad 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
@@ -26,7 +26,6 @@
import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.core.CameraControl
@@ -101,7 +100,7 @@
@Before
fun setUp() {
- val fakeUseCaseCamera = FakeUseCaseCamera()
+ val fakeUseCaseCameraRequestControl = FakeUseCaseCameraRequestControl()
val fakeCameraProperties = FakeCameraProperties(metadata)
torchControl =
TorchControl(
@@ -111,16 +110,16 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeUseCaseCameraRequestControl },
fakeUseCaseThreads,
)
- torchControl.useCaseCamera = fakeUseCaseCamera
+ torchControl.requestControl = fakeUseCaseCameraRequestControl
}
@Test
fun enableTorch_whenNoFlashUnit(): Unit = runBlocking {
assertThrows<IllegalStateException> {
- val fakeUseCaseCamera = FakeUseCaseCamera()
+ val fakeUseCaseCameraRequestControl = FakeUseCaseCameraRequestControl()
val fakeCameraProperties = FakeCameraProperties()
// Without a flash unit, this Job will complete immediately with a IllegalStateException
@@ -131,10 +130,10 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeUseCaseCameraRequestControl },
fakeUseCaseThreads,
)
- .also { it.useCaseCamera = fakeUseCaseCamera }
+ .also { it.requestControl = fakeUseCaseCameraRequestControl }
.setTorchAsync(true)
.await()
}
@@ -142,7 +141,7 @@
@Test
fun getTorchState_whenNoFlashUnit() {
- val fakeUseCaseCamera = FakeUseCaseCamera()
+ val fakeUseCaseCameraRequestControl = FakeUseCaseCameraRequestControl()
val fakeCameraProperties = FakeCameraProperties()
val torchState =
@@ -153,10 +152,10 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeUseCaseCameraRequestControl },
fakeUseCaseThreads,
)
- .also { it.useCaseCamera = fakeUseCaseCamera }
+ .also { it.requestControl = fakeUseCaseCameraRequestControl }
.torchStateLiveData
.value
@@ -166,7 +165,7 @@
@Test
fun enableTorch_whenInactive(): Unit = runBlocking {
assertThrows<CameraControl.OperationCanceledException> {
- val fakeUseCaseCamera = FakeUseCaseCamera()
+ val fakeUseCaseCameraRequestControl = FakeUseCaseCameraRequestControl()
val fakeCameraProperties = FakeCameraProperties(metadata)
TorchControl(
@@ -176,7 +175,7 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeUseCaseCameraRequestControl },
fakeUseCaseThreads,
)
.setTorchAsync(true)
@@ -186,7 +185,7 @@
@Test
fun getTorchState_whenInactive() {
- torchControl.useCaseCamera = null
+ torchControl.requestControl = null
Truth.assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.OFF)
}
@@ -199,7 +198,7 @@
@Test
fun enableTorch_torchStateOn_whenNoFlashUnit_butFlashUnitAvailabilityIsIgnored() = runBlocking {
- val fakeUseCaseCamera = FakeUseCaseCamera()
+ val fakeUseCaseCameraRequestControl = FakeUseCaseCameraRequestControl()
val fakeCameraProperties = FakeCameraProperties()
val torchControl =
@@ -210,11 +209,11 @@
NoOpAutoFlashAEModeDisabler,
aeFpsRange,
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeUseCaseCameraRequestControl },
fakeUseCaseThreads,
)
.also {
- it.useCaseCamera = fakeUseCaseCamera
+ it.requestControl = fakeUseCaseCameraRequestControl
it.setTorchAsync(torch = true, ignoreFlashUnitAvailability = true)
}
@@ -247,10 +246,7 @@
fun enableTorchTwice_cancelPreviousFuture(): Unit = runBlocking {
val deferred =
torchControl
- .also {
- it.useCaseCamera =
- FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
- }
+ .also { it.requestControl = neverCompleteTorchRequestControl }
.setTorchAsync(true)
torchControl.setTorchAsync(true)
@@ -262,10 +258,7 @@
fun setInActive_cancelPreviousFuture(): Unit = runBlocking {
val deferred =
torchControl
- .also {
- it.useCaseCamera =
- FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
- }
+ .also { it.requestControl = neverCompleteTorchRequestControl }
.setTorchAsync(true)
// reset() will be called after all the UseCases are detached.
@@ -314,18 +307,16 @@
@Test
fun useCaseCameraUpdated_setTorchResultShouldPropagate(): Unit = runBlocking {
// Arrange.
- torchControl.useCaseCamera =
- FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
+ torchControl.requestControl = neverCompleteTorchRequestControl
val deferred = torchControl.setTorchAsync(true)
val fakeRequestControl =
FakeUseCaseCameraRequestControl().apply {
setTorchResult = CompletableDeferred<Result3A>()
}
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
// Act. Simulate the UseCaseCamera is recreated.
- torchControl.useCaseCamera = fakeUseCaseCamera
+ torchControl.requestControl = fakeRequestControl
// Simulate setTorch is completed in the recreated UseCaseCamera
fakeRequestControl.setTorchResult.complete(Result3A(status = Result3A.Status.OK))
@@ -337,16 +328,14 @@
@Test
fun useCaseCameraUpdated_onlyCompleteLatestRequest(): Unit = runBlocking {
// Arrange.
- torchControl.useCaseCamera =
- FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
+ torchControl.requestControl = neverCompleteTorchRequestControl
val deferred = torchControl.setTorchAsync(true)
val fakeRequestControl =
FakeUseCaseCameraRequestControl().apply { setTorchResult = CompletableDeferred() }
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
// Act. Simulate the UseCaseCamera is recreated.
- torchControl.useCaseCamera = fakeUseCaseCamera
+ torchControl.requestControl = fakeRequestControl
// Act. Set Torch mode again.
val deferred2 = torchControl.setTorchAsync(false)
// Simulate setTorch is completed in the recreated UseCaseCamera
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
index a619b15..a6f3eb7 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
@@ -122,7 +122,7 @@
// Act
requestControl.setSessionConfigAsync(sessionConfigBuilder.build()).await()
requestControl
- .addParametersAsync(
+ .setParametersAsync(
values = mapOf(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION to 5)
)
.await()
@@ -188,7 +188,7 @@
type = UseCaseCameraRequestControl.Type.CAMERA2_CAMERA_CONTROL,
config = camera2CameraControlConfig
)
- requestControl.addParametersAsync(
+ requestControl.setParametersAsync(
values = mapOf(CaptureRequest.CONTROL_AE_MODE to CaptureRequest.CONTROL_AE_MODE_OFF)
)
requestControl.setSessionConfigAsync(sessionConfigBuilder.build()).await()
@@ -210,9 +210,6 @@
val testCamera2InteropTagKey = "testCamera2InteropTagKey"
val testCamera2InteropTagValue = "testCamera2InteropTagValue"
- val testTagKey = "testTagKey"
- val testTagValue = "testTagValue"
-
val sessionConfigBuilder =
SessionConfig.Builder().also { sessionConfigBuilder ->
sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
@@ -232,10 +229,6 @@
.build(),
tags = mapOf(testCamera2InteropTagKey to testCamera2InteropTagValue)
)
- requestControl.addParametersAsync(
- values = mapOf(CaptureRequest.CONTROL_AE_MODE to CaptureRequest.CONTROL_AE_MODE_OFF),
- tags = mapOf(testTagKey to testTagValue)
- )
requestControl.setSessionConfigAsync(sessionConfigBuilder.build()).await()
// Assert.
@@ -244,14 +237,12 @@
assertThat(tagBundle).isNotNull()
assertThat(tagBundle.getTag(testSessionTagKey)).isEqualTo(testSessionTagValue)
assertThat(tagBundle.getTag(testCamera2InteropTagKey)).isEqualTo(testCamera2InteropTagValue)
- assertThat(tagBundle.getTag(testTagKey)).isEqualTo(testTagValue)
}
@Test
fun testMergeListener(): Unit = runBlocking {
// Arrange
val testRequestListener = TestRequestListener()
- val testRequestListener1 = TestRequestListener()
val testCaptureCallback =
object : CameraCaptureCallback() {
val latch = CountDownLatch(1)
@@ -282,10 +273,6 @@
.build(),
listeners = setOf(testRequestListener)
)
- requestControl.addParametersAsync(
- values = mapOf(CaptureRequest.CONTROL_AE_MODE to CaptureRequest.CONTROL_AE_MODE_OFF),
- listeners = setOf(testRequestListener1)
- )
requestControl.setSessionConfigAsync(sessionConfigBuilder.build()).await()
// Invoke the onComplete on all the listeners.
@@ -295,7 +282,6 @@
// Assert. All the listeners should receive the onComplete signal.
assertThat(testRequestListener.latch.await(1, TimeUnit.SECONDS)).isTrue()
- assertThat(testRequestListener1.latch.await(1, TimeUnit.SECONDS)).isTrue()
assertThat(testCaptureCallback.latch.await(1, TimeUnit.SECONDS)).isTrue()
}
@@ -345,7 +331,7 @@
// Act
requestControl.setSessionConfigAsync(sessionConfigBuilder.build()).await()
requestControl
- .addParametersAsync(
+ .setParametersAsync(
values = mapOf(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION to 5)
)
.await()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
deleted file mode 100644
index c900d84..0000000
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.integration.impl
-
-import android.hardware.camera2.CameraDevice
-import android.os.Build
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
-import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
-import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
-import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpInactiveSurfaceCloser
-import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpTemplateParamsOverride
-import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
-import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraph
-import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
-import androidx.camera.camera2.pipe.integration.testing.FakeCapturePipeline
-import androidx.camera.camera2.pipe.integration.testing.FakeSurface
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.SessionConfig
-import androidx.camera.testing.impl.fakes.FakeUseCase
-import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.asExecutor
-import org.junit.After
-import org.junit.Assume.assumeTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-@RunWith(RobolectricCameraPipeTestRunner::class)
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-@DoNotInstrument
-class UseCaseCameraTest {
- private val surface = FakeSurface()
- private val surfaceToStreamMap: Map<DeferrableSurface, StreamId> = mapOf(surface to StreamId(0))
- private val useCaseThreads by lazy {
- val dispatcher = Dispatchers.Default
- val cameraScope = CoroutineScope(Job() + dispatcher)
-
- UseCaseThreads(cameraScope, dispatcher.asExecutor(), dispatcher)
- }
- private val fakeCameraProperties = FakeCameraProperties()
- private val fakeCameraGraph = FakeCameraGraph()
- private val fakeUseCaseGraphConfig =
- UseCaseGraphConfig(
- graph = fakeCameraGraph,
- surfaceToStreamMap = surfaceToStreamMap,
- cameraStateAdapter = CameraStateAdapter(),
- )
- private val fakeUseCaseCameraState =
- UseCaseCameraState(
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- threads = useCaseThreads,
- sessionProcessorManager = null,
- templateParamsOverride = NoOpTemplateParamsOverride,
- )
- private val requestControl =
- UseCaseCameraRequestControlImpl(
- capturePipeline = FakeCapturePipeline(),
- state = fakeUseCaseCameraState,
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- )
-
- @After
- fun tearDown() {
- surface.close()
- }
-
- @Test
- fun setInvalidSessionConfig_repeatingShouldStop() {
- // Arrange
- val fakeUseCase =
- FakeTestUseCase().apply {
- // Set a valid SessionConfig with Surface and template.
- setupSessionConfig(
- SessionConfig.Builder().apply {
- setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
- addSurface(surface)
- }
- )
- }
-
- @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
- val useCaseCamera =
- UseCaseCameraImpl(
- controls =
- emptySet<UseCaseCameraControl>() as java.util.Set<UseCaseCameraControl>,
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- useCases = arrayListOf(fakeUseCase),
- useCaseSurfaceManager =
- UseCaseSurfaceManager(
- useCaseThreads,
- CameraPipe(
- CameraPipe.Config(ApplicationProvider.getApplicationContext())
- ),
- NoOpInactiveSurfaceCloser,
- ),
- threads = useCaseThreads,
- sessionProcessorManager = null,
- sessionConfigAdapter = SessionConfigAdapter(listOf(fakeUseCase)),
- requestControl = requestControl,
- )
- .also { it.runningUseCases = setOf(fakeUseCase) }
- assumeTrue(
- fakeCameraGraph.fakeCameraGraphSession.repeatingRequestSemaphore.tryAcquire(
- 1,
- 3,
- TimeUnit.SECONDS
- )
- )
-
- // Act. Set an invalid SessionConfig which doesn't have the template.
- fakeUseCase.setupSessionConfig(SessionConfig.Builder().apply { addSurface(surface) })
-
- useCaseCamera.runningUseCases = setOf(fakeUseCase)
-
- // Assert. The stopRepeating() should be called.
- assertThat(
- fakeCameraGraph.fakeCameraGraphSession.stopRepeatingSemaphore.tryAcquire(
- 1,
- 3,
- TimeUnit.SECONDS
- )
- )
- .isTrue()
- }
-}
-
-private class FakeTestUseCase :
- FakeUseCase(FakeUseCaseConfig.Builder().setTargetName("UseCase").useCaseConfig) {
-
- fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
- updateSessionConfig(listOf(sessionConfigBuilder.build()))
- notifyActive()
- }
-}
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 0e24668..37d015b 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
@@ -50,7 +50,6 @@
import androidx.camera.camera2.pipe.integration.compat.workaround.TemplateParamsOverride
import androidx.camera.camera2.pipe.integration.compat.workaround.TemplateParamsQuirkOverride
import androidx.camera.camera2.pipe.integration.config.CameraConfig
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera.RunningUseCasesChangeListener
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.testing.FakeCamera2CameraControlCompat
@@ -124,7 +123,7 @@
useCaseManager.attach(listOf(useCase))
// Assert
- val enabledUseCases = useCaseManager.camera?.runningUseCases
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
assertThat(enabledUseCases).isEmpty()
}
@@ -140,7 +139,7 @@
useCaseManager.activate(useCase)
// Assert
- val enabledUseCases = useCaseManager.camera?.runningUseCases
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
assertThat(enabledUseCases).containsExactly(useCase)
}
@@ -163,7 +162,7 @@
// Assert
assertNotNull(useCaseManager.camera)
- assertThat(useCaseManager.camera!!.runningUseCases)
+ assertThat(useCaseManager.getRunningUseCasesForTest())
.containsExactly(previewUseCase, imageCaptureUseCase)
}
@@ -206,7 +205,7 @@
// Assert
assertNotNull(useCaseManager.camera)
// Check that the new set of running use cases is Preview, ImageCapture and ImageAnalysis.
- assertThat(useCaseManager.camera!!.runningUseCases)
+ assertThat(useCaseManager.getRunningUseCasesForTest())
.containsExactly(previewUseCase, imageCaptureUseCase, imageAnalysisUseCase)
}
@@ -224,7 +223,7 @@
useCaseManager.activate(imageCapture)
// Assert
- val enabledUseCases = useCaseManager.camera?.runningUseCases
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
assertThat(enabledUseCases).containsExactly(preview, imageCapture)
}
@@ -240,7 +239,8 @@
useCaseManager.activate(imageCapture)
// Assert
- val enabledUseCaseClasses = useCaseManager.camera?.runningUseCases?.map { it::class.java }
+ val enabledUseCaseClasses =
+ useCaseManager.getRunningUseCasesForTest().map { it::class.java }
assertThat(enabledUseCaseClasses)
.containsExactly(ImageCapture::class.java, MeteringRepeating::class.java)
}
@@ -260,7 +260,7 @@
useCaseManager.activate(preview)
// Assert
- val activeUseCases = useCaseManager.camera?.runningUseCases
+ val activeUseCases = useCaseManager.getRunningUseCasesForTest()
assertThat(activeUseCases).containsExactly(preview, imageCapture)
}
@@ -279,7 +279,8 @@
useCaseManager.detach(listOf(preview))
// Assert
- val enabledUseCaseClasses = useCaseManager.camera?.runningUseCases?.map { it::class.java }
+ val enabledUseCaseClasses =
+ useCaseManager.getRunningUseCasesForTest().map { it::class.java }
assertThat(enabledUseCaseClasses)
.containsExactly(ImageCapture::class.java, MeteringRepeating::class.java)
}
@@ -319,7 +320,7 @@
useCaseManager.deactivate(imageCapture)
// Assert
- val enabledUseCases = useCaseManager.camera?.runningUseCases
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
assertThat(enabledUseCases).isEmpty()
}
@@ -383,19 +384,18 @@
// Arrange
initializeUseCaseThreads(this)
val fakeControl =
- object : UseCaseCameraControl, RunningUseCasesChangeListener {
- var runningUseCases: Set<UseCase> = emptySet()
+ object : UseCaseCameraControl, UseCaseManager.RunningUseCasesChangeListener {
+ var runningUseCaseSet: Set<UseCase> = emptySet()
- @Suppress("UNUSED_PARAMETER")
- override var useCaseCamera: UseCaseCamera?
+ override var requestControl: UseCaseCameraRequestControl?
get() = TODO("Not yet implemented")
- set(value) {
- runningUseCases = value?.runningUseCases ?: emptySet()
- }
+ set(_) {}
override fun reset() {}
- override fun onRunningUseCasesChanged() {}
+ override fun onRunningUseCasesChanged(runningUseCases: Set<UseCase>) {
+ runningUseCaseSet = runningUseCases
+ }
}
val useCaseManager = createUseCaseManager(controls = setOf(fakeControl))
@@ -408,7 +408,7 @@
useCaseManager.attach(listOf(preview, useCase))
// Assert
- assertThat(fakeControl.runningUseCases).isEqualTo(setOf(preview, useCase))
+ assertThat(fakeControl.runningUseCaseSet).isEqualTo(setOf(preview, useCase))
}
@Test
@@ -428,7 +428,7 @@
advanceUntilIdle()
assertNotNull(useCaseManager.camera)
- assertThat(useCaseManager.camera!!.runningUseCases)
+ assertThat(useCaseManager.getRunningUseCasesForTest())
.containsExactly(previewUseCase, imageCaptureUseCase)
assertTrue(previewUseCase.cameraControlReady)
assertTrue(imageCaptureUseCase.cameraControlReady)
@@ -477,7 +477,7 @@
// Assert
assertNotNull(useCaseManager.camera)
// Check that the new set of running use cases is Preview, ImageCapture and ImageAnalysis.
- assertThat(useCaseManager.camera!!.runningUseCases)
+ assertThat(useCaseManager.getRunningUseCasesForTest())
.containsExactly(previewUseCase, imageCaptureUseCase, imageAnalysisUseCase)
// Despite only attaching the ImageAnalysis use case in the prior step. All not-yet-notified
// use cases should be notified that their camera controls are ready.
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
index aa18499..bed7cf8 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
@@ -18,7 +18,7 @@
import android.os.Build
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
+import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
import androidx.camera.core.CameraControl
import androidx.testutils.MainDispatcherRule
@@ -68,7 +68,7 @@
fun setUp() {
zoomControl =
ZoomControl(fakeUseCaseThreads, zoomCompat).apply {
- useCaseCamera = FakeUseCaseCamera()
+ requestControl = FakeUseCaseCameraRequestControl()
}
}
@@ -110,7 +110,7 @@
// Act. Simulate the UseCaseCamera is recreated before applying zoom.
zoomCompat.applyAsyncResult = CompletableDeferred() // incomplete deferred of new camera
- zoomControl.useCaseCamera = FakeUseCaseCamera()
+ zoomControl.requestControl = FakeUseCaseCameraRequestControl()
zoomCompat.applyAsyncResult.complete(Unit)
// Assert. The setZoomRatio task should be completed.
@@ -144,7 +144,7 @@
val result1 = zoomControl.setZoomRatio(3.0f)
// Act. Simulate the UseCaseCamera is recreated,
- zoomControl.useCaseCamera = FakeUseCaseCamera()
+ zoomControl.requestControl = FakeUseCaseCameraRequestControl()
// Act. Submit a new zoom ratio.
val result2 = zoomControl.setZoomRatio(2.0f)
zoomCompat.applyAsyncResult.complete(Unit)
@@ -162,7 +162,7 @@
zoomCompat.applyAsyncResult = CompletableDeferred() // incomplete deferred
val result = zoomControl.setZoomRatio(3.0f)
- zoomControl.useCaseCamera = null
+ zoomControl.requestControl = null
assertFutureFailedWithOperationCancellation(result)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControlTest.kt
index 009772e..371648c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControlTest.kt
@@ -28,7 +28,6 @@
import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.impl.toParameters
-import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
@@ -68,7 +67,6 @@
}
private val comboRequestListener = ComboRequestListener()
private val fakeRequestControl = FakeUseCaseCameraRequestControl()
- private val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
private val camera2CameraControlCompatImpl = Camera2CameraControlCompatImpl()
private lateinit var camera2CameraControl: Camera2CameraControl
@@ -80,7 +78,7 @@
threads = fakeUseCaseThreads,
requestListener = comboRequestListener,
)
- camera2CameraControl.useCaseCamera = fakeUseCaseCamera
+ camera2CameraControl.requestControl = fakeRequestControl
}
@Test
@@ -89,7 +87,6 @@
val completeDeferred = CompletableDeferred<Unit>()
val fakeRequestControl =
FakeUseCaseCameraRequestControl().apply { setConfigResult = completeDeferred }
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
val resultFuture =
camera2CameraControl.setCaptureRequestOptions(
@@ -102,7 +99,7 @@
)
// Act. Simulate the UseCaseCamera is recreated.
- camera2CameraControl.useCaseCamera = fakeUseCaseCamera
+ camera2CameraControl.requestControl = fakeRequestControl
// Simulate setRequestOption is completed in the recreated UseCaseCamera
completeDeferred.complete(Unit)
val requestsToCamera =
@@ -126,7 +123,6 @@
val completeDeferred = CompletableDeferred<Unit>()
val fakeRequestControl =
FakeUseCaseCameraRequestControl().apply { setConfigResult = completeDeferred }
- val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
val resultFuture =
camera2CameraControl.setCaptureRequestOptions(
@@ -139,7 +135,7 @@
)
// Act. Simulate the UseCaseCamera is recreated.
- camera2CameraControl.useCaseCamera = fakeUseCaseCamera
+ camera2CameraControl.requestControl = fakeRequestControl
// Act. Submit a new request option.
val resultFuture2 =
camera2CameraControl.setCaptureRequestOptions(
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCamera2CameraControlCompat.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCamera2CameraControlCompat.kt
index e4f612e..04e14cd 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCamera2CameraControlCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCamera2CameraControlCompat.kt
@@ -17,7 +17,7 @@
package androidx.camera.camera2.pipe.integration.testing
import androidx.camera.camera2.pipe.integration.compat.Camera2CameraControlCompat
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import kotlinx.coroutines.CompletableDeferred
@@ -41,7 +41,10 @@
// No-op
}
- override fun applyAsync(camera: UseCaseCamera?, cancelPreviousTask: Boolean): Deferred<Void?> {
+ override fun applyAsync(
+ requestControl: UseCaseCameraRequestControl?,
+ cancelPreviousTask: Boolean
+ ): Deferred<Void?> {
return CompletableDeferred<Void?>(null).apply { complete(null) }
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 1d251df..1b53a80 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -113,7 +113,7 @@
mapOf(CameraBackendId(cameraId.value) to listOf(cameraProperties.metadata))
)
): CameraInfoAdapter {
- val fakeUseCaseCamera = FakeUseCaseCamera()
+ val fakeRequestControl = FakeUseCaseCameraRequestControl()
val fakeStreamConfigurationMap =
StreamConfigurationMapCompat(
streamConfigurationMap,
@@ -130,7 +130,7 @@
NoOpAutoFlashAEModeDisabler,
AeFpsRange(fakeCameraQuirks),
)
- .apply { useCaseCamera = fakeUseCaseCamera }
+ .apply { requestControl = fakeRequestControl }
return CameraInfoAdapter(
cameraProperties,
CameraConfig(cameraId),
@@ -150,7 +150,7 @@
useCaseThreads,
FakeZoomCompat(),
)
- .apply { useCaseCamera = fakeUseCaseCamera },
+ .apply { requestControl = fakeRequestControl },
fakeCameraQuirks,
EncoderProfilesProviderAdapter(cameraId.value, fakeCameraQuirks.quirks),
fakeStreamConfigurationMap,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeEvCompCompat.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeEvCompCompat.kt
index 2e685e3..a82319a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeEvCompCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeEvCompCompat.kt
@@ -19,7 +19,7 @@
import android.util.Range
import android.util.Rational
import androidx.camera.camera2.pipe.integration.compat.EvCompCompat
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import kotlinx.coroutines.Deferred
class FakeEvCompCompat
@@ -34,7 +34,7 @@
override fun applyAsync(
evCompIndex: Int,
- camera: UseCaseCamera,
+ requestControl: UseCaseCameraRequestControl,
cancelPreviousTask: Boolean,
): Deferred<Int> {
TODO("Not yet implemented")
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt
index 9b3dbe0..65ce9f4 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeState3AControlCreator.kt
@@ -23,13 +23,13 @@
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.State3AControl
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import org.robolectric.shadows.StreamConfigurationMapBuilder
object FakeState3AControlCreator {
fun createState3AControl(
properties: CameraProperties = FakeCameraProperties(),
- useCaseCamera: UseCaseCamera = FakeUseCaseCamera(),
+ requestControl: UseCaseCameraRequestControl = FakeUseCaseCameraRequestControl(),
) =
State3AControl(
properties,
@@ -47,5 +47,5 @@
)
),
)
- .apply { this.useCaseCamera = useCaseCamera }
+ .apply { this.requestControl = requestControl }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 312045f..9da279b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -34,7 +34,6 @@
import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import androidx.camera.core.ImageCapture
-import androidx.camera.core.UseCase
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.DeferrableSurface
@@ -72,12 +71,12 @@
override fun build(): UseCaseCameraComponent {
buildInvocationCount++
- return FakeUseCaseCameraComponent(config.provideUseCaseList())
+ return FakeUseCaseCameraComponent()
}
}
-class FakeUseCaseCameraComponent(useCases: List<UseCase>) : UseCaseCameraComponent {
- private val fakeUseCaseCamera = FakeUseCaseCamera(useCases.toSet())
+class FakeUseCaseCameraComponent() : UseCaseCameraComponent {
+ private val fakeUseCaseCamera = FakeUseCaseCamera()
private val cameraGraph = FakeCameraGraph()
private val cameraStateAdapter = CameraStateAdapter()
@@ -102,12 +101,10 @@
var setConfigResult = CompletableDeferred(Unit)
var setTorchResult = CompletableDeferred(Result3A(status = Result3A.Status.OK))
- override fun addParametersAsync(
+ override fun setParametersAsync(
type: UseCaseCameraRequestControl.Type,
values: Map<CaptureRequest.Key<*>, Any>,
optionPriority: Config.OptionPriority,
- tags: Map<String, Any>,
- listeners: Set<Request.Listener>
): Deferred<Unit> {
addParameterCalls.add(values)
return addParameterResult
@@ -233,30 +230,9 @@
// TODO: Further implement the methods in this class as needed
class FakeUseCaseCamera(
- override var runningUseCases: Set<UseCase> = emptySet(),
override var requestControl: UseCaseCameraRequestControl = FakeUseCaseCameraRequestControl(),
) : UseCaseCamera {
- override var isPrimary: Boolean = true
- set(value) {
- field = value
- }
-
- override fun <T> setParameterAsync(
- key: CaptureRequest.Key<T>,
- value: T,
- priority: Config.OptionPriority
- ): Deferred<Unit> {
- return CompletableDeferred(Unit)
- }
-
- override fun setParametersAsync(
- values: Map<CaptureRequest.Key<*>, Any>,
- priority: Config.OptionPriority
- ): Deferred<Unit> {
- return CompletableDeferred(Unit)
- }
-
override fun close(): Job {
return CompletableDeferred(Unit)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
index ecb8a1f..b0b62df 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeZoomCompat.kt
@@ -18,7 +18,7 @@
import android.graphics.Rect
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
-import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
@@ -31,7 +31,10 @@
var zoomRatio = 0f
var applyAsyncResult = CompletableDeferred(Unit) // already completed deferred
- override fun applyAsync(zoomRatio: Float, camera: UseCaseCamera): Deferred<Unit> {
+ override fun applyAsync(
+ zoomRatio: Float,
+ requestControl: UseCaseCameraRequestControl
+ ): Deferred<Unit> {
return applyAsyncResult.also { result ->
result.invokeOnCompletion { this.zoomRatio = zoomRatio }
}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequence.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequence.kt
index 0d27ccf4..4372224 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequence.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequence.kt
@@ -16,12 +16,19 @@
package androidx.camera.camera2.pipe.testing
+import android.hardware.camera2.CaptureRequest
+import android.view.Surface
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CaptureSequence
import androidx.camera.camera2.pipe.CaptureSequences.invokeOnRequests
import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Metadata
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamId
+import kotlinx.atomicfu.atomic
/** A CaptureSequence used for testing interactions with a [FakeCaptureSequenceProcessor] */
public data class FakeCaptureSequence(
@@ -53,4 +60,104 @@
invokeOnRequests { requestMetadata, _, listener ->
listener.onRequestSequenceCompleted(requestMetadata, frameNumber)
}
+
+ public companion object {
+ private val requestNumbers = atomic(0L)
+ private val fakeCaptureSequenceListener = FakeCaptureSequenceListener()
+
+ public fun create(
+ cameraId: CameraId,
+ repeating: Boolean,
+ requests: List<Request>,
+ surfaceMap: Map<StreamId, Surface>,
+ defaultTemplate: RequestTemplate = RequestTemplate(1),
+ defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
+ requiredParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
+ listeners: List<Request.Listener> = emptyList(),
+ sequenceListener: CaptureSequence.CaptureSequenceListener = fakeCaptureSequenceListener
+ ): FakeCaptureSequence? {
+ if (surfaceMap.isEmpty()) {
+ println(
+ "No surfaces configured for $this! Cannot build CaptureSequence for $requests"
+ )
+ return null
+ }
+
+ val requestInfoMap = mutableMapOf<Request, RequestMetadata>()
+ val requestInfoList = mutableListOf<RequestMetadata>()
+ for (request in requests) {
+ val captureParameters = mutableMapOf<CaptureRequest.Key<*>, Any?>()
+ val metadataParameters = mutableMapOf<Metadata.Key<*>, Any?>()
+ for ((k, v) in defaultParameters) {
+ if (k != null) {
+ if (k is CaptureRequest.Key<*>) {
+ captureParameters[k] = v
+ } else if (k is Metadata.Key<*>) {
+ metadataParameters[k] = v
+ }
+ }
+ }
+ for ((k, v) in request.parameters) {
+ captureParameters[k] = v
+ }
+ for ((k, v) in request.extras) {
+ metadataParameters[k] = v
+ }
+ for ((k, v) in requiredParameters) {
+ if (k != null) {
+ if (k is CaptureRequest.Key<*>) {
+ captureParameters[k] = v
+ } else if (k is Metadata.Key<*>) {
+ metadataParameters[k] = v
+ }
+ }
+ }
+
+ val requestNumber = RequestNumber(requestNumbers.incrementAndGet())
+ val streamMap = mutableMapOf<StreamId, Surface>()
+ var hasSurface = false
+ for (stream in request.streams) {
+ val surface = surfaceMap[stream]
+ if (surface == null) {
+ println("Failed to find surface for $stream on $request")
+ continue
+ }
+ hasSurface = true
+ streamMap[stream] = surface
+ }
+
+ if (!hasSurface) {
+ println("No surfaces configured for $request! Cannot build CaptureSequence.")
+ return null
+ }
+
+ val requestMetadata =
+ FakeRequestMetadata(
+ request = request,
+ requestParameters = captureParameters,
+ metadata = metadataParameters,
+ template = request.template ?: defaultTemplate,
+ streams = streamMap,
+ repeating = repeating,
+ requestNumber = requestNumber
+ )
+ requestInfoList.add(requestMetadata)
+ requestInfoMap[request] = requestMetadata
+ }
+
+ // Copy maps / lists for tests.
+ return FakeCaptureSequence(
+ repeating = repeating,
+ cameraId = cameraId,
+ captureRequestList = requests.toList(),
+ captureMetadataList = requestInfoList,
+ requestMetadata = requestInfoMap,
+ defaultParameters = defaultParameters.toMap(),
+ requiredParameters = requiredParameters.toMap(),
+ listeners = listeners.toList(),
+ sequenceListener = sequenceListener,
+ sequenceNumber = -1 // Sequence number is not set until it has been submitted.
+ )
+ }
+ }
}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
index ff09344..5010c90 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
@@ -16,16 +16,12 @@
package androidx.camera.camera2.pipe.testing
-import android.hardware.camera2.CaptureRequest
import android.view.Surface
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CaptureSequence.CaptureSequenceListener
import androidx.camera.camera2.pipe.CaptureSequenceProcessor
-import androidx.camera.camera2.pipe.Metadata
import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.StreamId
import kotlinx.atomicfu.atomic
@@ -47,7 +43,6 @@
private val lock = Any()
private val sequenceIds = atomic(0)
private val eventChannel = Channel<Event>(Channel.UNLIMITED)
- private val requestCounter = atomic(0L)
@GuardedBy("lock") private var pendingSequence: CompletableDeferred<FakeCaptureSequence>? = null
@@ -80,12 +75,16 @@
listeners: List<Request.Listener>,
sequenceListener: CaptureSequenceListener
): FakeCaptureSequence? {
- return buildFakeCaptureSequence(
+ return FakeCaptureSequence.create(
+ cameraId = cameraId,
repeating = isRepeating,
- requests,
- defaultParameters,
- requiredParameters,
- listeners
+ requests = requests,
+ surfaceMap = surfaceMap,
+ defaultTemplate = defaultTemplate,
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ listeners = listeners,
+ sequenceListener = sequenceListener
)
}
@@ -139,7 +138,7 @@
requestSequence?.invokeOnSequenceAborted()
}
- override fun close() {
+ override suspend fun shutdown() {
synchronized(lock) {
rejectRequests = true
check(eventChannel.trySend(Event(close = true)).isSuccess)
@@ -172,93 +171,6 @@
}
}
- private fun buildFakeCaptureSequence(
- repeating: Boolean,
- requests: List<Request>,
- defaultParameters: Map<*, Any?>,
- requiredParameters: Map<*, Any?>,
- defaultListeners: List<Request.Listener>,
- ): FakeCaptureSequence? {
- val surfaceMap = surfaceMap
- if (surfaceMap.isEmpty()) {
- println("No surfaces configured for $this! Cannot build CaptureSequence for $requests")
- return null
- }
-
- val requestInfoMap = mutableMapOf<Request, RequestMetadata>()
- val requestInfoList = mutableListOf<RequestMetadata>()
- for (request in requests) {
- val captureParameters = mutableMapOf<CaptureRequest.Key<*>, Any?>()
- val metadataParameters = mutableMapOf<Metadata.Key<*>, Any?>()
- for ((k, v) in defaultParameters) {
- if (k != null) {
- if (k is CaptureRequest.Key<*>) {
- captureParameters[k] = v
- } else if (k is Metadata.Key<*>) {
- metadataParameters[k] = v
- }
- }
- }
- for ((k, v) in request.parameters) {
- captureParameters[k] = v
- }
- for ((k, v) in requiredParameters) {
- if (k != null) {
- if (k is CaptureRequest.Key<*>) {
- captureParameters[k] = v
- } else if (k is Metadata.Key<*>) {
- metadataParameters[k] = v
- }
- }
- }
-
- val requestNumber = RequestNumber(requestCounter.incrementAndGet())
- val streamMap = mutableMapOf<StreamId, Surface>()
- var hasSurface = false
- for (stream in request.streams) {
- val surface = surfaceMap[stream]
- if (surface == null) {
- println("Failed to find surface for $stream on $request")
- continue
- }
- hasSurface = true
- streamMap[stream] = surface
- }
-
- if (!hasSurface) {
- println("No surfaces configured for $request! Cannot build CaptureSequence.")
- return null
- }
-
- val requestMetadata =
- FakeRequestMetadata(
- request = request,
- requestParameters = captureParameters,
- metadata = metadataParameters,
- template = request.template ?: defaultTemplate,
- streams = streamMap,
- repeating = repeating,
- requestNumber = requestNumber
- )
- requestInfoList.add(requestMetadata)
- requestInfoMap[request] = requestMetadata
- }
-
- // Copy maps / lists for tests.
- return FakeCaptureSequence(
- repeating = repeating,
- cameraId = cameraId,
- captureRequestList = requests.toList(),
- captureMetadataList = requestInfoList,
- requestMetadata = requestInfoMap,
- defaultParameters = defaultParameters.toMap(),
- requiredParameters = requiredParameters.toMap(),
- listeners = defaultListeners.toList(),
- sequenceListener = FakeCaptureSequenceListener(),
- sequenceNumber = -1
- )
- }
-
/** TODO: It's probably better to model this as a sealed class. */
public data class Event(
val requestSequence: FakeCaptureSequence? = null,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
index 5b4721e..77c8444 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CaptureSequenceProcessor.kt
@@ -71,5 +71,5 @@
* Signal that this [CaptureSequenceProcessor] is no longer in use. Active requests may continue
* to be processed, and [abortCaptures] and [stopRepeating] may still be invoked.
*/
- public fun close()
+ public suspend fun shutdown()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index 7549fe4..17fb37e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -37,8 +37,6 @@
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.core.Log.MonitoredLogMessages.REPEATING_REQUEST_STARTED_TIMEOUT
-import androidx.camera.camera2.pipe.core.Log.rethrowExceptionAfterLogging
-import androidx.camera.camera2.pipe.core.Threading.runBlockingWithTimeout
import androidx.camera.camera2.pipe.core.Threads
import androidx.camera.camera2.pipe.graph.StreamGraphImpl
import androidx.camera.camera2.pipe.media.AndroidImageWriter
@@ -47,6 +45,8 @@
import javax.inject.Inject
import kotlin.reflect.KClass
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.withTimeout
internal interface Camera2CaptureSequenceProcessorFactory {
fun create(
@@ -181,14 +181,13 @@
// Finally, write required parameters to the request builder. This will override any
// value that has ben previously set.
//
- // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
- // from internal 3A state machine at last and provide a flag in the Request object
- // to
- // specify when the clients want to explicitly override some of the 3A parameters
- // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
- // freely and when that happens intercept those parameters from the request and keep
- // the
- // internal 3A state machine in sync.
+ // TODO(sushilnath@): Implement one of the two options
+ // (1) Apply the 3A parameters from internal 3A state machine at last and provide
+ // a flag in the Request object to specify when the clients want to explicitly
+ // override some of the 3A parameters directly. Add code to handle the flag.
+ // (2) Let clients override the 3A parameters freely and when that happens
+ // intercept those parameters from the request and keep the internal 3A state
+ // machine in sync.
requestBuilder.writeParameters(requiredParameters)
}
val requestNumber = nextRequestNumber()
@@ -284,9 +283,7 @@
override fun submit(captureSequence: Camera2CaptureSequence): Int? =
synchronized(lock) {
if (closed) {
- Log.warn {
- "Capture sequence processor closed. $captureSequence won't be submitted"
- }
+ Log.warn { "$this closed. $captureSequence won't be submitted" }
return null
}
val captureCallback = captureSequence as CameraCaptureSession.CaptureCallback
@@ -327,48 +324,52 @@
session.stopRepeating()
}
- override fun close() =
+ override suspend fun shutdown() {
+ val captureSequence: Camera2CaptureSequence?
synchronized(lock) {
if (closed) {
- return@synchronized
+ return
}
- // Close should not shut down
- Debug.trace("$this#close") {
- if (shouldWaitForRepeatingRequest) {
- lastSingleRepeatingRequestSequence?.let {
- Log.debug { "Waiting for the last repeating request sequence $it" }
- // On certain devices, the submitted repeating request sequence may not give
- // us
- // onCaptureStarted() or onCaptureSequenceAborted() [1]. Hence we wrap the
- // wait
- // under a timeout to prevent us from waiting forever.
- //
- // [1] b/307588161 - [ANR] at
- //
- // androidx.camera.camera2.pipe.compat.Camera2CaptureSequenceProcessor.close
- rethrowExceptionAfterLogging(
- "$this#close: $REPEATING_REQUEST_STARTED_TIMEOUT" +
- ", lastSingleRepeatingRequestSequence = $it"
- ) {
- runBlockingWithTimeout(
- threads.backgroundDispatcher,
- WAIT_FOR_REPEATING_TIMEOUT_MS
- ) {
- it.awaitStarted()
- }
- }
- }
- }
+ closed = true
+ captureSequence = lastSingleRepeatingRequestSequence
+ }
+
+ if (shouldWaitForRepeatingRequest && captureSequence != null) {
+ awaitRepeatingRequestStarted(captureSequence)
+ }
+
+ // Shutdown is responsible for releasing resources that are no longer in use.
+ Debug.trace("$this#close") {
+ synchronized(lock) {
imageWriter?.close()
session.inputSurface?.release()
- closed = true
}
}
+ }
override fun toString(): String {
return "Camera2CaptureSequenceProcessor-$debugId"
}
+ private suspend fun awaitRepeatingRequestStarted(captureSequence: Camera2CaptureSequence) {
+ Log.debug { "Waiting for the last repeating request sequence: $captureSequence" }
+ // On certain devices, the submitted repeating request sequence may not give
+ // us onCaptureStarted() or onCaptureSequenceAborted() [1]. Hence we wrap
+ // the wait under a timeout to prevent us from waiting forever.
+ //
+ // [1] b/307588161 - [ANR] at
+ // androidx.camera.camera2.pipe.compat.Camera2CaptureSequenceProcessor.close
+ try {
+ withTimeout(WAIT_FOR_REPEATING_TIMEOUT_MS) { captureSequence.awaitStarted() }
+ } catch (e: TimeoutCancellationException) {
+ Log.error {
+ "$this#close: $REPEATING_REQUEST_STARTED_TIMEOUT" +
+ ", lastSingleRepeatingRequestSequence = $captureSequence"
+ }
+ throw e
+ }
+ }
+
/**
* The [ImageWriterWrapper] is created once per capture session when the capture session is
* created, assuming it's a reprocessing session.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
index 0379996..15425cb 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
@@ -40,6 +40,7 @@
import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
import kotlin.reflect.KClass
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.runBlocking
public class ExternalCameraController(
private val graphId: CameraGraphId,
@@ -78,7 +79,9 @@
}
override fun close() {
- graphProcessor.close()
+ // TODO: ExternalRequestProcessor will be deprecated. This is a temporary patch to allow
+ // graphProcessor to have a suspending shutdown function.
+ runBlocking { graphProcessor.shutdown() }
}
override fun updateSurfaceMap(surfaceMap: Map<StreamId, Surface>) {
@@ -189,7 +192,7 @@
processor.stopRepeating()
}
- override fun close() {
+ override suspend fun shutdown() {
if (closed.compareAndSet(expect = false, update = true)) {
processor.close()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
index b805289..a01f102 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
@@ -44,7 +44,7 @@
* @param label A name of the code section to appear in the trace.
* @param block A block of code which is being traced.
*/
- public inline fun <T> trace(label: String, block: () -> T): T {
+ public inline fun <T> trace(label: String, crossinline block: () -> T): T {
try {
traceStart { label }
return block()
@@ -54,7 +54,7 @@
}
/** Wrap the specified [block] in a trace and timing calls. */
- internal inline fun <T> instrument(label: String, block: () -> T): T {
+ internal inline fun <T> instrument(label: String, crossinline block: () -> T): T {
val start = systemTimeSource.now()
try {
traceStart { label }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/ProcessingQueue.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/ProcessingQueue.kt
new file mode 100644
index 0000000..6eb5fe7
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/ProcessingQueue.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.core
+
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+/**
+ * ProcessingQueue handles the sequential aggregation and processing of a running list of elements.
+ * It is designed to iteratively invoke the provided [process] function with a [MutableList] of
+ * elements that have been aggregated from the previous iteration. [process] is guaranteed to be
+ * invoked sequentially. [process] is expected to remove items from the [MutableList] that are no
+ * longer needed (or that have been processed). Items that are not removed will be present in
+ * subsequent [process] invocations.
+ *
+ * A key design consideration for this class, and the reason it does not operate on generic flows,
+ * is the handling of unprocessed elements, which may need to be handled and/or closed. This is
+ * non-trivial for buffered flows. [onUnprocessedElements] will be invoked synchronously with all
+ * un-processed items exactly once if there is a non-zero number of unprocessed elements when the
+ * ProcessingQueue scope is closed.
+ *
+ * Example Usage:
+ * ```
+ * class MyClass(scope: CoroutineScope) {
+ * private val processingQueue = ProcessingQueue<Int>(
+ * onUnprocessedElements = ::onUnprocessedElements
+ * process = ::processInts
+ * ).processIn(scope)
+ *
+ * fun processAnInt(value: Int) {
+ * processingQueue.emitChecked(value)
+ * }
+ *
+ * private suspend fun processInts(items: MutableList<Int>) {
+ * val first = items.removeFirst()
+ * println("Processing: $first")
+ * }
+ *
+ * private fun onUnprocessedElements(items: List<Int>) {
+ * println("Releasing unprocessed items: items")
+ * }
+ * }
+ * ```
+ *
+ * This class is thread safe.
+ */
+internal class ProcessingQueue<T>(
+ val capacity: Int = Channel.UNLIMITED,
+ private val onUnprocessedElements: (List<T>) -> Unit = {},
+ private val process: suspend (MutableList<T>) -> Unit
+) {
+ private val started = atomic(false)
+ private val channel = Channel<T>(capacity = capacity, onUndeliveredElement = { queue.add(it) })
+ private val queue = ArrayDeque<T>()
+
+ /** Emit an element into the queue, suspending if the queue is at capacity. */
+ suspend fun emit(element: T) {
+ channel.send(element)
+ }
+
+ /** Emit an element into the queue, throwing an exception if it is closed or at capacity. */
+ fun emitChecked(element: T) {
+ val result = channel.trySend(element)
+ check(result.isSuccess) { "Failed to emit item to ProcessingQueue!: $result" }
+ }
+
+ /**
+ * Synchronously emit an element into the queue. Returns false if closed or if the queue is at
+ * capacity.
+ */
+ fun tryEmit(element: T): Boolean {
+ return channel.trySend(element).isSuccess
+ }
+
+ private suspend fun processingLoop() {
+ try {
+ // The core loop is:
+ // 1. Wait for a new item in the channel.
+ // 2. Add all items that can be immediately received from the channel into queue.
+ // 3. Process items (maybe suspend)
+ // 4. If the queue of items is the same, assume processing did nothing and jump to 1.
+ // 5. If the queue of items is different, assume processing did something and jump to 2.
+
+ while (true) {
+ // Suspend until we receive a element from the channel
+ val element = channel.receive()
+ queue.add(element)
+
+ while (queue.isNotEmpty()) {
+ // Buffer any additional elements from the inputChannel that may have been sent
+ // during the last call to process
+ var nextResult = channel.tryReceive()
+ while (nextResult.isSuccess) {
+ queue.add(nextResult.getOrThrow())
+ nextResult = channel.tryReceive()
+ }
+
+ // Emit the list of elements. This may suspend, and the consumer may modify the
+ // list, which will be updated and sent back on the next iteration.
+ val size = queue.size
+ process(queue)
+ if (size == queue.size) {
+ break
+ }
+ }
+ }
+ } catch (e: Throwable) {
+ releaseUnprocessedElements(e)
+ throw e
+ }
+ }
+
+ private fun releaseUnprocessedElements(cause: Throwable?) {
+ // If we reach here, it means the scope that was driving the processing loop has been
+ // cancelled. It means that the last call to `processor` has exited. The first time
+ // that channel.close() is called, the `onUndeliveredElement` handler will be invoked
+ // with the item that was pending for delivery. This, however, does not include *all*
+ // of the items, and we may need to iterate and handle the remaining items that may
+ // still be in the channel.
+ if (channel.close(cause)) {
+
+ // After closing the channel, there may be remaining items in the channel that
+ // were sent after the receiving scope was closed. Read these items out and send
+ // them to the onUnpressedElements handler.
+ var nextResult = channel.tryReceive()
+ while (nextResult.isSuccess) {
+ queue.add(nextResult.getOrThrow())
+ nextResult = channel.tryReceive()
+ }
+
+ // Synchronously invoke the onUnprocessedElements handler with the remaining items.
+ if (queue.isNotEmpty()) {
+ onUnprocessedElements(queue.toMutableList())
+ queue.clear()
+ }
+ }
+ }
+
+ internal companion object {
+ /** Launch the processing loop in the provided processing scope. */
+ fun <T> ProcessingQueue<T>.processIn(scope: CoroutineScope): ProcessingQueue<T> {
+ check(started.compareAndSet(expect = false, update = true)) {
+ "ProcessingQueue cannot be re-started!"
+ }
+
+ // Launch the processing loop in the provided scope.
+ val job = scope.launch { processingLoop() }
+
+ // If the scope is already cancelled, then `process` will never be invoked. To ensure
+ // items are released, attempt to close the channel and release any remaining items.
+ if (job.isCancelled) {
+ releaseUnprocessedElements(null)
+ }
+ return this
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index eaef6a4..c552afc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -67,7 +67,7 @@
override fun startRepeating(request: Request) {
check(!token.released) { "Cannot call startRepeating on $this after close." }
- graphProcessor.startRepeating(request)
+ graphProcessor.repeatingRequest = request
}
override fun abort() {
@@ -77,8 +77,7 @@
override fun stopRepeating() {
check(!token.released) { "Cannot call stopRepeating on $this after close." }
- graphProcessor.stopRepeating()
- controller3A.onStopRepeating()
+ graphProcessor.repeatingRequest = null
}
override fun close() {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CaptureLimiter.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CaptureLimiter.kt
new file mode 100644
index 0000000..492d7f9
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CaptureLimiter.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.graph
+
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.core.Log
+import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.update
+import kotlinx.atomicfu.updateAndGet
+
+/**
+ * On some devices, we need to wait for 10 frames to complete before we can guarantee the success of
+ * single capture requests. This is a quirk identified as part of b/287020251 and reported in
+ * b/289284907.
+ *
+ * During initialization, setting the graphLoop will disableCaptureProcessing until after the
+ * required number of frames have been completed.
+ */
+internal class CaptureLimiter(private val requestsUntilActive: Long) :
+ Request.Listener, GraphLoop.Listener {
+ init {
+ require(requestsUntilActive > 0)
+ }
+
+ private val frameCount = atomic(0L)
+ private var _graphLoop: GraphLoop? = null
+ var graphLoop: GraphLoop
+ get() = _graphLoop!!
+ set(value) {
+ check(_graphLoop == null) { "GraphLoop has already been set!" }
+ _graphLoop = value
+ value.captureProcessingEnabled = false
+ Log.warn {
+ "Capture processing has been disabled for $value until $requestsUntilActive " +
+ "frames have been completed."
+ }
+ }
+
+ override fun onComplete(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ result: FrameInfo
+ ) {
+ val count = frameCount.updateAndGet { if (it == -1L) -1 else it + 1 }
+ if (count == requestsUntilActive) {
+ Log.warn { "Capture processing is now enabled for $_graphLoop after $count frames." }
+ graphLoop.captureProcessingEnabled = true
+ }
+ }
+
+ override fun onStopRepeating() {
+ // Ignored
+ }
+
+ override fun onGraphStopped() {
+ // If the cameraGraph is stopped, reset the counter
+ frameCount.update { if (it == -1L) -1 else 0 }
+ graphLoop.captureProcessingEnabled = false
+ Log.warn {
+ "Capture processing has been disabled for $graphLoop until $requestsUntilActive " +
+ "frames have been completed."
+ }
+ }
+
+ override fun onGraphShutdown() {
+ frameCount.value = -1
+ graphLoop.captureProcessingEnabled = false
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 841683ec..177ce94 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -186,7 +186,7 @@
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request we should update the current
// parameters, but should not invalidate or trigger set a new listener.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
graphState3A.update(
aeMode,
afMode,
@@ -222,7 +222,7 @@
return result
}
- suspend fun submit3A(
+ fun submit3A(
aeMode: AeMode? = null,
afMode: AfMode? = null,
awbMode: AwbMode? = null,
@@ -231,9 +231,10 @@
awbRegions: List<MeteringRectangle>? = null
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
+
// Add the listener to a global pool of 3A listeners to monitor the state change to the
// desired one.
val listener = createListenerFor3AParams(aeMode, afMode, awbMode)
@@ -247,7 +248,7 @@
afRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
awbRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
- if (!graphProcessor.trySubmit(extra3AParams)) {
+ if (!graphProcessor.submit(extra3AParams)) {
graphListener3A.removeListener(listener)
return deferredResult3ASubmitFailed
}
@@ -308,7 +309,7 @@
// If the GraphProcessor does not have a repeating request we should update the current
// parameters, but should not invalidate or trigger set a new listener.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
@@ -316,7 +317,7 @@
// a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
if (afLockBehaviorSanitized.shouldUnlockAf()) {
debug { "lock3A - sending a request to unlock af first." }
- if (!graphProcessor.trySubmit(parameterForAfTriggerCancel)) {
+ if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
return deferredResult3ASubmitFailed
}
}
@@ -394,7 +395,7 @@
* There are two requests involved in this operation, (a) a single request with af trigger =
* cancel, to unlock af, and then (a) a repeating request to unlock ae, awb.
*/
- suspend fun unlock3A(
+ fun unlock3A(
ae: Boolean? = null,
af: Boolean? = null,
awb: Boolean? = null,
@@ -410,17 +411,14 @@
return CompletableDeferred(Result3A(Status.OK, /* frameMetadata= */ null))
}
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
// If we explicitly need to unlock af first before proceeding to lock it, we need to send
// a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
if (afSanitized == true) {
debug { "unlock3A - sending a request to unlock af first." }
- if (!graphProcessor.trySubmit(parameterForAfTriggerCancel)) {
- debug { "unlock3A - request to unlock af failed, returning early." }
- return deferredResult3ASubmitFailed
- }
+ graphProcessor.submit(parameterForAfTriggerCancel)
}
// As needed unlock ae, awb and wait for ae, af and awb to converge.
@@ -463,7 +461,7 @@
* which the locks were applied or the frame number at which the method returned early because
* either frame limit or time limit was reached.
*/
- suspend fun lock3AForCapture(
+ fun lock3AForCapture(
lockedCondition: ((FrameMetadata) -> Boolean)? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
@@ -496,7 +494,7 @@
* which the locks were applied or the frame number at which the method returned early because
* either frame limit or time limit was reached.
*/
- suspend fun lock3AForCapture(
+ fun lock3AForCapture(
triggerAf: Boolean = true,
waitForAwb: Boolean = false,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
@@ -538,14 +536,14 @@
* which the locks were applied or the frame number at which the method returned early because
* either frame limit or time limit was reached.
*/
- private suspend fun lock3AForCapture(
+ private fun lock3AForCapture(
triggerCondition: Map<CaptureRequest.Key<*>, Any>? = null,
lockedCondition: ((FrameMetadata) -> Boolean)? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
@@ -569,24 +567,19 @@
)
graphListener3A.addListener(listener)
-
debug { "lock3AForCapture - sending a request to trigger ae precapture metering and af." }
- if (!graphProcessor.trySubmit(finalTriggerCondition)) {
- debug {
- "lock3AForCapture - request to trigger ae precapture metering and af failed, " +
- "returning early."
- }
+
+ if (!graphProcessor.submit(finalTriggerCondition)) {
graphListener3A.removeListener(listener)
return deferredResult3ASubmitFailed
}
-
graphProcessor.invalidate()
return listener.result
}
- suspend fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A> {
+ fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A> {
// If the GraphProcessor does not have a repeating request, we should fail immediately.
- if (!graphProcessor.hasRepeatingRequest()) {
+ if (graphProcessor.repeatingRequest == null) {
return deferredResult3ASubmitFailed
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -601,22 +594,15 @@
* REF :
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
- private suspend fun unlock3APostCaptureAndroidLAndBelow(
- cancelAf: Boolean = true
- ): Deferred<Result3A> {
+ private fun unlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true): Deferred<Result3A> {
debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
- if (
- !graphProcessor.trySubmit(
- if (cancelAf) {
- unlock3APostCaptureLockAeAndCancelAfParams
- } else {
- unlock3APostCaptureLockAeParams
- }
- )
- ) {
- debug { "unlock3AForCapture - request to cancel af and lock ae as failed." }
- return deferredResult3ASubmitFailed
- }
+ val cancelParams =
+ if (cancelAf) {
+ unlock3APostCaptureLockAeAndCancelAfParams
+ } else {
+ unlock3APostCaptureLockAeParams
+ }
+ if (!graphProcessor.submit(cancelParams)) return deferredResult3ASubmitFailed
// Listener to monitor when we receive the capture result corresponding to the request
// below.
@@ -624,8 +610,7 @@
graphListener3A.addListener(listener)
debug { "unlock3AForCapture - sending a request to turn off ae." }
- if (!graphProcessor.trySubmit(unlock3APostCaptureUnlockAeParams)) {
- debug { "unlock3AForCapture - request to unlock ae failed." }
+ if (!graphProcessor.submit(unlock3APostCaptureUnlockAeParams)) {
graphListener3A.removeListener(listener)
return deferredResult3ASubmitFailed
}
@@ -639,16 +624,10 @@
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
*/
@RequiresApi(23)
- private suspend fun unlock3APostCaptureAndroidMAndAbove(
- cancelAf: Boolean = true
- ): Deferred<Result3A> {
+ private fun unlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true): Deferred<Result3A> {
debug { "unlock3APostCapture - sending a request to reset af and ae precapture metering." }
val cancelParams = if (cancelAf) aePrecaptureAndAfCancelParams else aePrecaptureCancelParams
- if (!graphProcessor.trySubmit(cancelParams)) {
- debug {
- "unlock3APostCapture - request to reset af and ae precapture metering failed, " +
- "returning early."
- }
+ if (!graphProcessor.submit(cancelParams)) {
return deferredResult3ASubmitFailed
}
@@ -677,11 +656,7 @@
return update3A(aeMode = desiredAeMode, flashMode = flashMode)
}
- internal fun onStopRepeating() {
- graphListener3A.onStopRepeating()
- }
-
- private suspend fun lock3ANow(
+ private fun lock3ANow(
aeLockBehavior: Lock3ABehavior?,
afLockBehavior: Lock3ABehavior?,
awbLockBehavior: Lock3ABehavior?,
@@ -724,7 +699,9 @@
}
debug { "lock3A - submitting a request to lock af." }
- val submitSuccess = graphProcessor.trySubmit(parameterForAfTriggerStart)
+ if (!graphProcessor.submit(parameterForAfTriggerStart)) {
+ return deferredResult3ASubmitFailed
+ }
lastAeMode?.let {
graphState3A.update(aeMode = it)
@@ -733,14 +710,6 @@
// w.r.t. building the parameter snapshot
graphProcessor.invalidate()
}
-
- if (!submitSuccess) {
- // TODO(sushilnath@): Change the error code to a more specific code so it's clear
- // that one of the request in sequence of requests failed and the caller should
- // unlock 3A to bring the 3A system to an initial state and then try again if they
- // want to. The other option is to reset or restore the 3A state here.
- return deferredResult3ASubmitFailed
- }
return resultForLocked!!
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
new file mode 100644
index 0000000..80ce6b0f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
@@ -0,0 +1,489 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.graph
+
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraGraphId
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.ProcessingQueue
+import androidx.camera.camera2.pipe.core.ProcessingQueue.Companion.processIn
+import androidx.camera.camera2.pipe.putAllMetadata
+import java.io.Closeable
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+
+/**
+ * GraphLoop is a thread-safe class that handles incoming state changes and requests and executes
+ * them, in order, on a dispatcher. In addition, this implementation handles several optimizations
+ * that enable requests to be deterministically skipped or aborted, and is responsible for the
+ * cleanup of pending requests during shutdown.
+ */
+internal class GraphLoop(
+ private val cameraGraphId: CameraGraphId,
+ private val defaultParameters: Map<*, Any?>,
+ private val requiredParameters: Map<*, Any?>,
+ private val graphListeners: List<Request.Listener>,
+ private val graphState3A: GraphState3A?,
+ private val listeners: List<GraphLoop.Listener>,
+ private val shutdownScope: CoroutineScope,
+ dispatcher: CoroutineDispatcher
+) : Closeable {
+ internal interface Listener {
+ fun onStopRepeating()
+
+ fun onGraphStopped()
+
+ fun onGraphShutdown()
+ }
+
+ private val lock = Any()
+ private val graphProcessorScope =
+ CoroutineScope(dispatcher.plus(CoroutineName("CXCP-GraphLoop")))
+ private val processingQueue =
+ ProcessingQueue(onUnprocessedElements = ::onShutdown, process = ::commandLoop)
+ .processIn(graphProcessorScope)
+
+ @Volatile private var closed = false
+
+ @GuardedBy("lock") private var _requestProcessor: GraphRequestProcessor? = null
+
+ @GuardedBy("lock") private var _repeatingRequest: Request? = null
+
+ var requestProcessor: GraphRequestProcessor?
+ get() = synchronized(lock) { _requestProcessor }
+ set(value) {
+ synchronized(lock) {
+ val previous = _requestProcessor
+ _requestProcessor = value
+
+ if (closed) {
+ _requestProcessor = null
+ if (value != null) {
+ shutdownScope.launch { value.shutdown() }
+ }
+ return
+ }
+
+ // Ignore duplicate calls to set with the same value.
+ if (previous === value) {
+ return@synchronized
+ }
+
+ if (previous != null) {
+ // Closing the request processor can (sometimes) block the calling thread.
+ // Make sure this is invoked in the background.
+ processingQueue.tryEmit(CloseRequestProcessor(previous))
+ }
+
+ if (value != null) {
+ val repeatingRequest = _repeatingRequest
+ if (repeatingRequest == null) {
+ // This handles the case where a single request has been issued before
+ // the GraphRequestProcessor was configured when there is not repeating
+ // request. Invalidate will cause the commandLoop to re-evaluate, which
+ // may succeed now that a valid request processor has been configured.
+ processingQueue.tryEmit(Invalidate)
+ } else {
+ // If there is an active repeating request, make sure the request is
+ // issued to the new request processor. This serves the same purpose as
+ // Invalidate which re-triggers the commandLoop.
+ processingQueue.tryEmit(StartRepeating(repeatingRequest))
+ }
+ }
+ }
+
+ if (value == null) {
+ for (i in listeners.indices) {
+ listeners[i].onGraphStopped()
+ }
+ }
+ }
+
+ var repeatingRequest: Request?
+ get() = synchronized(lock) { _repeatingRequest }
+ set(value) {
+ synchronized(lock) {
+ val previous = _repeatingRequest
+ _repeatingRequest = value
+
+ // Ignore duplicate calls to set null, this avoids multiple stopRepeating calls from
+ // being invoked.
+ if (previous == null && value == null) {
+ return@synchronized
+ }
+
+ if (value != null) {
+ processingQueue.tryEmit(StartRepeating(value))
+ } else {
+ // If the repeating request is set to null, stop repeating (using the current
+ // request processor instance). This is allowed because stop and abort can be
+ // called on a requestProcessor that has, or is in the process, of being
+ // released.
+ processingQueue.tryEmit(StopRepeating(_requestProcessor))
+ }
+ }
+
+ if (value == null) {
+ for (i in listeners.indices) {
+ listeners[i].onStopRepeating()
+ }
+ }
+ }
+
+ private val _captureProcessingEnabled = atomic(true)
+ var captureProcessingEnabled: Boolean
+ get() = _captureProcessingEnabled.value
+ set(value) {
+ _captureProcessingEnabled.value = value
+ if (value) {
+ invalidate()
+ }
+ }
+
+ fun submit(request: Request): Boolean = submit(listOf(request))
+
+ fun submit(requests: List<Request>): Boolean {
+ if (!processingQueue.tryEmit(SubmitCapture(requests))) {
+ abortRequests(requests)
+ return false
+ }
+ return true
+ }
+
+ fun submit(parameters: Map<*, Any?>): Boolean {
+ synchronized(lock) {
+ val currentRepeatingRequest = _repeatingRequest
+ check(currentRepeatingRequest != null) {
+ "Cannot submit parameters without an active repeating request!"
+ }
+ return processingQueue.tryEmit(SubmitParameters(currentRepeatingRequest, parameters))
+ }
+ }
+
+ fun abort() {
+ processingQueue.tryEmit(AbortCaptures(requestProcessor))
+ }
+
+ fun invalidate() {
+ synchronized(lock) {
+ val currentRepeatingRequest = _repeatingRequest
+ if (currentRepeatingRequest != null) {
+ processingQueue.tryEmit(StartRepeating(currentRepeatingRequest))
+ } else {
+ processingQueue.tryEmit(Invalidate)
+ }
+ }
+ }
+
+ override fun close() {
+ synchronized(lock) {
+ if (closed) return
+ closed = true
+
+ val previousRequestProcessor = _requestProcessor
+ _requestProcessor = null
+
+ // Shutdown Process - This will occur when the CameraGraph is closed:
+ // 1. Clear the _requestProcessor reference. This stops enqueued requests from being
+ // processed, since they use the current requestProcessor instance.
+ // 2. Emit a Shutdown call. This will clear or abort any previous requests and will
+ // close the request processor before cancelling the scope.
+ processingQueue.tryEmit(Shutdown(previousRequestProcessor))
+ }
+
+ for (i in listeners.indices) {
+ listeners[i].onGraphShutdown()
+ }
+ }
+
+ /**
+ * Invoke the onAborted listener for each request, prioritizing internal listeners over the
+ * request-specific listeners.
+ */
+ private fun abortRequests(requests: List<Request>) {
+ // Internal listeners
+ for (rIdx in requests.indices) {
+ val request = requests[rIdx]
+ for (listenerIdx in graphListeners.indices) {
+ graphListeners[listenerIdx].onAborted(request)
+ }
+ }
+
+ // Request-specific listeners
+ for (rIdx in requests.indices) {
+ val request = requests[rIdx]
+ for (listenerIdx in request.listeners.indices) {
+ request.listeners[listenerIdx].onAborted(request)
+ }
+ }
+ }
+
+ private fun onShutdown(unprocessedCommands: List<GraphCommand>) {
+ // Cleanup unprocessed state and commands.
+ for (command in unprocessedCommands) {
+ when (command) {
+ is SubmitCapture -> abortRequests(command.requests)
+ else -> continue
+ }
+ }
+ }
+
+ private var lastRepeatingRequest: Request? = null
+
+ private suspend fun commandLoop(commands: MutableList<GraphCommand>) {
+ // Command Loop Design:
+ //
+ // 1. Iterate through commands, newest first.
+ // 2. If any of the commands match in this first phase, execute the command, remove it (as
+ // well as any other commands that are no longer valid), and then return. This will cause
+ // processCommands to be check for new commands and re-invoke.
+ // 3. If none of the phase 1 commands match, process the remaining commands in the order
+ // they were submitted, returning after each submission.
+
+ // ### Phase 1: LIFO High Priority Command Selection ###
+
+ var idx = -1
+ if (commands.size > 1) {
+ for (i in commands.indices.reversed()) {
+ when (commands[i]) {
+ is Invalidate,
+ is Shutdown,
+ is AbortCaptures,
+ is CloseRequestProcessor,
+ is StopRepeating -> {
+ idx = i
+ break
+ }
+ is StartRepeating,
+ is SubmitCapture,
+ is SubmitParameters -> continue
+ }
+ }
+
+ if (idx < 0) {
+ // ### Phase 2: LIFO Secondary Command Selection ###
+ //
+ // This primarily exists so that [StartRepeating, StopRepeating, StartRepeating]
+ // will execute the StopRepeating before the StartRepeating command. SubmitCapture
+ // and SubmitParameters are not affected because they are not skip-able.
+ for (i in commands.indices.reversed()) {
+ if (commands[i] is StartRepeating) {
+ idx = i
+ break
+ }
+ }
+ }
+ }
+
+ if (idx < 0) {
+ // Default: Pick the first command in the queue.
+ idx = 0
+ }
+
+ // Process and optionally remove the selected command.
+ when (val command = commands[idx]) {
+ is Invalidate -> commands.removeAt(idx)
+ is Shutdown -> {
+ commands.removeAt(idx)
+
+ // Remove all commands leading up to Shutdown and abort requests.
+ commands.removeUpTo(idx) {
+ if (it is SubmitCapture) {
+ abortRequests(it.requests)
+ }
+ true
+ }
+
+ // If the request processor is not null, shut it down. Consider making this a
+ // suspending call instead of just blocking to allow suspend-with-timeout.
+ command.requestProcessor?.shutdown()
+
+ // Cancel the scope. This will trigger the onUnprocessedItems callback after after
+ // this hits the next suspension point.
+ graphProcessorScope.cancel()
+ }
+ is CloseRequestProcessor -> {
+ commands.removeAt(idx)
+ command.requestProcessor.shutdown()
+ }
+ is AbortCaptures -> {
+ commands.removeAt(idx)
+
+ // Attempt to abort captures in the approximate order they were submitted:
+ // 1. Abort captures submitted to the camera
+ // 2. Invoke abort on captures that have not yet been submitted to the camera.
+ if (command.requestProcessor != null) {
+ command.requestProcessor.abortCaptures()
+ }
+ commands.removeUpTo(idx) {
+ when (it) {
+ is AbortCaptures -> it.requestProcessor === command.requestProcessor
+ is SubmitCapture -> {
+ abortRequests(it.requests)
+ true
+ }
+ is SubmitParameters -> {
+ // Silently remove parameter requests. These are normally associated
+ // with a repeating request, which will not expect abort commands to
+ // fire.
+ true
+ }
+ else -> false
+ }
+ }
+ }
+ is StopRepeating -> {
+ commands.removeAt(idx)
+ if (command.requestProcessor != null) {
+ command.requestProcessor.stopRepeating()
+ }
+ // Always remove prior SubmitRepeating and StopRepeating commands, but only if the
+ // StopRepeating commands are associated with the same requestProcessor instance.
+ commands.removeUpTo(idx) {
+ it is StartRepeating ||
+ (it is StopRepeating && it.requestProcessor === command.requestProcessor)
+ }
+ }
+ is StartRepeating -> {
+ val success =
+ requestProcessor?.buildAndSubmit(
+ isRepeating = true,
+ requests = listOf(command.request)
+ ) == true
+ if (success) {
+ lastRepeatingRequest = command.request
+ commands.removeAt(idx)
+ }
+ commands.removeUpTo(idx) { it is StartRepeating }
+ }
+ is SubmitCapture -> {
+ if (!_captureProcessingEnabled.value) {
+ Log.warn {
+ "Skipping SubmitCapture because capture processing is paused: " +
+ "${command.requests}"
+ }
+ return
+ }
+ val success =
+ requestProcessor?.buildAndSubmit(
+ isRepeating = false,
+ requests = command.requests
+ ) == true
+ if (success) {
+ commands.removeAt(idx)
+ } else {
+ Log.warn {
+ "SubmitCapture failed to submit requests to $requestProcessor: " +
+ "${command.requests}, may be retried."
+ }
+ }
+ }
+ is SubmitParameters -> {
+ if (!_captureProcessingEnabled.value) {
+ Log.warn {
+ "Skipping SubmitParameters because capture processing is paused: " +
+ "${command.parameters}"
+ }
+ return
+ }
+
+ val success =
+ requestProcessor?.buildAndSubmit(
+ isRepeating = false,
+ requests = listOf(command.request),
+ parameters = command.parameters
+ ) == true
+ if (success) {
+ commands.removeAt(idx)
+ } else {
+ Log.warn {
+ "SubmitParameters failed to submit to $requestProcessor: " +
+ Debug.formatParameterMap(command.parameters)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility function to remove items by index from a mutable list up-to a given index that match
+ * the provided function.
+ */
+ private inline fun <T> MutableList<T>.removeUpTo(idx: Int, predicate: (T) -> Boolean) {
+ var a = 0
+ var b = idx
+ while (a < b) {
+ if (predicate(this[a])) {
+ this.removeAt(a)
+ b-- // Reduce upper bound
+ } else {
+ a++ // Advance lower bound
+ }
+ }
+ }
+
+ private fun GraphRequestProcessor.buildAndSubmit(
+ isRepeating: Boolean,
+ requests: List<Request>,
+ parameters: Map<*, Any?> = emptyMap<Any, Any?>()
+ ): Boolean {
+ val graphRequiredParameters = buildMap {
+ // Build the required parameter map:
+ // 1. graphState3A parameters override provided parameters.
+ // 2. requiredParameters override graphState and parameters.
+ this.putAllMetadata(parameters)
+ graphState3A?.writeTo(this)
+ this.putAllMetadata(requiredParameters)
+ }
+
+ return this.submit(
+ isRepeating = isRepeating,
+ requests = requests,
+ defaultParameters = defaultParameters,
+ requiredParameters = graphRequiredParameters,
+ listeners = graphListeners
+ )
+ }
+
+ override fun toString(): String = "GraphLoop($cameraGraphId)"
+
+ private sealed class GraphCommand
+
+ private object Invalidate : GraphCommand()
+
+ private class Shutdown(val requestProcessor: GraphRequestProcessor?) : GraphCommand()
+
+ private class CloseRequestProcessor(val requestProcessor: GraphRequestProcessor) :
+ GraphCommand()
+
+ private class StopRepeating(val requestProcessor: GraphRequestProcessor?) : GraphCommand()
+
+ private class AbortCaptures(val requestProcessor: GraphRequestProcessor?) : GraphCommand()
+
+ private class StartRepeating(val request: Request) : GraphCommand()
+
+ private class SubmitCapture(val requests: List<Request>) : GraphCommand()
+
+ private class SubmitParameters(val request: Request, val parameters: Map<*, Any?>) :
+ GraphCommand()
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index 858f8e2..1233c6b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -13,11 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.camera.camera2.pipe.graph
-import android.os.Build
-import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraphId
import androidx.camera.camera2.pipe.CaptureSequenceProcessor
@@ -35,25 +32,14 @@
import androidx.camera.camera2.pipe.compat.CameraPipeKeys
import androidx.camera.camera2.pipe.config.CameraGraphScope
import androidx.camera.camera2.pipe.config.ForCameraGraph
-import androidx.camera.camera2.pipe.core.CoroutineMutex
-import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.core.Log.info
-import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.core.Threads
-import androidx.camera.camera2.pipe.core.withLockLaunch
-import androidx.camera.camera2.pipe.formatForLogs
-import androidx.camera.camera2.pipe.putAllMetadata
import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
import javax.inject.Inject
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
/**
* The [GraphProcessor] is responsible for queuing and then submitting them to a
@@ -63,18 +49,20 @@
internal interface GraphProcessor {
val graphState: StateFlow<GraphState>
- fun submit(request: Request)
+ /**
+ * The currently configured repeating request. Setting this value to null will attempt to call
+ * stopRepeating on the Camera.
+ */
+ var repeatingRequest: Request?
- fun submit(requests: List<Request>)
+ fun submit(request: Request): Boolean
+
+ fun submit(requests: List<Request>): Boolean
/**
- * This tries to submit a list of parameters — essentially a list of request settings usually
- * from 3A methods. It does this by setting the given parameters onto the current repeating
- * request on a best-effort basis.
- *
- * If the CameraGraph hasn't been started yet, but we do have a pending repeating request
- * queued, the method will suspend until we have a submitted repeating request and only then
- * submits the parameters.
+ * This tries to submit a list of parameters based on the current repeating request. If the
+ * CameraGraph hasn't been started but a valid repeating request has already been set this
+ * method will enqueue the submission based on the repeating request.
*
* This behavior is required if users call 3A methods immediately after start. For example:
* ```
@@ -89,21 +77,9 @@
* implementation handles this on a best-effort basis for the developer. Please read b/263211462
* for more context.
*
- * However, if the CameraGraph does NOT have a current repeating request or any repeating
- * requests queued up, the method will return false.
+ * This method will throw a checked exception if no repeating request has been configured.
*/
- suspend fun trySubmit(parameters: Map<*, Any?>): Boolean
-
- fun startRepeating(request: Request)
-
- fun stopRepeating()
-
- /**
- * Checks whether we have a repeating request in progress. Returns true when we have a repeating
- * request already submitted or is being submitted. This is used to check whether we can try to
- * submit parameters (used by 3A methods).
- */
- fun hasRepeatingRequest(): Boolean
+ fun submit(parameters: Map<*, Any?>): Boolean
/**
* Indicates that internal parameters may have changed, and that the repeating request should be
@@ -129,34 +105,50 @@
internal class GraphProcessorImpl
@Inject
constructor(
- private val threads: Threads,
+ threads: Threads,
private val cameraGraphId: CameraGraphId,
private val cameraGraphConfig: CameraGraph.Config,
- private val graphState3A: GraphState3A,
- @ForCameraGraph private val graphScope: CoroutineScope,
- @ForCameraGraph private val graphListeners: List<@JvmSuppressWildcards Request.Listener>
+ graphState3A: GraphState3A,
+ graphListener3A: Listener3A,
+ @ForCameraGraph graphListeners: List<@JvmSuppressWildcards Request.Listener>
) : GraphProcessor, GraphListener {
- private val lock = Any()
- private val tryStartRepeatingExecutionLock = Any()
- private val coroutineMutex = CoroutineMutex()
+ private val graphLoop: GraphLoop
- @GuardedBy("lock") private val submitQueue: MutableList<List<Request>> = ArrayList()
+ init {
+ val defaultParameters = cameraGraphConfig.defaultParameters
+ val requiredParameters = cameraGraphConfig.requiredParameters
+ val ignore3AState =
+ (defaultParameters[CameraPipeKeys.ignore3ARequiredParameters] == true) ||
+ (requiredParameters[CameraPipeKeys.ignore3ARequiredParameters] == true)
- @GuardedBy("lock") private val repeatingQueue: MutableList<Request> = ArrayList()
+ if (ignore3AState) {
+ info {
+ "${CameraPipeKeys.ignore3ARequiredParameters} is set to true, " +
+ "ignoring GraphState3A parameters."
+ }
+ }
- @GuardedBy("lock") private var currentRepeatingRequest: Request? = null
+ val captureLimiter =
+ if (Camera2Quirks.shouldWaitForRepeatingBeforeCapture()) {
+ CaptureLimiter(10)
+ } else {
+ null
+ }
- @GuardedBy("lock") private var _requestProcessor: GraphRequestProcessor? = null
+ graphLoop =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ graphListeners = graphListeners + listOfNotNull(captureLimiter),
+ graphState3A = if (ignore3AState) null else graphState3A,
+ listeners = listOfNotNull(graphListener3A, captureLimiter),
+ shutdownScope = threads.globalScope,
+ dispatcher = threads.lightweightDispatcher
+ )
- @GuardedBy("lock") private var submitting = false
-
- @GuardedBy("lock") private var dirty = false
-
- @GuardedBy("lock") private var closed = false
-
- @GuardedBy("lock") private var pendingParameters: Map<*, Any?>? = null
-
- @GuardedBy("lock") private var pendingParametersDeferred: CompletableDeferred<Boolean>? = null
+ captureLimiter?.graphLoop = graphLoop
+ }
// On some devices, we need to wait for 10 frames to complete before we can guarantee the
// success of single capture requests. This is a quirk identified as part of b/287020251 and
@@ -179,12 +171,16 @@
}
}
}
-
private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
-
override val graphState: StateFlow<GraphState>
get() = _graphState
+ override var repeatingRequest: Request?
+ get() = graphLoop.repeatingRequest
+ set(value) {
+ graphLoop.repeatingRequest = value
+ }
+
override fun onGraphStarting() {
debug { "$this onGraphStarting" }
_graphState.value = GraphStateStarting
@@ -193,24 +189,7 @@
override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {
debug { "$this onGraphStarted" }
_graphState.value = GraphStateStarted
- var old: GraphRequestProcessor? = null
- synchronized(lock) {
- if (closed) {
- requestProcessor.close()
- return
- }
-
- if (_requestProcessor != null && _requestProcessor !== requestProcessor) {
- old = _requestProcessor
- }
- _requestProcessor = requestProcessor
- }
-
- val processorToClose = old
- if (processorToClose != null) {
- synchronized(processorToClose) { processorToClose.close() }
- }
- resubmit()
+ graphLoop.requestProcessor = requestProcessor
}
override fun onGraphStopping() {
@@ -221,41 +200,12 @@
override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {
debug { "$this onGraphStopped" }
_graphState.value = GraphStateStopped
- if (requestProcessor == null) return
- var old: GraphRequestProcessor? = null
- synchronized(lock) {
- if (closed) {
- return
- }
-
- if (requestProcessor === _requestProcessor) {
- old = _requestProcessor
- _requestProcessor = null
- } else {
- warn {
- "Refusing to detach $requestProcessor. " +
- "It is different from $_requestProcessor"
- }
- }
- }
-
- val processorToClose = old
- if (processorToClose != null) {
- synchronized(processorToClose) { processorToClose.close() }
- }
+ graphLoop.requestProcessor = null
}
override fun onGraphModified(requestProcessor: GraphRequestProcessor) {
debug { "$this onGraphModified" }
- synchronized(lock) {
- if (closed) {
- return
- }
- if (requestProcessor !== _requestProcessor) {
- return
- }
- }
- resubmit()
+ graphLoop.invalidate()
}
override fun onGraphError(graphStateError: GraphStateError) {
@@ -269,60 +219,19 @@
}
}
- override fun startRepeating(request: Request) {
- synchronized(lock) {
- if (closed) return
- repeatingQueue.add(request)
- debug { "startRepeating with ${request.formatForLogs()}" }
+ override fun submit(request: Request): Boolean = submit(listOf(request))
- coroutineMutex.withLockLaunch(graphScope) { tryStartRepeating() }
- }
- }
-
- override fun stopRepeating() {
- val processor: GraphRequestProcessor?
-
- synchronized(lock) {
- processor = _requestProcessor
- repeatingQueue.clear()
- currentRepeatingRequest = null
-
- coroutineMutex.withLockLaunch(graphScope) {
- Debug.traceStart { "$this#stopRepeating" }
- // Start with requests that have already been submitted
- if (processor != null) {
- synchronized(processor) { processor.stopRepeating() }
- }
- Debug.traceStop()
+ override fun submit(requests: List<Request>): Boolean {
+ val reprocessingRequest = requests.firstOrNull { it.inputRequest != null }
+ if (reprocessingRequest != null) {
+ checkNotNull(cameraGraphConfig.input) {
+ "Cannot submit $reprocessingRequest with input request " +
+ "${reprocessingRequest.inputRequest} to $this because CameraGraph was not " +
+ "configured to support reprocessing"
}
}
- }
- override fun submit(request: Request) {
- submit(listOf(request))
- }
-
- override fun submit(requests: List<Request>) {
- requests
- .firstOrNull { it.inputRequest != null }
- ?.let {
- check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- "Reprocessing not supported on Android ${Build.VERSION.SDK_INT} devices"
- }
- checkNotNull(cameraGraphConfig.input) {
- "Cannot submit request $it with input request ${it.inputRequest} " +
- "to $this because CameraGraph was not configured to support reprocessing"
- }
- }
- synchronized(lock) {
- if (closed) {
- graphScope.launch(threads.lightweightDispatcher) { abortBurst(requests) }
- return
- }
- submitQueue.add(requests)
- }
-
- graphScope.launch(threads.lightweightDispatcher) { submitLoop() }
+ return graphLoop.submit(requests)
}
/**
@@ -331,319 +240,18 @@
* false. Otherwise, the method tries to submit the provided [parameters] and suspends until it
* finishes.
*/
- override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean =
- withContext(threads.lightweightDispatcher) {
- val processor: GraphRequestProcessor?
- val request: Request?
- val requiredParameters: MutableMap<Any, Any?> = mutableMapOf()
- var deferredResult: CompletableDeferred<Boolean>? = null
-
- synchronized(lock) {
- if (closed) return@withContext false
- processor = _requestProcessor
- request = currentRepeatingRequest
-
- // If there is no current repeating request and no repeating requests are in the
- // queue (i.e., startRepeating wasn't called before the 3A methods), we should just
- // fail immediately.
- if (request == null && repeatingQueue.isEmpty()) {
- return@withContext false
- }
-
- requiredParameters.putAllMetadata(parameters.toMutableMap())
- graphState3A.writeTo(requiredParameters)
- requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
-
- if (processor == null || request == null) {
- // If a previous set of parameters haven't been submitted yet, consider it stale
- pendingParametersDeferred?.complete(false)
-
- debug { "Holding parameters to be submitted later" }
- deferredResult = CompletableDeferred<Boolean>()
- pendingParametersDeferred = deferredResult
- pendingParameters = requiredParameters
- }
- }
-
- return@withContext when {
- processor == null || request == null -> deferredResult?.await() == true
- else ->
- processor.submit(
- isRepeating = false,
- requests = listOf(request),
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = requiredParameters,
- listeners = graphListeners
- )
- }
- }
-
- override fun hasRepeatingRequest() =
- synchronized(tryStartRepeatingExecutionLock) {
- synchronized(lock) { currentRepeatingRequest != null || repeatingQueue.isNotEmpty() }
- }
+ override fun submit(parameters: Map<*, Any?>): Boolean = graphLoop.submit(parameters)
override fun invalidate() {
- // Invalidate is only used for updates to internal state (listeners, parameters, etc) and
- // should not (currently) attempt to resubmit the normal request queue.
- graphScope.launch(threads.lightweightDispatcher) { tryStartRepeating() }
+ graphLoop.invalidate()
}
override fun abort() {
- val processor: GraphRequestProcessor?
- val requests: List<List<Request>>
-
- synchronized(lock) {
- processor = _requestProcessor
- requests = submitQueue.toList()
- submitQueue.clear()
- }
-
- graphScope.launch(threads.lightweightDispatcher) {
- Debug.traceStart { "$this#abort" }
- // Start with requests that have already been submitted
- if (processor != null) {
- synchronized(processor) { processor.abortCaptures() }
- }
-
- // Then abort requests that have not been submitted
- for (burst in requests) {
- abortBurst(burst)
- }
- Debug.traceStop()
- }
+ graphLoop.abort()
}
override fun close() {
- val processor: GraphRequestProcessor?
- synchronized(lock) {
- if (closed) {
- return
- }
- closed = true
- processor = _requestProcessor
- _requestProcessor = null
- }
-
- processor?.close()
- abort()
- }
-
- private fun resubmit() {
- graphScope.launch(threads.lightweightDispatcher) {
- tryStartRepeating()
- submitLoop()
- }
- }
-
- private fun abortBurst(requests: List<Request>) {
- for (request in requests) {
- abortRequest(request)
- }
- }
-
- private fun abortRequest(request: Request) {
- for (listenerIdx in graphListeners.indices) {
- graphListeners[listenerIdx].onAborted(request)
- }
-
- for (listenerIdx in request.listeners.indices) {
- request.listeners[listenerIdx].onAborted(request)
- }
- }
-
- private fun tryStartRepeating() =
- synchronized(tryStartRepeatingExecutionLock) {
- val processor: GraphRequestProcessor
- val requests = mutableListOf<Request>()
- var shouldRetryRequests = false
-
- synchronized(lock) {
- if (closed || _requestProcessor == null) return
-
- processor = _requestProcessor!!
-
- if (repeatingQueue.isNotEmpty()) {
- requests.addAll(repeatingQueue)
- repeatingQueue.clear()
- shouldRetryRequests = true
- } else {
- currentRepeatingRequest?.let { requests.add(it) }
- }
- }
- if (requests.isEmpty()) return
-
- Debug.traceStart { "$this#startRepeating" }
- var succeededIndex = -1
- synchronized(processor) {
- // Here an important optimization is applied. Newer repeating requests should always
- // supersede older ones. Instead of going from oldest request to newest, we can
- // start
- // from the newest request and immediately break when a request submission succeeds.
- for ((index, request) in requests.reversed().withIndex()) {
- val requiredParameters = mutableMapOf<Any, Any?>()
- graphState3A.writeTo(requiredParameters)
- requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
-
- if (
- processor.submit(
- isRepeating = true,
- requests = listOf(request),
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = requiredParameters,
- listeners = graphProcessorRepeatingListeners,
- )
- ) {
- // ONLY update the current repeating request if the update succeeds
- synchronized(lock) {
- if (processor === _requestProcessor) {
- currentRepeatingRequest = request
- trySubmitPendingParameters(processor, request)
- }
- }
- succeededIndex = index
- break
- }
- }
- }
- Debug.traceStop()
-
- if (shouldRetryRequests) {
- synchronized(lock) {
- // We should only retry the requests newer than the succeeded request, since the
- // succeeded request would prevail over the preceding requests that failed.
- val requestsToRetry = requests.slice(succeededIndex + 1 until requests.size)
-
- // We might have new repeating requests at this point, and these requests to
- // retry
- // should be placed in the front in order to preserve FIFO order.
- repeatingQueue.addAll(0, requestsToRetry)
- }
- }
- }
-
- @GuardedBy("lock")
- private fun trySubmitPendingParameters(processor: GraphRequestProcessor, request: Request) {
- val parameters = pendingParameters
- val deferred = pendingParametersDeferred
- if (parameters != null && deferred != null) {
- val resubmitResult =
- processor.submit(
- isRepeating = false,
- requests = listOf(request),
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = parameters,
- listeners = graphListeners
- )
- deferred.complete(resubmitResult)
-
- pendingParameters = null
- pendingParametersDeferred = null
- }
- }
-
- private fun submitLoop() {
- if (Camera2Quirks.shouldWaitForRepeatingBeforeCapture() && hasRepeatingRequest()) {
- debug {
- "Quirk: Waiting for 10 repeating requests to complete before submitting requests"
- }
- if (!repeatingRequestsCompleted.await(2, TimeUnit.SECONDS)) {
- warn { "Failed to wait for 10 repeating requests to complete after 2 seconds" }
- }
- }
-
- var burst: List<Request>
- var processor: GraphRequestProcessor
-
- synchronized(lock) {
- if (closed) return
-
- if (submitting) {
- dirty = true
- return
- }
-
- val nullableProcessor = _requestProcessor
- val nullableBurst = submitQueue.firstOrNull()
- if (nullableProcessor == null || nullableBurst == null) {
- return
- }
-
- processor = nullableProcessor
- burst = nullableBurst
-
- submitting = true
- }
-
- while (true) {
- var submitted = false
- Debug.traceStart { "$this#submit" }
- try {
- submitted =
- synchronized(processor) {
- val requiredParameters = mutableMapOf<Any, Any?>()
- if (
- cameraGraphConfig.defaultParameters[
- CameraPipeKeys.ignore3ARequiredParameters] == true ||
- cameraGraphConfig.requiredParameters[
- CameraPipeKeys.ignore3ARequiredParameters] == true
- ) {
- info {
- "${CameraPipeKeys.ignore3ARequiredParameters} is set to true, " +
- "ignoring 3A required parameters"
- }
- } else {
- graphState3A.writeTo(requiredParameters)
- }
- requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
-
- processor.submit(
- isRepeating = false,
- requests = burst,
- defaultParameters = cameraGraphConfig.defaultParameters,
- requiredParameters = requiredParameters,
- listeners = graphListeners
- )
- }
- } finally {
- Debug.traceStop()
- synchronized(lock) {
- if (submitted) {
- // submitQueue can potentially be cleared by abort() before entering here.
- check(submitQueue.isEmpty() || submitQueue.removeAt(0) === burst)
-
- val nullableBurst = submitQueue.firstOrNull()
- if (nullableBurst == null) {
- dirty = false
- submitting = false
- return
- }
-
- burst = nullableBurst
- } else if (!dirty) {
- debug { "Failed to submit $burst, and the queue is not dirty." }
- // If we did not submit, and we are also not dirty, then exit the loop
- submitting = false
- return
- } else {
- debug {
- "Failed to submit $burst but the request queue or processor is " +
- "dirty. Clearing dirty flag and attempting retry."
- }
- dirty = false
-
- // One possible situation is that the _requestProcessor was replaced or
- // set to null. If this happens, try to update the requestProcessor we
- // are currently using. If the current request processor is null, then
- // we cannot submit anyways.
- val nullableProcessor = _requestProcessor
- if (nullableProcessor != null) {
- processor = nullableProcessor
- }
- }
- }
- }
- }
+ graphLoop.close()
}
override fun toString(): String = "GraphProcessor(cameraGraph: $cameraGraphId)"
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
index 12b1f1e..cf9d3c2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphRequestProcessor.kt
@@ -61,10 +61,8 @@
object : CaptureSequence.CaptureSequenceListener {
override fun onCaptureSequenceComplete(captureSequence: CaptureSequence<*>) {
// Listen to the completion of active capture sequences and remove them from the
- // list
- // of currently active capture sequences. Since repeating requests are not required
- // to
- // execute, only non-repeating capture sequences are tracked.
+ // list of currently active capture sequences. Since repeating requests are not
+ // required to execute, only non-repeating capture sequences are tracked.
if (!captureSequence.repeating) {
synchronized(activeCaptureSequences) {
activeCaptureSequences.remove(captureSequence)
@@ -106,10 +104,10 @@
captureSequenceProcessor.stopRepeating()
}
- internal fun close() {
+ internal suspend fun shutdown() {
Log.debug { "Closing $this" }
if (closed.compareAndSet(expect = false, update = true)) {
- captureSequenceProcessor.close()
+ captureSequenceProcessor.shutdown()
}
}
@@ -122,7 +120,7 @@
): Boolean {
// Reject incoming requests if this instance has been stopped or closed.
if (closed.value) {
- Log.warn { "Rejecting requests $requests: Request processor is closed." }
+ Log.warn { "Failed to submit $requests: $this is closed." }
return false
}
@@ -156,7 +154,7 @@
// case, it was handled by aborting the requests and closing the images.
return true
}
- Log.warn { "Rejecting requests $requests: Could not create the capture sequence." }
+ Log.warn { "Failed to submit $requests: $this failed to build CaptureSequence." }
// We do not need to invoke the sequenceCompleteListener since it has not been added to
// the list of activeCaptureSequences yet.
@@ -166,7 +164,7 @@
// Re-check again and reject requests if this instance has been closed or stopped.
// This is an optimization since building the captureSequence can take non-zero time.
if (closed.value) {
- Log.warn { "Rejecting requests $requests: Request processor is closed." }
+ Log.warn { "Failed to submit $requests: $this is closed." }
return false
}
@@ -177,7 +175,7 @@
var captured = false
return try {
- Log.debug { "Submitting $captureSequence" }
+ Log.debug { "$this submitting $captureSequence" }
captureSequence.invokeOnRequestSequenceCreated()
// NOTE: This is an unusual synchronization call. The purpose is to avoid a rare but
@@ -189,11 +187,11 @@
synchronized(lock = captureSequence) {
// Check closed state right before submitting.
if (closed.value) {
- Log.warn { "Did not submit $captureSequence, $this was closed!" }
+ Log.warn { "Failed to submit $captureSequence: $this is closed." }
return false
}
- Debug.trace("CXCP#submitCaptureSequence") {
+ Debug.trace("CXCP#submit(CaptureSequence)") {
val sequenceNumber = captureSequenceProcessor.submit(captureSequence) ?: -1
captureSequence.sequenceNumber = sequenceNumber
sequenceNumber
@@ -203,10 +201,10 @@
if (result != -1) {
captureSequence.invokeOnRequestSequenceSubmitted()
captured = true
- Log.debug { "Submitted $captureSequence" }
+ Log.debug { "$this submitted $captureSequence" }
true
} else {
- Log.warn { "Did not submit $captureSequence, SequenceNumber was -1" }
+ Log.warn { "Failed to submit $captureSequence: $this received -1 from submit." }
false
}
} catch (closedException: ObjectUnavailableException) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
index 5cb02d3..f3477ffb 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Listener3A.kt
@@ -33,7 +33,7 @@
* for desired 3A state changes.
*/
@CameraGraphScope
-internal class Listener3A @Inject constructor() : Request.Listener {
+internal class Listener3A @Inject constructor() : Request.Listener, GraphLoop.Listener {
private val listeners: CopyOnWriteArrayList<Result3AStateListener> = CopyOnWriteArrayList()
override fun onRequestSequenceCreated(requestMetadata: RequestMetadata) {
@@ -66,12 +66,6 @@
listeners.remove(listener)
}
- internal fun onStopRepeating() {
- for (listener in listeners) {
- listener.onRequestSequenceStopped()
- }
- }
-
private fun updateListeners(requestNumber: RequestNumber, metadata: FrameMetadata) {
for (listener in listeners) {
if (listener.update(requestNumber, metadata)) {
@@ -79,4 +73,22 @@
}
}
}
+
+ override fun onStopRepeating() {
+ for (listener in listeners) {
+ listener.onStopRepeating()
+ }
+ }
+
+ override fun onGraphStopped() {
+ for (listener in listeners) {
+ listener.onStopRepeating()
+ }
+ }
+
+ override fun onGraphShutdown() {
+ for (listener in listeners) {
+ listener.onStopRepeating()
+ }
+ }
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
index 17aca1f..d338a9e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
@@ -38,12 +38,10 @@
* This update method can be called multiple times as we get newer [CaptureResult]s from the camera
* device. This class also exposes a [Deferred] to query the status of desired state.
*/
-internal interface Result3AStateListener {
+internal interface Result3AStateListener : GraphLoop.Listener {
fun onRequestSequenceCreated(requestNumber: RequestNumber)
fun update(requestNumber: RequestNumber, frameMetadata: FrameMetadata): Boolean
-
- fun onRequestSequenceStopped()
}
internal class Result3AStateListenerImpl(
@@ -133,12 +131,16 @@
return true
}
- override fun onRequestSequenceStopped() {
+ override fun onStopRepeating() {
_result.complete(Result3A(Result3A.Status.SUBMIT_CANCELLED))
}
- fun getDeferredResult(): Deferred<Result3A> {
- return _result
+ override fun onGraphStopped() {
+ _result.complete(Result3A(Result3A.Status.SUBMIT_CANCELLED))
+ }
+
+ override fun onGraphShutdown() {
+ _result.complete(Result3A(Result3A.Status.SUBMIT_CANCELLED))
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ProcessingQueueTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ProcessingQueueTest.kt
new file mode 100644
index 0000000..60f5233
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ProcessingQueueTest.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.core
+
+import android.os.Build
+import androidx.camera.camera2.pipe.core.ProcessingQueue.Companion.processIn
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.robolectric.annotation.Config
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(JUnit4::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ProcessingQueueTest {
+ private val testScope = TestScope()
+ private val processingScope =
+ CoroutineScope(
+ Job() +
+ StandardTestDispatcher(testScope.testScheduler) +
+ CoroutineExceptionHandler { _, throwable -> lastUncaughtException = throwable }
+ )
+
+ private var lastUncaughtException: Throwable? = null
+ private val unprocessedElements = mutableListOf<List<Int>>()
+ private val processingCalls = mutableListOf<List<Int>>()
+ private val unprocessElementHandler: (List<Int>) -> Unit = {
+ unprocessedElements.add(it.toMutableList())
+ }
+
+ @Test
+ fun processingQueueBuffersItems() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(
+ capacity = 2,
+ onUnprocessedElements = unprocessElementHandler
+ ) {}
+
+ assertThat(processingQueue.tryEmit(1)).isTrue()
+ assertThat(processingQueue.tryEmit(2)).isTrue()
+ assertThat(processingQueue.tryEmit(3)).isFalse() // Queue is full (2 items)
+ }
+
+ @Test
+ fun processInProcessesItems() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(
+ capacity = 2,
+ onUnprocessedElements = unprocessElementHandler
+ ) {
+ processingCalls.add(it.toMutableList())
+ it.removeAt(0)
+ }
+ .processIn(processingScope)
+
+ assertThat(processingQueue.tryEmit(1)).isTrue()
+ assertThat(processingQueue.tryEmit(2)).isTrue()
+ assertThat(processingQueue.tryEmit(3)).isFalse() // Queue is full
+
+ advanceUntilIdle() // Processing loop runs
+
+ // Processing loop receives [1, 2], removes 1, then is re-invoked with [2]
+ assertThat(processingCalls).containsExactly(listOf(1, 2), listOf(2))
+
+ processingScope.cancel()
+ }
+
+ @Test
+ fun processingQueueIterativelyProcessesElements() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(
+ capacity = 2,
+ onUnprocessedElements = unprocessElementHandler
+ ) {
+ processingCalls.add(it.toMutableList())
+ it.removeAt(0) // Mutation works
+ }
+ .processIn(processingScope)
+
+ processingQueue.tryEmit(1)
+ processingQueue.tryEmit(2)
+ advanceUntilIdle()
+
+ processingQueue.tryEmit(3)
+ advanceUntilIdle()
+
+ processingQueue.tryEmit(4)
+ processingQueue.tryEmit(5)
+ advanceUntilIdle()
+
+ // Processing loop run 5 times:
+ // [1, 2] (removes 1)
+ // [2] (removes 2)
+ // [3] (removes 3)
+ // [4, 5] (removes 4)
+ // [5] (removes 5)
+ assertThat(processingCalls)
+ .containsExactly(listOf(1, 2), listOf(2), listOf(3), listOf(4, 5), listOf(5))
+
+ processingScope.cancel()
+ }
+
+ @Test
+ fun processingQueueAggregatesElements() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(onUnprocessedElements = unprocessElementHandler) {
+ processingCalls.add(it.toMutableList())
+ }
+ .processIn(processingScope)
+
+ processingQueue.tryEmit(1)
+ processingQueue.tryEmit(2)
+ advanceUntilIdle()
+
+ processingQueue.tryEmit(3)
+ advanceUntilIdle()
+
+ processingQueue.tryEmit(4)
+ processingQueue.tryEmit(5)
+ advanceUntilIdle()
+
+ // Processing loop does not remove anything
+ assertThat(processingCalls)
+ .containsExactly(listOf(1, 2), listOf(1, 2, 3), listOf(1, 2, 3, 4, 5))
+
+ processingScope.cancel()
+ }
+
+ @Test
+ fun processInOnCanceledScopeInvokesOnUnprocessedElements() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(onUnprocessedElements = unprocessElementHandler) {
+ processingCalls.add(it.toMutableList())
+ it.clear()
+ }
+
+ processingQueue.tryEmit(1)
+ processingQueue.tryEmit(2)
+
+ processingScope.cancel()
+ processingQueue.processIn(processingScope)
+
+ // Processing loop does not receive anything
+ assertThat(processingCalls).isEmpty()
+ assertThat(unprocessedElements).containsExactly(listOf(1, 2))
+ }
+
+ @Test
+ fun cancellingProcessingScopeStopsProcessing() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(onUnprocessedElements = unprocessElementHandler) {
+ processingCalls.add(it.toMutableList())
+ it.clear()
+ }
+ .processIn(processingScope)
+
+ processingQueue.tryEmit(1)
+ processingQueue.tryEmit(2)
+ advanceUntilIdle()
+
+ assertThat(processingQueue.tryEmit(3)).isTrue() // Normal
+ assertThat(processingQueue.tryEmit(4)).isTrue() // Normal
+ processingScope.cancel()
+ assertThat(processingQueue.tryEmit(5)).isTrue() // Channel hasn't been closed
+ assertThat(processingQueue.tryEmit(6)).isTrue() // Channel hasn't been closed
+ advanceUntilIdle()
+
+ assertThat(processingQueue.tryEmit(7)).isFalse() // fails
+ assertThat(processingQueue.tryEmit(8)).isFalse() // fails
+
+ // Processing loop does not remove anything
+ assertThat(processingCalls)
+ .containsExactly(
+ listOf(1, 2),
+ )
+ // Processing loop does not remove anything
+ assertThat(unprocessedElements).containsExactly(listOf(3, 4, 5, 6))
+ }
+
+ @Test
+ fun longProcessingBlocksAggregateItems() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(onUnprocessedElements = unprocessElementHandler) {
+ processingCalls.add(it.toMutableList())
+ delay(100)
+ it.clear()
+ }
+ .processIn(processingScope)
+
+ processingQueue.emitChecked(1)
+ processingQueue.emitChecked(2)
+ processingQueue.emitChecked(3)
+ advanceTimeBy(50) // Triggers initial processing call
+
+ processingQueue.emitChecked(4)
+ processingQueue.emitChecked(5)
+ advanceTimeBy(25) // No updates, process function is still suspended
+
+ processingQueue.emitChecked(6)
+ advanceUntilIdle() // Last update includes all previous updates.
+
+ // Processing loop does not remove anything
+ assertThat(processingCalls)
+ .containsExactly(
+ listOf(1, 2, 3),
+ listOf(4, 5, 6),
+ )
+ processingScope.cancel()
+ }
+
+ @Test
+ fun exceptionsDuringProcessingArePropagated() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(onUnprocessedElements = unprocessElementHandler) {
+ processingCalls.add(it.toMutableList())
+ it.clear()
+ delay(100)
+ throw RuntimeException("Test")
+ }
+ .processIn(processingScope)
+
+ processingQueue.emitChecked(1)
+ processingQueue.emitChecked(2)
+ processingQueue.emitChecked(3)
+ advanceTimeBy(50) // Triggers initial processing call, but not exception
+
+ processingQueue.emitChecked(4)
+ processingQueue.emitChecked(5)
+ advanceUntilIdle() // Trigger exception.
+
+ assertThat(processingCalls).containsExactly(listOf(1, 2, 3))
+ assertThat(unprocessedElements).containsExactly(listOf(4, 5))
+ assertThat(lastUncaughtException).isInstanceOf(RuntimeException::class.java)
+ }
+
+ @Test
+ fun duplicateItemsAreNotOmitted() =
+ testScope.runTest {
+ val processingQueue =
+ ProcessingQueue<Int>(onUnprocessedElements = unprocessElementHandler) {
+ processingCalls.add(it.toMutableList())
+ it.clear()
+ }
+ .processIn(processingScope)
+
+ processingQueue.emitChecked(1)
+ processingQueue.emitChecked(1)
+ advanceUntilIdle()
+ processingQueue.emitChecked(1)
+ processingQueue.emitChecked(1)
+ processingQueue.emitChecked(1)
+ advanceUntilIdle()
+
+ assertThat(processingCalls).containsExactly(listOf(1, 1), listOf(1, 1, 1))
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
index 15db1bd..8c3b7bc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImplTest.kt
@@ -57,7 +57,11 @@
private val graphState3A = GraphState3A()
private val listener3A = Listener3A()
private val graphProcessor =
- FakeGraphProcessor(graphState3A = graphState3A, defaultListeners = listOf(listener3A))
+ FakeGraphProcessor(
+ graphState3A = graphState3A,
+ graphListener3A = listener3A,
+ defaultListeners = listOf(listener3A)
+ )
private val fakeCaptureSequenceProcessor = FakeCaptureSequenceProcessor()
private val fakeGraphRequestProcessor = GraphRequestProcessor.from(fakeCaptureSequenceProcessor)
private val controller3A =
@@ -102,12 +106,16 @@
session.startRepeating(Request(streams = listOf(StreamId(1))))
graphProcessor.invalidate()
- val result = session.lock3A(aeLockBehavior = Lock3ABehavior.IMMEDIATE)
+ val deferred = session.lock3A(aeLockBehavior = Lock3ABehavior.IMMEDIATE)
+
+ assertThat(deferred.isCompleted).isFalse()
// Don't return any results to simulate that the 3A conditions haven't been met, but the
// app calls stopRepeating(). In which case, we should fail here with SUBMIT_CANCELLED.
session.stopRepeating()
- assertThat(result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ assertThat(deferred.isCompleted).isTrue()
+ val result = deferred.await()
+ assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CaptureLimiterTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CaptureLimiterTest.kt
new file mode 100644
index 0000000..67b2c66
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CaptureLimiterTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.graph
+
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraphId
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.robolectric.annotation.Config
+
+@RunWith(JUnit4::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class CaptureLimiterTest {
+ private val testScope = TestScope()
+ private val testDispatcher = StandardTestDispatcher(testScope.testScheduler)
+ private val graphState3A = GraphState3A()
+
+ private val fakeRequestMetadata = FakeRequestMetadata()
+ private val fakeFrameInfo = FakeFrameInfo()
+
+ private val captureLimiter = CaptureLimiter(3)
+ private val cameraGraphId = CameraGraphId.nextId()
+
+ private val graphLoop =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = emptyMap<Any, Any?>(),
+ requiredParameters = emptyMap<Any, Any?>(),
+ graphListeners = listOf(),
+ graphState3A = graphState3A,
+ listeners = listOf(captureLimiter),
+ shutdownScope = testScope,
+ dispatcher = testDispatcher,
+ )
+
+ init {
+ captureLimiter.graphLoop = graphLoop
+ }
+
+ @Test
+ fun captureLimiterEnablesCaptureProcessingAfterFrameCount() {
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+
+ // ACT
+ simulateFrames(2)
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+
+ // ACT
+ simulateFrames(1)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+ }
+
+ @Test
+ fun captureLimiterResetsAfterGraphProcessorIsRemoved() {
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+
+ // ACT
+ graphLoop.requestProcessor = null
+
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+ }
+
+ @Test
+ fun captureLimiterPermanentlyDisablesAfterClose() =
+ testScope.runTest {
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isTrue()
+
+ // ACT
+ graphLoop.close()
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+ simulateFrames(3)
+ assertThat(graphLoop.captureProcessingEnabled).isFalse()
+ }
+
+ private fun simulateFrames(count: Long) {
+ for (i in 1L..count) {
+ captureLimiter.onComplete(fakeRequestMetadata, FrameNumber(i), fakeFrameInfo)
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
index 3af94c5..3d72cac 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
@@ -34,6 +34,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
@@ -231,9 +232,12 @@
// There are different conditions that can lead to the request processor not being able
// to successfully submit the desired request. For this test we are closing the processor.
graphProcessor.close()
+ advanceUntilIdle()
// Since the request processor is closed the submit3A method call will fail.
- val result = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH).await()
+ val deferred = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH)
+ assertThat(deferred.isCompleted)
+ val result = deferred.await()
assertThat(result.frameMetadata).isNull()
assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
index 0b14071..93f81ba 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
@@ -253,6 +253,6 @@
private fun initGraphProcessor() {
graphProcessor.onGraphStarted(fakeGraphRequestProcessor)
- graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
+ graphProcessor.repeatingRequest = Request(streams = listOf(StreamId(1)))
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
new file mode 100644
index 0000000..728d859
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
@@ -0,0 +1,912 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.graph
+
+import android.os.Build
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraphId
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CaptureSequence
+import androidx.camera.camera2.pipe.CaptureSequenceProcessor
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.Result3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequence
+import androidx.camera.camera2.pipe.testing.FakeMetadata.Companion.TEST_KEY
+import androidx.camera.camera2.pipe.testing.FakeSurfaces
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.robolectric.annotation.Config
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(JUnit4::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class GraphLoopTest {
+ private val testScope = TestScope()
+ private val testDispatcher = StandardTestDispatcher(testScope.testScheduler)
+ private val shutdownScope = CoroutineScope(testDispatcher)
+
+ private val graphState3A = GraphState3A()
+ private val listener3A = Listener3A()
+ private val defaultParameters = emptyMap<Any, Any?>()
+ private val requiredParameters = emptyMap<Any, Any?>()
+ private val mockListener: Request.Listener = mock<Request.Listener>()
+
+ private val fakeCameraMetadata = FakeCameraMetadata()
+ private val fakeCameraId = fakeCameraMetadata.camera
+ private val stream1 = StreamId(1)
+ private val stream2 = StreamId(2)
+ private val fakeSurfaces = FakeSurfaces()
+ private val surfaceMap =
+ mapOf(
+ stream1 to fakeSurfaces.createFakeSurface(),
+ stream2 to fakeSurfaces.createFakeSurface()
+ )
+
+ private val csp1 = SimpleCSP(fakeCameraId, surfaceMap)
+ private val csp2 = SimpleCSP(fakeCameraId, surfaceMap)
+
+ private val grp1 = GraphRequestProcessor.from(csp1)
+ private val grp2 = GraphRequestProcessor.from(csp2)
+
+ private val request1 = Request(streams = listOf(stream1))
+ private val request2 = Request(streams = listOf(stream2))
+ private val request3 = Request(streams = listOf(stream1, stream2))
+ private val cameraGraphId = CameraGraphId.nextId()
+
+ private val graphLoop =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ graphListeners = listOf(mockListener),
+ graphState3A = graphState3A,
+ listeners = listOf(listener3A),
+ shutdownScope = shutdownScope,
+ dispatcher = testDispatcher,
+ )
+
+ @After
+ fun teardown() {
+ fakeSurfaces.close()
+ shutdownScope.cancel()
+ }
+
+ @Test
+ fun graphLoopSubmitsRequests() =
+ testScope.runTest {
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ graphLoop.requestProcessor = grp1
+ assertThat(csp1.events).isEmpty()
+
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun abortRemovesPendingRequests() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.submit(listOf(request1))
+ graphLoop.abort()
+ assertThat(csp1.events).isEmpty()
+
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isAbort).isTrue()
+ }
+
+ @Test
+ fun abortBeforeRequestProcessorDoesNotInvokeAbortOnRequestProcessor() =
+ testScope.runTest {
+ graphLoop.submit(listOf(request1))
+ graphLoop.abort()
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ assertThat(csp1.events).isEmpty()
+
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun repeatingRequestsCanBeSkipped() =
+ testScope.runTest {
+ graphLoop.repeatingRequest = request1
+ graphLoop.repeatingRequest = request2
+ graphLoop.requestProcessor = grp1
+ assertThat(csp1.events).isEmpty()
+ advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+
+ graphLoop.repeatingRequest = request3
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+
+ assertThat(csp1.events[1].isRepeating).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request3)
+ }
+
+ @Test
+ fun nullRequestProcessorHaltsProcessing() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ graphLoop.requestProcessor = null
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isClose).isTrue()
+ }
+
+ @Test
+ fun nullRepeatingRequestInvokesStopRepeating() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ // Set to null and toggle to ensure only one stopRepeating event is issued.
+ graphLoop.repeatingRequest = null
+ graphLoop.repeatingRequest = request2
+ graphLoop.repeatingRequest = null
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isStopRepeating).isTrue()
+ }
+
+ @Test
+ fun repeatingAfterStopRepeatingDoesNotSkipStopRepeating() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ // Set to null and toggle to ensure only one stopRepeating event is issued.
+ graphLoop.repeatingRequest = null
+ graphLoop.repeatingRequest = request2
+ graphLoop.repeatingRequest = null
+ graphLoop.repeatingRequest = request2
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isStopRepeating).isTrue()
+
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun changingRequestProcessorsReIssuesRepeatingRequest() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ graphLoop.requestProcessor = grp2
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun changingRequestProcessorsReIssuesCaptureRequests() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ csp1.shutdown() // reject incoming requests
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+
+ graphLoop.requestProcessor = grp2
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(2)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ assertThat(csp2.events[1].isCapture).isTrue()
+ assertThat(csp2.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun capturesThatFailCanBeRetried() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ csp1.shutdown() // reject incoming requests
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ graphLoop.requestProcessor = grp2
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun closingGraphLoopAbortsPendingRequests() =
+ testScope.runTest {
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ graphLoop.close()
+
+ // Ensure close does not synchronously cause shutdown to fire.
+ verify(mockListener, never()).onAborted(request1)
+ verify(mockListener, never()).onAborted(request2)
+
+ advanceUntilIdle()
+
+ // Ensure listeners have been invoked.
+ verify(mockListener).onAborted(request1)
+ verify(mockListener).onAborted(request2)
+ }
+
+ @Test
+ fun mixedUpdatesPrioritizeRepeatingRequests() =
+ testScope.runTest {
+ graphLoop.submit(listOf(request1))
+ graphLoop.repeatingRequest = request2
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun submitParametersUsesLatestRepeatingRequest() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ graphLoop.repeatingRequest = request2
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ assertThat(csp1.events[1].isCapture).isTrue() // Capture, based on request 2, with keys
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+ }
+
+ @Test
+ fun abortCaptureIsOnlyInvokedOnActiveGraphRequestProcessor() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.submit(listOf(request1))
+ graphLoop.abort()
+ graphLoop.requestProcessor = grp2 // Change the graphRequestProcessor
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isClose).isTrue()
+ assertThat(csp1.events[1].isAbort).isTrue() // Abort is allowed to fire after close.
+
+ assertThat(csp2.events).isEmpty()
+ }
+
+ @Test
+ fun stopCaptureIsOnlyInvokedOnActiveGraphRequestProcessor() =
+ testScope.runTest {
+ graphLoop.repeatingRequest = request1
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ graphLoop.repeatingRequest = request2
+ graphLoop.repeatingRequest = null
+ graphLoop.requestProcessor = grp2 // Change the graphRequestProcessor
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[1].isClose).isTrue()
+ assertThat(csp1.events[2].isStopRepeating).isTrue() // StopRepeating is allowed to fire
+
+ assertThat(csp2.events).isEmpty()
+ }
+
+ @Test
+ fun abortAndStopDoNotPropagateToNewRequestProcessor() =
+ testScope.runTest {
+ graphLoop.repeatingRequest = request1
+ graphLoop.submit(listOf(request2))
+ graphLoop.repeatingRequest = null
+ graphLoop.abort()
+ graphLoop.submit(listOf(request3))
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request3)
+ }
+
+ @Test
+ fun stopCaptureOnlyRemovesPriorStopCapturesFromSameGraphRequestProcessor() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ graphLoop.repeatingRequest = null // issue stopCapture #1 with grp1
+ graphLoop.requestProcessor = grp2
+ graphLoop.repeatingRequest = request2
+ graphLoop.repeatingRequest = null // issue stopCapture #2 with grp2 (skip r1, r2)
+
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isClose).isTrue()
+ assertThat(csp1.events[1].isStopRepeating).isTrue()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isStopRepeating).isTrue()
+ }
+
+ @Test
+ fun submitParametersBeforeRequestProcessorUsesLatestRepeatingRequest() =
+ testScope.runTest {
+ graphLoop.repeatingRequest = request1
+ graphLoop.repeatingRequest = request2
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.repeatingRequest = request3
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request3)
+ assertThat(csp1.events[0].requiredParameters).isEmpty()
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+ }
+
+ @Test
+ fun abortWillSkipSubmitParameters() =
+ testScope.runTest {
+ graphLoop.repeatingRequest = request1
+ graphLoop.repeatingRequest = request2
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.repeatingRequest = request3
+ graphLoop.requestProcessor = grp1
+ graphLoop.abort()
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isAbort).isTrue()
+ assertThat(csp1.events[1].isRepeating).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request3)
+ assertThat(csp1.events[1].requiredParameters).isEmpty()
+ }
+
+ @Test
+ fun requestsCanBeSubmittedWithParameters() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[0].requiredParameters).isEmpty()
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ assertThat(csp1.events[2].requiredParameters).isEmpty()
+ }
+
+ @Test
+ fun defaultParametersAreAppliedToAllRequests() =
+ testScope.runTest {
+ val gl =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = mapOf<Any, Any?>(TEST_KEY to 10),
+ requiredParameters = requiredParameters,
+ graphListeners = listOf(mockListener),
+ graphState3A = graphState3A,
+ listeners = listOf(listener3A),
+ shutdownScope = shutdownScope,
+ dispatcher = testDispatcher,
+ )
+
+ gl.requestProcessor = grp1
+ gl.repeatingRequest = request1
+ gl.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ gl.submit(listOf(request2))
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[0].defaultParameters).containsEntry(TEST_KEY, 10)
+ assertThat(csp1.events[0].requiredParameters).isEmpty()
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].defaultParameters).containsEntry(TEST_KEY, 10)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ assertThat(csp1.events[2].defaultParameters).containsEntry(TEST_KEY, 10)
+ assertThat(csp1.events[2].requiredParameters).isEmpty()
+ }
+
+ @Test
+ fun requiredParametersOverrideSubmittedParameters() =
+ testScope.runTest {
+ val gl =
+ GraphLoop(
+ cameraGraphId = cameraGraphId,
+ defaultParameters = emptyMap<Any, Any?>(),
+ requiredParameters = mapOf<Any, Any?>(TEST_KEY to 10),
+ graphListeners = listOf(mockListener),
+ graphState3A = graphState3A,
+ listeners = listOf(listener3A),
+ shutdownScope = shutdownScope,
+ dispatcher = testDispatcher,
+ )
+
+ gl.requestProcessor = grp1
+ gl.repeatingRequest = request1
+ gl.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ gl.submit(listOf(request2))
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[0].defaultParameters).isEmpty()
+ assertThat(csp1.events[0].requiredParameters).containsEntry(TEST_KEY, 10)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].defaultParameters).isEmpty()
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 10)
+
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ assertThat(csp1.events[2].defaultParameters).isEmpty()
+ assertThat(csp1.events[2].requiredParameters).containsEntry(TEST_KEY, 10)
+ }
+
+ @Test
+ fun requestsSubmittedToClosedRequestProcessorAreEnqueuedToTheNextOne() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ grp1.shutdown()
+ graphLoop.repeatingRequest = request1
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+
+ graphLoop.requestProcessor = grp2
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(3)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ assertThat(csp2.events[0].requiredParameters).isEmpty()
+
+ assertThat(csp2.events[1].isCapture).isTrue()
+ assertThat(csp2.events[1].requests).containsExactly(request1)
+ assertThat(csp2.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+
+ assertThat(csp2.events[2].isCapture).isTrue()
+ assertThat(csp2.events[2].requests).containsExactly(request2)
+ assertThat(csp2.events[2].requiredParameters).isEmpty()
+ }
+
+ @Test
+ fun closingGraphLoopClosesRequestProcessor() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.close()
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
+ }
+
+ @Test
+ fun swappingRequestProcessorClosesPreviousRequestProcessor() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.requestProcessor = grp2
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
+
+ assertThat(csp2.events).isEmpty()
+ }
+
+ @Test
+ fun submitParametersUseInitialRequest() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.repeatingRequest = request2
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ assertThat(csp1.events[0].requiredParameters).isEmpty()
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1) // uses original request
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+ }
+
+ @Test
+ fun submitParametersWorksIfRepeatingRequestIsStopped() =
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.repeatingRequest = null
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isStopRepeating).isTrue()
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1) // uses original request
+ assertThat(csp1.events[1].requiredParameters).containsEntry(TEST_KEY, 42)
+ }
+
+ @Test
+ fun exceptionsAreThrown() {
+ assertThrows(RuntimeException::class.java) {
+ testScope.runTest {
+ graphLoop.requestProcessor = grp1
+ csp1.throwOnBuild = true
+ graphLoop.repeatingRequest = request1
+
+ advanceUntilIdle()
+ }
+ }
+ .hasMessageThat()
+ .contains("Test Exception")
+ }
+
+ @Test
+ fun stopRepeatingCancelsTriggers() =
+ testScope.runTest {
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+ assertThat(listener.result.isCompleted).isFalse()
+
+ graphLoop.repeatingRequest = null
+
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+
+ @Test
+ fun clearingRequestProcessorCancelsTriggers() =
+ testScope.runTest {
+ // Setup the graph loop so that the repeating request and trigger are enqueued before
+ // the graphRequestProcessor is configured. Assert that the listener is not invoked
+ // until after the requestProcessor is stopped.
+ graphLoop.repeatingRequest = request1
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+ graphLoop.submit(mapOf<Any, Any?>(TEST_KEY to 42))
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ assertThat(listener.result.isCompleted).isFalse()
+
+ graphLoop.requestProcessor = null
+ advanceUntilIdle()
+
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+
+ @Test
+ fun shutdownRequestProcessorCancelsTriggers() =
+ testScope.runTest {
+ // Arrange
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+
+ // Act
+ graphLoop.requestProcessor = null
+
+ // Assert
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+
+ @Test
+ fun swappingRequestProcessorsDoesNotCancelTriggers() {
+ testScope.runTest {
+ // Arrange
+
+ // Setup the graph loop so that the repeating request and trigger are enqueued before
+ // the graphRequestProcessor is configured. Assert that the listener is not invoked
+ // until after the requestProcessor is stopped.
+ graphLoop.requestProcessor = grp1
+ val listener = Result3AStateListenerImpl({ _ -> true }, 10, 1_000_000_000)
+ listener3A.addListener(listener)
+ graphLoop.requestProcessor = grp2 // Does not cancel trigger
+ advanceUntilIdle()
+ assertThat(listener.result.isCompleted).isFalse()
+
+ // Act
+ graphLoop.requestProcessor = null // Cancel triggers
+
+ // Assert
+ assertThat(listener.result.isCompleted).isTrue()
+ assertThat(listener.result.await().status).isEqualTo(Result3A.Status.SUBMIT_CANCELLED)
+ }
+ }
+
+ @Test
+ fun pausingCaptureProcessingPreventsCaptureRequests() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+ graphLoop.captureProcessingEnabled = false // Disable captureProcessing
+
+ // Act
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+
+ // Assert: Events are not processed
+ assertThat(csp1.events.size).isEqualTo(0)
+ }
+
+ @Test
+ fun resumingCaptureProcessingResumesCaptureRequests() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+ graphLoop.captureProcessingEnabled = false // Disable captureProcessing
+
+ // Act
+ graphLoop.submit(listOf(request1))
+ graphLoop.submit(listOf(request2))
+ advanceUntilIdle()
+ graphLoop.captureProcessingEnabled = true // Enable processing
+ advanceUntilIdle()
+
+ // Assert: Events are not processed
+ assertThat(csp1.events.size).isEqualTo(2)
+
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun disablingCaptureProcessingAllowsRepeatingRequests() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+
+ // Act
+ graphLoop.captureProcessingEnabled = false // Disable captureProcessing
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
+
+ // Assert
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun settingNullForRequestProcessorAfterCloseDoesNotCrash() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.requestProcessor = grp1
+ graphLoop.close()
+
+ // Act
+ graphLoop.requestProcessor = null
+ advanceUntilIdle()
+
+ // Assert: does not crash, and only Close is invoked.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
+ }
+
+ @Test
+ fun settingRequestProcessorAfterCloseCausesRequestProcessorToBeShutdown() =
+ testScope.runTest {
+ // Arrange
+ graphLoop.close()
+
+ // Act
+ graphLoop.requestProcessor = grp1
+ advanceUntilIdle()
+
+ // Assert: Does not crash, and request processor is closed.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
+ }
+
+ @Test
+ fun settingRequestProcessorAfterShutdownCausesRequestProcessorToBeShutdown() =
+ testScope.runTest {
+ // Arrange
+
+ graphLoop.requestProcessor = grp1
+ graphLoop.close()
+ advanceUntilIdle() // Shutdown fully completes
+
+ // Act
+ graphLoop.requestProcessor = grp2
+ advanceUntilIdle()
+
+ // Assert: Does not crash, and request processor is closed.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isClose).isTrue()
+ }
+
+ private val SimpleCSP.SimpleCSPEvent.requests: List<Request>
+ get() = (this as SimpleCSP.Submit).captureSequence.captureRequestList
+
+ private val SimpleCSP.SimpleCSPEvent.requiredParameters: Map<*, Any?>
+ get() = (this as SimpleCSP.Submit).captureSequence.requiredParameters
+
+ private val SimpleCSP.SimpleCSPEvent.defaultParameters: Map<*, Any?>
+ get() = (this as SimpleCSP.Submit).captureSequence.defaultParameters
+
+ private val SimpleCSP.SimpleCSPEvent.isRepeating: Boolean
+ get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating ?: false
+
+ private val SimpleCSP.SimpleCSPEvent.isCapture: Boolean
+ get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating == false
+
+ private val SimpleCSP.SimpleCSPEvent.isAbort: Boolean
+ get() = this is SimpleCSP.AbortCaptures
+
+ private val SimpleCSP.SimpleCSPEvent.isStopRepeating: Boolean
+ get() = this is SimpleCSP.StopRepeating
+
+ private val SimpleCSP.SimpleCSPEvent.isClose: Boolean
+ get() = this is SimpleCSP.Close
+
+ internal class SimpleCSP(
+ private val cameraId: CameraId,
+ private val surfaceMap: Map<StreamId, Surface>
+ ) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
+ val events = mutableListOf<SimpleCSPEvent>()
+ var throwOnBuild = false
+ private var closed = false
+ private val sequenceIds = atomic(0)
+
+ override fun build(
+ isRepeating: Boolean,
+ requests: List<Request>,
+ defaultParameters: Map<*, Any?>,
+ requiredParameters: Map<*, Any?>,
+ listeners: List<Request.Listener>,
+ sequenceListener: CaptureSequence.CaptureSequenceListener
+ ): FakeCaptureSequence? {
+ if (closed) return null
+ if (throwOnBuild) throw RuntimeException("Test Exception")
+ return FakeCaptureSequence.create(
+ cameraId = cameraId,
+ repeating = isRepeating,
+ requests = requests,
+ surfaceMap = surfaceMap,
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ listeners = listeners,
+ sequenceListener = sequenceListener
+ )
+ }
+
+ override fun abortCaptures() {
+ events.add(AbortCaptures)
+ }
+
+ override fun stopRepeating() {
+ events.add(StopRepeating)
+ }
+
+ override suspend fun shutdown() {
+ closed = true
+ events.add(Close)
+ }
+
+ override fun submit(captureSequence: FakeCaptureSequence): Int? {
+ if (!closed) {
+ events.add(Submit(captureSequence))
+ return sequenceIds.incrementAndGet()
+ }
+ return null
+ }
+
+ sealed class SimpleCSPEvent
+
+ object Close : SimpleCSPEvent()
+
+ object StopRepeating : SimpleCSPEvent()
+
+ object AbortCaptures : SimpleCSPEvent()
+
+ data class Submit(val captureSequence: FakeCaptureSequence) : SimpleCSPEvent()
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index d95aa17..13e29b9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -33,9 +33,9 @@
import androidx.camera.camera2.pipe.testing.FakeRequestListener
import androidx.camera.camera2.pipe.testing.FakeThreads
import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
@@ -54,6 +54,7 @@
internal class GraphProcessorTest {
private val globalListener = FakeRequestListener()
private val graphState3A = GraphState3A()
+ private val graphListener3A = Listener3A()
private val streamId = StreamId(0)
private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
@@ -82,7 +83,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -104,7 +105,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -128,7 +129,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -156,7 +157,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -176,7 +177,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -208,7 +209,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
@@ -246,13 +247,13 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
- graphProcessor.startRepeating(request2)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.repeatingRequest = request2
advanceUntilIdle()
val event =
@@ -271,19 +272,19 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
fakeProcessor1.rejectRequests = true
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
val event1 = fakeProcessor1.nextEvent()
assertThat(event1.rejected).isTrue()
assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
- graphProcessor.startRepeating(request2)
+ graphProcessor.repeatingRequest = request2
val event2 = fakeProcessor1.nextEvent()
assertThat(event2.rejected).isTrue()
fakeProcessor1.awaitEvent(request = request2) {
@@ -291,7 +292,7 @@
}
fakeProcessor1.rejectRequests = false
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.invalidate()
fakeProcessor1.awaitEvent(request = request2) {
it.submit && it.requestSequence?.repeating == true
@@ -306,12 +307,12 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
advanceUntilIdle()
fakeProcessor1.awaitEvent(request = request1) {
@@ -334,13 +335,13 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
fakeProcessor1.rejectRequests = true
graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
fakeProcessor1.awaitEvent(request = request1) { it.rejected }
graphProcessor.onGraphStarted(graphRequestProcessor2)
@@ -357,11 +358,11 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
graphProcessor.submit(request2)
delay(50)
@@ -393,11 +394,11 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
graphProcessor.submit(request2)
// Abort queued and in-flight requests.
@@ -426,14 +427,15 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.close()
+ advanceUntilIdle()
// Abort queued and in-flight requests.
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.startRepeating(request1)
+ // graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.repeatingRequest = request1
graphProcessor.submit(request2)
val abortEvent1 =
@@ -441,8 +443,6 @@
val abortEvent2 = requestListener2.onAbortedFlow.first()
assertThat(abortEvent1).isNull()
assertThat(abortEvent2.request).isSameInstanceAs(request2)
-
- assertThat(fakeProcessor1.nextEvent().close).isTrue()
}
@Test
@@ -453,23 +453,25 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
// Submit a repeating request first to make sure we have one in progress.
- graphProcessor.startRepeating(request1)
+ graphProcessor.repeatingRequest = request1
advanceUntilIdle()
- val result = async {
- graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- }
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
advanceUntilIdle()
graphProcessor.onGraphStarted(graphRequestProcessor1)
advanceUntilIdle()
-
- assertThat(result.await()).isTrue()
+ val event1 = fakeProcessor1.nextEvent()
+ assertThat(event1.requestSequence?.repeating).isTrue()
+ val event2 = fakeProcessor1.nextEvent()
+ assertThat(event2.requestSequence?.repeating).isFalse()
+ assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
+ .isFalse()
}
@Test
@@ -480,21 +482,14 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
// Submit a repeating request first to make sure we have one in progress.
- graphProcessor.startRepeating(request1)
- advanceUntilIdle()
-
- val result1 = async {
- graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- }
- advanceUntilIdle()
- val result2 = async {
- graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
- }
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
advanceUntilIdle()
graphProcessor.onGraphStarted(graphRequestProcessor1)
@@ -505,10 +500,11 @@
val event2 = fakeProcessor1.nextEvent()
assertThat(event2.requestSequence?.repeating).isFalse()
assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
+ .isFalse()
+ val event3 = fakeProcessor1.nextEvent()
+ assertThat(event3.requestSequence?.repeating).isFalse()
+ assertThat(event3.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
.isTrue()
-
- assertThat(result1.await()).isFalse()
- assertThat(result2.await()).isTrue()
}
@Test
@@ -519,16 +515,16 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
graphProcessor.onGraphStarted(graphRequestProcessor1)
advanceUntilIdle()
- val result =
- graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
- assertThat(result).isFalse()
+ assertThrows<IllegalStateException> {
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ }
}
@Test
@@ -539,7 +535,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
@@ -559,7 +555,7 @@
CameraGraphId.nextId(),
FakeGraphConfigs.graphConfig,
graphState3A,
- this,
+ graphListener3A,
arrayListOf(globalListener)
)
assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
index 93d4a6e..dbeb67d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphTestContext.kt
@@ -33,7 +33,7 @@
init {
captureSequenceProcessor.surfaceMap = surfaceMap
graphProcessor.onGraphStarted(graphRequestProcessor)
- graphProcessor.startRepeating(Request(streams = listOf(streamId)))
+ graphProcessor.repeatingRequest = Request(streams = listOf(streamId))
}
override fun close() {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 0137588..4205aef 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -27,14 +27,17 @@
import androidx.camera.camera2.pipe.graph.GraphProcessor
import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
import androidx.camera.camera2.pipe.graph.GraphState3A
+import androidx.camera.camera2.pipe.graph.Listener3A
import androidx.camera.camera2.pipe.putAllMetadata
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.runBlocking
/** Fake implementation of a [GraphProcessor] for tests. */
internal class FakeGraphProcessor(
val graphState3A: GraphState3A = GraphState3A(),
+ val graphListener3A: Listener3A = Listener3A(),
val defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
val defaultListeners: List<Request.Listener> = emptyList()
) : GraphProcessor, GraphListener {
@@ -44,8 +47,15 @@
var closed = false
private set
- var repeatingRequest: Request? = null
- private set
+ private var _repeatingRequest: Request? = null
+ override var repeatingRequest: Request?
+ get() = _repeatingRequest
+ set(value) {
+ _repeatingRequest = value
+ if (value == null) {
+ graphListener3A.onStopRepeating()
+ }
+ }
val requestQueue: List<List<Request>>
get() = _requestQueue
@@ -58,29 +68,17 @@
override val graphState: StateFlow<GraphState>
get() = _graphState
- override fun startRepeating(request: Request) {
- repeatingRequest = request
- }
+ override fun submit(request: Request): Boolean = submit(listOf(request))
- override fun stopRepeating() {
- repeatingRequest = null
- }
-
- override fun hasRepeatingRequest() = repeatingRequest != null
-
- override fun submit(request: Request) {
- submit(listOf(request))
- }
-
- override fun submit(requests: List<Request>) {
+ override fun submit(requests: List<Request>): Boolean {
+ if (closed) return false
_requestQueue.add(requests)
+ return true
}
- override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean {
- if (closed) {
- return false
- }
- if (repeatingRequest == null) return false
+ override fun submit(parameters: Map<*, Any?>): Boolean {
+ check(repeatingRequest != null)
+ if (closed) return false
val currProcessor = processor
val currRepeatingRequest = repeatingRequest
@@ -88,22 +86,37 @@
requiredParameters.putAllMetadata(parameters)
graphState3A.writeTo(requiredParameters)
- return when {
- currProcessor == null || currRepeatingRequest == null -> false
- else ->
- currProcessor.submit(
- isRepeating = false,
- requests = listOf(currRepeatingRequest),
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = defaultListeners
- )
+ if (currProcessor != null && currRepeatingRequest != null) {
+ currProcessor.submit(
+ isRepeating = false,
+ requests = listOf(currRepeatingRequest),
+ defaultParameters = defaultParameters,
+ requiredParameters = requiredParameters,
+ listeners = defaultListeners
+ )
}
+ return true
}
override fun abort() {
+ val requests = _requestQueue.toList()
_requestQueue.clear()
- // TODO: Invoke abort on the listeners in the queue.
+
+ for (burst in requests) {
+ for (request in burst) {
+ for (listener in defaultListeners) {
+ listener.onAborted(request)
+ }
+ }
+ }
+
+ for (burst in requests) {
+ for (request in burst) {
+ for (listener in request.listeners) {
+ listener.onAborted(request)
+ }
+ }
+ }
}
override fun close() {
@@ -113,6 +126,7 @@
closed = true
active = false
_requestQueue.clear()
+ graphListener3A.onGraphShutdown()
}
override fun onGraphStarting() {
@@ -123,11 +137,12 @@
_graphState.value = GraphStateStarted
val old = processor
processor = requestProcessor
- old?.close()
+ runBlocking { old?.shutdown() }
}
override fun onGraphStopping() {
_graphState.value = GraphStateStopping
+ graphListener3A.onGraphStopped()
}
override fun onGraphStopped(requestProcessor: GraphRequestProcessor?) {
@@ -136,7 +151,7 @@
val old = processor
if (requestProcessor === old) {
processor = null
- old.close()
+ runBlocking { old.shutdown() }
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
index 8c4b838..11779e4 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/UpdateCounting3AStateListener.kt
@@ -37,5 +37,9 @@
return listener.update(requestNumber, frameMetadata)
}
- override fun onRequestSequenceStopped() {}
+ override fun onStopRepeating() {}
+
+ override fun onGraphStopped() {}
+
+ override fun onGraphShutdown() {}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/AndroidRZoomImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/AndroidRZoomImpl.java
index f9c6863..c30da9b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/AndroidRZoomImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/AndroidRZoomImpl.java
@@ -27,6 +27,7 @@
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.params.CaptureRequestParameterCompat;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraControl;
import androidx.camera.core.impl.Config;
@@ -41,11 +42,13 @@
private float mCurrentZoomRatio = DEFAULT_ZOOM_RATIO;
private CallbackToFutureAdapter.Completer<Void> mPendingZoomRatioCompleter;
private float mPendingZoomRatio = 1.0f;
+ private boolean mShouldOverrideZoom = false;
AndroidRZoomImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
mCameraCharacteristics = cameraCharacteristics;
mZoomRatioRange = mCameraCharacteristics
.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
+ mShouldOverrideZoom = mCameraCharacteristics.isZoomOverrideAvailable();
}
@Override
@@ -63,6 +66,10 @@
public void addRequestOption(@NonNull Camera2ImplConfig.Builder builder) {
builder.setCaptureRequestOptionWithPriority(CaptureRequest.CONTROL_ZOOM_RATIO,
mCurrentZoomRatio, Config.OptionPriority.REQUIRED);
+ if (mShouldOverrideZoom) {
+ CaptureRequestParameterCompat.setSettingsOverrideZoom(builder,
+ Config.OptionPriority.REQUIRED);
+ }
}
@Override
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index 15934dd..2333562 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -17,6 +17,7 @@
package androidx.camera.camera2.internal.compat;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Build;
@@ -72,11 +73,8 @@
* caching it.
*/
private boolean isKeyNonCacheable(@NonNull CameraCharacteristics.Key<?> key) {
- // SENSOR_ORIENTATION value scould change in some circumstances.
- if (key.equals(CameraCharacteristics.SENSOR_ORIENTATION)) {
- return true;
- }
- return false;
+ // SENSOR_ORIENTATION value should change in some circumstances.
+ return key.equals(CameraCharacteristics.SENSOR_ORIENTATION);
}
/**
@@ -121,6 +119,24 @@
}
/**
+ * Returns {@code true} if overriding zoom setting is available, otherwise {@code false}.
+ */
+ public boolean isZoomOverrideAvailable() {
+ if (Build.VERSION.SDK_INT >= 34) {
+ int[] availableSettingsOverrides = mCameraCharacteristicsImpl.get(
+ CameraCharacteristics.CONTROL_AVAILABLE_SETTINGS_OVERRIDES);
+ if (availableSettingsOverrides != null) {
+ for (int i : availableSettingsOverrides) {
+ if (i == CameraMetadata.CONTROL_SETTINGS_OVERRIDE_ZOOM) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
* Obtains the {@link StreamConfigurationMapCompat} which contains the output sizes related
* workarounds in it.
*/
@@ -170,13 +186,13 @@
*/
public interface CameraCharacteristicsCompatImpl {
/**
- * Gets the key/values from the CameraCharacteristics .
+ * Gets the key/values from the CameraCharacteristics.
*/
@Nullable
<T> T get(@NonNull CameraCharacteristics.Key<T> key);
/**
- * Get physical camera ids.
+ * Gets physical camera ids.
*/
@NonNull
Set<String> getPhysicalCameraIds();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/CaptureRequestParameterCompat.kt b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/CaptureRequestParameterCompat.kt
new file mode 100644
index 0000000..6b55e40
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/CaptureRequestParameterCompat.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.params
+
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import androidx.annotation.OptIn
+import androidx.camera.camera2.impl.Camera2ImplConfig
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.camera.core.impl.Config.OptionPriority
+
+/** Helper for accessing features in [CaptureRequest] in a backwards compatible fashion. */
+internal object CaptureRequestParameterCompat {
+ /** Sets the [CaptureRequest.CONTROL_SETTINGS_OVERRIDE_ZOOM] option if supported. */
+ @OptIn(ExperimentalCamera2Interop::class)
+ @JvmStatic
+ fun setSettingsOverrideZoom(options: Camera2ImplConfig.Builder, priority: OptionPriority) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ options.setCaptureRequestOptionWithPriority(
+ CaptureRequest.CONTROL_SETTINGS_OVERRIDE,
+ CameraMetadata.CONTROL_SETTINGS_OVERRIDE_ZOOM,
+ priority
+ )
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
index 28fe77d..2f1ae09 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
@@ -27,9 +27,9 @@
/**
* <p>QuirkSummary
- * Bug Id: 252818931, 261744070, 319913852
- * Description: On certain devices, the captured image has color issue for reprocessing. We
- * need to disable zero-shutter lag and return false for
+ * Bug Id: 252818931, 261744070, 319913852, 361328838
+ * Description: On certain devices, the captured image has color or zoom freezing issue for
+ * reprocessing. We need to disable zero-shutter lag and return false for
* {@link CameraInfo#isZslSupported()}.
* Device(s): Samsung Fold4, Samsung s22, Xiaomi Mi 8
*/
@@ -39,7 +39,9 @@
"SM-F936",
"SM-S901U",
"SM-S908U",
- "SM-S908U1"
+ "SM-S908U1",
+ "SM-F721U1",
+ "SM-S928U1"
);
private static final List<String> AFFECTED_XIAOMI_MODEL = Arrays.asList(
diff --git a/camera/camera-compose/api/current.txt b/camera/camera-compose/api/current.txt
new file mode 100644
index 0000000..18c7f03
--- /dev/null
+++ b/camera/camera-compose/api/current.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.camera.compose {
+
+ public final class CameraXViewfinderKt {
+ method @androidx.compose.runtime.Composable public static void CameraXViewfinder(androidx.camera.core.SurfaceRequest surfaceRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.camera.viewfinder.surface.ImplementationMode implementationMode, optional androidx.camera.viewfinder.compose.MutableCoordinateTransformer? coordinateTransformer);
+ }
+
+}
+
diff --git a/activity/activity-compose/api/res-1.10.0-beta01.txt b/camera/camera-compose/api/res-current.txt
similarity index 100%
copy from activity/activity-compose/api/res-1.10.0-beta01.txt
copy to camera/camera-compose/api/res-current.txt
diff --git a/camera/camera-compose/api/restricted_current.txt b/camera/camera-compose/api/restricted_current.txt
new file mode 100644
index 0000000..18c7f03
--- /dev/null
+++ b/camera/camera-compose/api/restricted_current.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.camera.compose {
+
+ public final class CameraXViewfinderKt {
+ method @androidx.compose.runtime.Composable public static void CameraXViewfinder(androidx.camera.core.SurfaceRequest surfaceRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.camera.viewfinder.surface.ImplementationMode implementationMode, optional androidx.camera.viewfinder.compose.MutableCoordinateTransformer? coordinateTransformer);
+ }
+
+}
+
diff --git a/camera/camera-compose/build.gradle b/camera/camera-compose/build.gradle
new file mode 100644
index 0000000..337ad6c
--- /dev/null
+++ b/camera/camera-compose/build.gradle
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(project(":camera:camera-core"))
+ // TODO(b/357895362): Switch to pinned dependencies when stable is released
+ api(project(":camera:viewfinder:viewfinder-compose"))
+ api(project(":camera:viewfinder:viewfinder-core"))
+ implementation("androidx.compose.foundation:foundation-layout:1.6.1")
+ implementation("androidx.compose.foundation:foundation:1.6.1")
+ implementation("androidx.compose.runtime:runtime:1.6.1")
+ implementation("androidx.core:core-ktx:1.12.0")
+
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":camera:camera-camera2"))
+ androidTestImplementation(project(":camera:camera-camera2-pipe-integration"))
+ androidTestImplementation(project(":camera:camera-lifecycle"))
+ androidTestImplementation(project(":camera:camera-testing")) {
+ // Ensure camera-testing does not pull in androidx.test dependencies
+ exclude(group:"androidx.test")
+ }
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.1")
+ androidTestImplementation("androidx.compose.ui:ui-test-manifest:1.6.1")
+}
+
+android {
+ compileSdk 35
+ namespace "androidx.camera.compose"
+ // TODO(b/349411310): Remove once we can update runtime to 1.7.0
+ experimentalProperties["android.lint.useK2Uast"] = false
+}
+
+androidx {
+ name = "Camera Compose"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "Jetpack Compose tools for users of the Jetpack Camera camera-core library"
+}
diff --git a/camera/camera-compose/samples/build.gradle b/camera/camera-compose/samples/build.gradle
new file mode 100644
index 0000000..a2d5268
--- /dev/null
+++ b/camera/camera-compose/samples/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api("androidx.annotation:annotation:1.8.1")
+ implementation(libs.kotlinStdlib)
+ implementation(project(":camera:camera-compose"))
+ implementation(project(":camera:camera-core"))
+ implementation("androidx.compose.foundation:foundation:1.6.8")
+ implementation("androidx.compose.runtime:runtime:1.6.8")
+ implementation("androidx.compose.ui:ui:1.6.8")
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.4")
+ compileOnly(project(":annotation:annotation-sampled"))
+}
+
+android {
+ compileSdk 35
+ namespace "androidx.camera.compose.samples"
+}
+
+androidx {
+ name = "Camera Compose Samples"
+ type = LibraryType.SAMPLES
+ inceptionYear = "2024"
+ description = "Contains sample code for the Androidx Camera Compose library"
+}
diff --git a/camera/camera-compose/samples/src/main/java/androidx/camera/compose/samples/CameraXViewfinderSamples.kt b/camera/camera-compose/samples/src/main/java/androidx/camera/compose/samples/CameraXViewfinderSamples.kt
new file mode 100644
index 0000000..82d2ba0
--- /dev/null
+++ b/camera/camera-compose/samples/src/main/java/androidx/camera/compose/samples/CameraXViewfinderSamples.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.compose.samples
+
+import android.util.Size
+import androidx.annotation.Sampled
+import androidx.camera.compose.CameraXViewfinder
+import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
+import androidx.camera.viewfinder.surface.ImplementationMode
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@Suppress("unused", "UNUSED_PARAMETER")
+@Sampled
+fun CameraXViewfinderSample() {
+ class PreviewViewModel : ViewModel() {
+ private val _surfaceRequests = MutableStateFlow<SurfaceRequest?>(null)
+
+ val surfaceRequests: StateFlow<SurfaceRequest?>
+ get() = _surfaceRequests.asStateFlow()
+
+ private fun produceSurfaceRequests(previewUseCase: Preview) {
+ // Always publish new SurfaceRequests from Preview
+ previewUseCase.setSurfaceProvider { newSurfaceRequest ->
+ _surfaceRequests.value = newSurfaceRequest
+ }
+ }
+
+ fun focusOnPoint(surfaceBounds: Size, x: Float, y: Float) {
+ // Create point for CameraX's CameraControl.startFocusAndMetering() and submit...
+ }
+
+ // ...
+ }
+
+ @Composable
+ fun MyCameraViewfinder(viewModel: PreviewViewModel, modifier: Modifier = Modifier) {
+ val currentSurfaceRequest: SurfaceRequest? by viewModel.surfaceRequests.collectAsState()
+
+ currentSurfaceRequest?.let { surfaceRequest ->
+
+ // CoordinateTransformer for transforming from Offsets to Surface coordinates
+ val coordinateTransformer = remember { MutableCoordinateTransformer() }
+
+ CameraXViewfinder(
+ surfaceRequest = surfaceRequest,
+ implementationMode = ImplementationMode.EXTERNAL, // Can also use EMBEDDED
+ modifier =
+ modifier.pointerInput(Unit) {
+ detectTapGestures {
+ with(coordinateTransformer) {
+ val surfaceCoords = it.transform()
+ viewModel.focusOnPoint(
+ surfaceRequest.resolution,
+ surfaceCoords.x,
+ surfaceCoords.y
+ )
+ }
+ }
+ },
+ coordinateTransformer = coordinateTransformer
+ )
+ }
+ }
+}
diff --git a/camera/camera-compose/src/androidTest/java/androidx.camera.compose/CameraXViewfinderTest.kt b/camera/camera-compose/src/androidTest/java/androidx.camera.compose/CameraXViewfinderTest.kt
new file mode 100644
index 0000000..3c53f75
--- /dev/null
+++ b/camera/camera-compose/src/androidTest/java/androidx.camera.compose/CameraXViewfinderTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.compose
+
+import android.content.Context
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.viewfinder.surface.ImplementationMode
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.concurrent.futures.await
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.resume
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.produceIn
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class CameraXViewfinderTest(private val implName: String, private val cameraConfig: CameraXConfig) {
+ @get:Rule
+ val cameraPipeConfigTestRule =
+ CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ )
+
+ @get:Rule
+ val useCamera =
+ CameraUtil.grantCameraPermissionAndPreTestAndPostTest(PreTestCameraIdList(cameraConfig))
+
+ @get:Rule val composeTest = createComposeRule()
+
+ @Test
+ fun viewfinderIsDisplayed_withValidSurfaceRequest() = runViewfinderTest {
+ composeTest.setContent {
+ val currentSurfaceRequest: SurfaceRequest? by surfaceRequests.collectAsState()
+ currentSurfaceRequest?.let { surfaceRequest ->
+ CameraXViewfinder(
+ surfaceRequest = surfaceRequest,
+ modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
+ )
+ }
+ }
+
+ // Start the camera
+ startCamera()
+
+ // Wait for first SurfaceRequest
+ surfaceRequests.filterNotNull().first()
+
+ composeTest.awaitIdle()
+
+ // CameraXViewfinder should now have a child Viewfinder
+ composeTest
+ .onNodeWithTag(CAMERAX_VIEWFINDER_TEST_TAG)
+ .assertIsDisplayed()
+ .assert(SemanticsMatcher.hasChild())
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ @Test
+ fun changingImplementation_sendsNewSurfaceRequest() = runViewfinderTest {
+ var implementationMode: ImplementationMode by mutableStateOf(ImplementationMode.EXTERNAL)
+ composeTest.setContent {
+ val currentSurfaceRequest: SurfaceRequest? by surfaceRequests.collectAsState()
+ currentSurfaceRequest?.let { surfaceRequest ->
+ CameraXViewfinder(
+ surfaceRequest = surfaceRequest,
+ implementationMode = implementationMode,
+ modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
+ )
+ }
+ }
+
+ // Collect expected number of SurfaceRequests for 2 mode changes
+ val surfaceRequestSequence = surfaceRequests.filterNotNull().take(3).produceIn(this)
+
+ // Start the camera
+ startCamera()
+
+ // Swap implementation modes twice to produce 3 SurfaceRequests
+ val allSurfaceRequests = buildList {
+ for (surfaceRequest in surfaceRequestSequence) {
+ add(surfaceRequest)
+ composeTest.awaitIdle()
+
+ if (!surfaceRequestSequence.isClosedForReceive) {
+ // Changing the implementation mode will invalidate the previous SurfaceRequest
+ // and cause Preview to send a new SurfaceRequest
+ implementationMode = implementationMode.swapMode()
+ composeTest.awaitIdle()
+ }
+ }
+ }
+
+ assertThat(allSurfaceRequests.size).isEqualTo(3)
+ assertThat(allSurfaceRequests).containsNoDuplicates()
+ }
+
+ @Test
+ fun cancelledSurfaceRequest_doesNotInstantiateViewfinder() = runViewfinderTest {
+ // Start the camera
+ startCamera()
+
+ // Wait for first SurfaceRequest
+ val surfaceRequest = surfaceRequests.filterNotNull().first()
+
+ // Reset surface provider to cause cancellation of the last SurfaceRequest
+ resetPreviewSurfaceProvider()
+
+ // Ensure the SurfaceRequest is cancelled
+ surfaceRequest.awaitCancellation()
+
+ // Pass on cancelled SurfaceRequest to CameraXViewfinder
+ composeTest.setContent {
+ CameraXViewfinder(
+ surfaceRequest = surfaceRequest,
+ modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
+ )
+ }
+
+ composeTest.awaitIdle()
+
+ // Viewfinder should not be displayed since SurfaceRequest was cancelled
+ composeTest.onNodeWithTag(CAMERAX_VIEWFINDER_TEST_TAG).assertIsNotDisplayed()
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() =
+ listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+
+ private const val CAMERAX_VIEWFINDER_TEST_TAG = "CameraXViewfinderTestTag"
+ }
+
+ private inline fun runViewfinderTest(crossinline block: suspend PreviewTestScope.() -> Unit) =
+ runBlocking {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraProvider =
+ withTimeout(10.seconds) {
+ ProcessCameraProvider.configureInstance(cameraConfig)
+ ProcessCameraProvider.getInstance(context).await()
+ }
+
+ var fakeLifecycleOwner: FakeLifecycleOwner? = null
+ try {
+ val preview = Preview.Builder().build()
+ val surfaceRequests = MutableStateFlow<SurfaceRequest?>(null)
+ val resetPreviewSurfaceProvider =
+ suspend {
+ withContext(Dispatchers.Main) {
+ // Reset the surface provider to a new lambda that will continue to
+ // publish to surfaceRequests
+ preview.setSurfaceProvider { surfaceRequest ->
+ surfaceRequests.value = surfaceRequest
+ }
+ }
+ }
+ .also { it.invoke() }
+
+ val startCamera = suspend {
+ withContext(Dispatchers.Main) {
+ val lifecycleOwner =
+ FakeLifecycleOwner().apply {
+ startAndResume()
+ fakeLifecycleOwner = this
+ }
+
+ val firstAvailableCameraSelector =
+ cameraProvider.availableCameraInfos
+ .asSequence()
+ .map { it.cameraSelector }
+ .first()
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ firstAvailableCameraSelector,
+ preview
+ )
+ }
+ }
+
+ with(
+ PreviewTestScope(
+ surfaceRequests = surfaceRequests.asStateFlow(),
+ resetPreviewSurfaceProvider = resetPreviewSurfaceProvider,
+ startCamera = startCamera,
+ coroutineContext = coroutineContext
+ )
+ ) {
+ block()
+ }
+ } finally {
+ fakeLifecycleOwner?.apply {
+ withContext(Dispatchers.Main) {
+ pauseAndStop()
+ destroy()
+ }
+ }
+ withTimeout(30.seconds) { cameraProvider.shutdownAsync().await() }
+ }
+ }
+
+ private data class PreviewTestScope(
+ val surfaceRequests: StateFlow<SurfaceRequest?>,
+ val resetPreviewSurfaceProvider: suspend () -> Unit,
+ val startCamera: suspend () -> Camera,
+ override val coroutineContext: CoroutineContext
+ ) : CoroutineScope
+}
+
+private fun ImplementationMode.swapMode(): ImplementationMode {
+ return when (this) {
+ ImplementationMode.EXTERNAL -> ImplementationMode.EMBEDDED
+ ImplementationMode.EMBEDDED -> ImplementationMode.EXTERNAL
+ }
+}
+
+private fun SemanticsMatcher.Companion.hasChild() =
+ SemanticsMatcher("Has child") { node -> node.children.isNotEmpty() }
+
+private suspend fun SurfaceRequest.awaitCancellation(): Unit = suspendCancellableCoroutine { cont ->
+ addRequestCancellationListener(Runnable::run) { cont.resume(Unit) }
+}
diff --git a/camera/camera-compose/src/main/java/androidx/camera/androidx-camera-camera-compose-documentation.md b/camera/camera-compose/src/main/java/androidx/camera/androidx-camera-camera-compose-documentation.md
new file mode 100644
index 0000000..bc5cbbc
--- /dev/null
+++ b/camera/camera-compose/src/main/java/androidx/camera/androidx-camera-camera-compose-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+Camera Compose
+
+# Package androidx.camera.compose
+
+Jetpack Compose tools for users of the Jetpack Camera camera-core library
diff --git a/camera/camera-compose/src/main/java/androidx/camera/compose/CameraXViewfinder.kt b/camera/camera-compose/src/main/java/androidx/camera/compose/CameraXViewfinder.kt
new file mode 100644
index 0000000..cfc1f06
--- /dev/null
+++ b/camera/camera-compose/src/main/java/androidx/camera/compose/CameraXViewfinder.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.compose
+
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.SurfaceRequest.TransformationInfo as CXTransformationInfo
+import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
+import androidx.camera.viewfinder.compose.Viewfinder
+import androidx.camera.viewfinder.surface.ImplementationMode
+import androidx.camera.viewfinder.surface.TransformationInfo
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.launch
+
+/**
+ * An adapter composable that displays frames from CameraX by completing provided [SurfaceRequest]s.
+ *
+ * This is a wrapper around [Viewfinder] that will convert a CameraX [SurfaceRequest] internally
+ * into a [ViewfinderSurfaceRequest]. Additionally, all interactions normally handled through the
+ * [ViewfinderSurfaceRequest] will be derived from the [SurfaceRequest].
+ *
+ * If [implementationMode] is changed while the provided [surfaceRequest] has been fulfilled, the
+ * surface request will be invalidated as if [SurfaceRequest.invalidate] has been called. This will
+ * allow CameraX to know that a new surface request is required since the underlying viewfinder
+ * implementation will be providing a new surface.
+ *
+ * Example usage:
+ *
+ * @sample androidx.camera.compose.samples.CameraXViewfinderSample
+ * @param surfaceRequest The surface request from CameraX
+ * @param modifier The [Modifier] to be applied to this viewfinder
+ * @param implementationMode The [ImplementationMode] to be used by this viewfinder.
+ * @param coordinateTransformer The [MutableCoordinateTransformer] used to map offsets of this
+ * viewfinder to the source coordinates of the data being provided to the surface that fulfills
+ * [surfaceRequest]
+ */
+@Composable
+public fun CameraXViewfinder(
+ surfaceRequest: SurfaceRequest,
+ modifier: Modifier = Modifier,
+ implementationMode: ImplementationMode = ImplementationMode.EXTERNAL,
+ coordinateTransformer: MutableCoordinateTransformer? = null
+) {
+ val currentImplementationMode by rememberUpdatedState(implementationMode)
+
+ val viewfinderArgs by
+ produceState<ViewfinderArgs?>(initialValue = null, surfaceRequest) {
+ // Convert the CameraX SurfaceRequest to ViewfinderSurfaceRequest. There should
+ // always be a 1:1 mapping of CameraX SurfaceRequest to ViewfinderSurfaceRequest.
+ val viewfinderSurfaceRequest =
+ ViewfinderSurfaceRequest.Builder(surfaceRequest.resolution).build()
+
+ // Launch undispatched so we always reach the try/finally in this coroutine
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ try {
+ // Forward request cancellation to the ViewfinderSurfaceRequest by marking it
+ // safe to release and cancelling this produceScope in case we haven't yet
+ // produced a complete ViewfinderArgs.
+ surfaceRequest.addRequestCancellationListener(Runnable::run) {
+ // This SurfaceRequest doesn't need to be completed, so let the
+ // Viewfinder know in case it has already generated a Surface.
+ viewfinderSurfaceRequest.markSurfaceSafeToRelease()
+ // Also complete the ViewfinderSurfaceRequest from the producer side
+ // in case we never sent it to the Viewfinder.
+ viewfinderSurfaceRequest.willNotProvideSurface()
+ [email protected]()
+ }
+
+ // Suspend until we retrieve the Surface
+ val surface = viewfinderSurfaceRequest.getSurface()
+ // Provide the surface and mark safe to release once the
+ // frame producer is finished.
+ surfaceRequest.provideSurface(surface, Runnable::run) {
+ viewfinderSurfaceRequest.markSurfaceSafeToRelease()
+ }
+ } finally {
+ // If we haven't provided the surface, such as if we're cancelled
+ // while suspending on getSurface(), this call will succeed. Otherwise
+ // it will be a no-op.
+ surfaceRequest.willNotProvideSurface()
+ }
+ }
+
+ // Convert the CameraX TransformationInfo callback into a StateFlow
+ val transformationInfoFlow: StateFlow<CXTransformationInfo?> =
+ MutableStateFlow<CXTransformationInfo?>(null)
+ .also { stateFlow ->
+ // Set a callback to update this state flow
+ surfaceRequest.setTransformationInfoListener(Runnable::run) { transformInfo
+ ->
+ // Set the next value of the flow
+ stateFlow.value = transformInfo
+ }
+ }
+ .asStateFlow()
+
+ // The ImplementationMode that will be used for all TransformationInfo updates.
+ // This is locked in once we have updated ViewfinderArgs and won't change until
+ // this produceState block is cancelled and restarted.
+ var snapshotImplementationMode: ImplementationMode? = null
+ snapshotFlow { currentImplementationMode }
+ .combine(transformationInfoFlow.filterNotNull()) { implMode, transformInfo ->
+ Pair(implMode, transformInfo)
+ }
+ .takeWhile { (implMode, _) ->
+ val shouldAbort =
+ snapshotImplementationMode != null && implMode != snapshotImplementationMode
+ if (shouldAbort) {
+ // Abort flow and invalidate SurfaceRequest so a new SurfaceRequest will
+ // be sent.
+ surfaceRequest.invalidate()
+ } else {
+ // Got the first ImplementationMode. This will be used until this
+ // produceState is cancelled.
+ snapshotImplementationMode = implMode
+ }
+ !shouldAbort
+ }
+ .collect { (implMode, transformInfo) ->
+ value =
+ ViewfinderArgs(
+ viewfinderSurfaceRequest,
+ implMode,
+ TransformationInfo(
+ sourceRotation = transformInfo.rotationDegrees,
+ isSourceMirroredHorizontally = transformInfo.isMirroring,
+ isSourceMirroredVertically = false,
+ cropRectLeft = transformInfo.cropRect.left,
+ cropRectTop = transformInfo.cropRect.top,
+ cropRectRight = transformInfo.cropRect.right,
+ cropRectBottom = transformInfo.cropRect.bottom
+ )
+ )
+ }
+ }
+
+ viewfinderArgs?.let { args ->
+ Viewfinder(
+ surfaceRequest = args.viewfinderSurfaceRequest,
+ implementationMode = args.implementationMode,
+ transformationInfo = args.transformationInfo,
+ modifier = modifier.fillMaxSize(),
+ coordinateTransformer = coordinateTransformer
+ )
+ }
+}
+
+@Immutable
+private data class ViewfinderArgs(
+ val viewfinderSurfaceRequest: ViewfinderSurfaceRequest,
+ val implementationMode: ImplementationMode,
+ val transformationInfo: TransformationInfo
+)
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index ffd3d525..475569e 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -165,14 +165,31 @@
method public androidx.camera.core.CameraXConfig getCameraXConfig();
}
+ public class CompositionSettings {
+ method public float getAlpha();
+ method public androidx.core.util.Pair<java.lang.Float!,java.lang.Float!> getOffset();
+ method public androidx.core.util.Pair<java.lang.Float!,java.lang.Float!> getScale();
+ field public static final androidx.camera.core.CompositionSettings DEFAULT;
+ }
+
+ public static final class CompositionSettings.Builder {
+ ctor public CompositionSettings.Builder();
+ method public androidx.camera.core.CompositionSettings build();
+ method public androidx.camera.core.CompositionSettings.Builder setAlpha(@FloatRange(from=0, to=1) float);
+ method public androidx.camera.core.CompositionSettings.Builder setOffset(@FloatRange(from=0xffffffff, to=1) float, @FloatRange(from=0xffffffff, to=1) float);
+ method public androidx.camera.core.CompositionSettings.Builder setScale(float, float);
+ }
+
public class ConcurrentCamera {
ctor public ConcurrentCamera(java.util.List<androidx.camera.core.Camera!>);
method public java.util.List<androidx.camera.core.Camera!> getCameras();
}
public static final class ConcurrentCamera.SingleCameraConfig {
+ ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.camera.core.CompositionSettings, androidx.lifecycle.LifecycleOwner);
ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.lifecycle.LifecycleOwner);
method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.camera.core.CompositionSettings getCompositionSettings();
method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
method public androidx.camera.core.UseCaseGroup getUseCaseGroup();
}
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index ffd3d525..475569e 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -165,14 +165,31 @@
method public androidx.camera.core.CameraXConfig getCameraXConfig();
}
+ public class CompositionSettings {
+ method public float getAlpha();
+ method public androidx.core.util.Pair<java.lang.Float!,java.lang.Float!> getOffset();
+ method public androidx.core.util.Pair<java.lang.Float!,java.lang.Float!> getScale();
+ field public static final androidx.camera.core.CompositionSettings DEFAULT;
+ }
+
+ public static final class CompositionSettings.Builder {
+ ctor public CompositionSettings.Builder();
+ method public androidx.camera.core.CompositionSettings build();
+ method public androidx.camera.core.CompositionSettings.Builder setAlpha(@FloatRange(from=0, to=1) float);
+ method public androidx.camera.core.CompositionSettings.Builder setOffset(@FloatRange(from=0xffffffff, to=1) float, @FloatRange(from=0xffffffff, to=1) float);
+ method public androidx.camera.core.CompositionSettings.Builder setScale(float, float);
+ }
+
public class ConcurrentCamera {
ctor public ConcurrentCamera(java.util.List<androidx.camera.core.Camera!>);
method public java.util.List<androidx.camera.core.Camera!> getCameras();
}
public static final class ConcurrentCamera.SingleCameraConfig {
+ ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.camera.core.CompositionSettings, androidx.lifecycle.LifecycleOwner);
ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.lifecycle.LifecycleOwner);
method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.camera.core.CompositionSettings getCompositionSettings();
method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
method public androidx.camera.core.UseCaseGroup getUseCaseGroup();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
index 94fa6ae..a0557a5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
@@ -18,6 +18,7 @@
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope
import androidx.lifecycle.LifecycleOwner
+import com.google.common.util.concurrent.ListenableFuture
/**
* A [CameraProvider] provides basic access to a set of cameras such as querying for camera
@@ -91,4 +92,12 @@
public fun getCameraInfo(cameraSelector: CameraSelector): CameraInfo {
throw UnsupportedOperationException("The camera provider is not implemented properly.")
}
+
+ /**
+ * Shuts down the camera provider.
+ *
+ * @return A [ListenableFuture] representing the shutdown status. Cancellation of this future is
+ * a no-op.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP) public fun shutdownAsync(): ListenableFuture<Void>
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java b/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java
index 0151734..9b44add 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CompositionSettings.java
@@ -18,19 +18,71 @@
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
+import androidx.camera.core.ConcurrentCamera.SingleCameraConfig;
import androidx.core.util.Pair;
/**
* Composition settings for dual concurrent camera. It includes alpha value for blending,
- * offset in x, y coordinates, scale of width and height. The offset, width and height are specified
- * in normalized device coordinates.
+ * offset in x, y coordinates, scale of width and height. The offset, and scale of width and height
+ * are specified in normalized device coordinates(NDCs). The offset is applied after scale.
+ * The origin of normalized device coordinates is at the center of the viewing volume. The positive
+ * X-axis extends to the right, the positive Y-axis extends upwards.The x, y values range from -1
+ * to 1. E.g. scale with {@code (0.5f, 0.5f)} and offset with {@code (0.5f, 0.5f)} is the
+ * bottom-right quadrant of the output device.
*
- * @see <a href="https://learnopengl.com/Getting-started/Coordinate-Systems">Normalized Device Coordinates</a>
+ * <p>Composited dual camera frames preview and recording can be supported using
+ * {@link CompositionSettings} and {@link SingleCameraConfig}. The z-order of composition is
+ * determined by the order of camera configs to bind. Currently the background color will be black
+ * by default. The resolution of camera frames for preview and recording will be determined by
+ * resolution selection strategy configured for each use case and the scale of width and height set
+ * in {@link CompositionSettings}, so it is recommended to use 16:9 aspect ratio strategy for
+ * preview if 16:9 quality selector is configured for video capture. The mirroring and rotation of
+ * the camera frame will be applied after composition because both cameras are using the same use
+ * cases.
+ *
+ * <p>The following code snippet demonstrates how to display in Picture-in-Picture mode:
+ * <pre>{@code
+ * ResolutionSelector resolutionSelector = new ResolutionSelector.Builder()
+ * .setAspectRatioStrategy(
+ * AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
+ * .build();
+ * Preview preview = new Preview.Builder()
+ * .setResolutionSelector(resolutionSelector)
+ * .build();
+ * preview.setSurfaceProvider(mSinglePreviewView.getSurfaceProvider());
+ * UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
+ * .addUseCase(preview)
+ * .addUseCase(mVideoCapture)
+ * .build();
+ * SingleCameraConfig primary = new SingleCameraConfig(
+ * cameraSelectorPrimary,
+ * useCaseGroup,
+ * new CompositionSettings.Builder()
+ * .setAlpha(1.0f)
+ * .setOffset(0.0f, 0.0f)
+ * .setScale(1.0f, 1.0f)
+ * .build(),
+ * lifecycleOwner);
+ * SingleCameraConfig secondary = new SingleCameraConfig(
+ * cameraSelectorSecondary,
+ * useCaseGroup,
+ * new CompositionSettings.Builder()
+ * .setAlpha(1.0f)
+ * .setOffset(-0.3f, -0.4f)
+ * .setScale(0.3f, 0.3f)
+ * .build(),
+ * lifecycleOwner);
+ * cameraProvider.bindToLifecycle(ImmutableList.of(primary, secondary));
+ * }}</pre>
+ *
+ * <img src="/images/reference/androidx/camera/camera-core/
+ * concurrent_camera_composition_settings.png"/>
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class CompositionSettings {
+ /**
+ * Default composition settings, which will display in full screen with no offset and scale.
+ */
public static final CompositionSettings DEFAULT = new Builder()
.setAlpha(1.0f)
.setOffset(0.0f, 0.0f)
@@ -85,7 +137,12 @@
private Pair<Float, Float> mOffset;
private Pair<Float, Float> mScale;
- /** Creates a new {@link Builder}. */
+ /**
+ * Creates a new {@link Builder}.
+ *
+ * <p>The default alpha is 1.0f, the default offset is (0.0f, 0.0f), the default scale is
+ * (1.0f, 1.0f).
+ */
public Builder() {
mAlpha = 1.0f;
mOffset = Pair.create(0.0f, 0.0f);
@@ -93,7 +150,7 @@
}
/**
- * Sets the alpha.
+ * Sets the alpha. 0 means fully transparent, 1 means fully opaque.
*
* @param alpha alpha value.
* @return Builder instance.
@@ -127,9 +184,7 @@
* @return Builder instance.
*/
@NonNull
- public Builder setScale(
- @FloatRange(from = -1, to = 1) float scaleX,
- @FloatRange(from = -1, to = 1) float scaleY) {
+ public Builder setScale(float scaleX, float scaleY) {
mScale = Pair.create(scaleX, scaleY);
return this;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ConcurrentCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/ConcurrentCamera.java
index e8d2997..037ee4c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ConcurrentCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ConcurrentCamera.java
@@ -19,7 +19,6 @@
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import androidx.lifecycle.LifecycleOwner;
import java.util.List;
@@ -103,7 +102,6 @@
* @param compositionSettings {@link CompositionSettings}.
* @param lifecycleOwner {@link LifecycleOwner}.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public SingleCameraConfig(
@NonNull CameraSelector cameraSelector,
@NonNull UseCaseGroup useCaseGroup,
@@ -146,7 +144,6 @@
* Returns {@link CompositionSettings}.
* @return {@link CompositionSettings} instance.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public CompositionSettings getCompositionSettings() {
return mCompositionSettings;
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 1725729..541a169 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
@@ -86,6 +86,7 @@
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType;
import androidx.camera.core.impl.stabilization.StabilizationMode;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler;
import androidx.camera.core.streamsharing.StreamSharing;
import androidx.core.util.Preconditions;
@@ -176,6 +177,8 @@
private final CompositionSettings mCompositionSettings;
@NonNull
private final CompositionSettings mSecondaryCompositionSettings;
+ private final StreamSharingForceEnabler mStreamSharingForceEnabler =
+ new StreamSharingForceEnabler();
/**
* Create a new {@link CameraUseCaseAdapter} instance.
@@ -359,7 +362,7 @@
// Force enable StreamSharing for Extensions to support VideoCapture. This means that
// applyStreamSharing is set to true when the use case combination contains
// VideoCapture and Extensions is enabled.
- if (!applyStreamSharing && hasExtension() && hasVideoCapture(appUseCases)) {
+ if (!applyStreamSharing && shouldForceEnableStreamSharing(appUseCases)) {
updateUseCases(appUseCases, /*applyStreamSharing*/true, isDualCamera);
return;
}
@@ -508,6 +511,15 @@
}
}
+ private boolean shouldForceEnableStreamSharing(@NonNull Collection<UseCase> appUseCases) {
+ if (hasExtension() && hasVideoCapture(appUseCases)) {
+ return true;
+ }
+
+ return mStreamSharingForceEnabler.shouldForceEnableStreamSharing(
+ mCameraInternal.getCameraInfoInternal().getCameraId(), appUseCases);
+ }
+
/**
* Return true if the given StreamSpec has any option with a different value than that
* of the given sessionConfig.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 86a9be5..86948ad 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -63,6 +63,10 @@
IncorrectJpegMetadataQuirk.load())) {
quirks.add(new IncorrectJpegMetadataQuirk());
}
+ if (quirkSettings.shouldEnableQuirk(ImageCaptureFailedForSpecificCombinationQuirk.class,
+ ImageCaptureFailedForSpecificCombinationQuirk.load())) {
+ quirks.add(new ImageCaptureFailedForSpecificCombinationQuirk());
+ }
return quirks;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureFailedForSpecificCombinationQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureFailedForSpecificCombinationQuirk.java
new file mode 100644
index 0000000..d468b9f
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureFailedForSpecificCombinationQuirk.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk;
+
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.Preview;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+
+import java.util.Collection;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: 359062845
+ * Description: Quirk required to check whether still image capture can run failed when a
+ * specific combination of UseCases are bound together.
+ * Device(s): OnePlus 12
+ */
+public final class ImageCaptureFailedForSpecificCombinationQuirk implements Quirk {
+ static boolean load() {
+ return isOnePlus12();
+ }
+
+ private static boolean isOnePlus12() {
+ return "oneplus".equalsIgnoreCase(Build.BRAND) && "cph2583".equalsIgnoreCase(Build.MODEL);
+ }
+
+ /**
+ * Returns whether stream sharing should be forced enabled for specific camera and UseCase
+ * combination.
+ */
+ public boolean shouldForceEnableStreamSharing(@NonNull String cameraId,
+ @NonNull Collection<UseCase> appUseCases) {
+ if (isOnePlus12()) {
+ return shouldForceEnableStreamSharingForOnePlus12(cameraId, appUseCases);
+ }
+ return false;
+ }
+
+ /**
+ * On OnePlus 12, still image capture run failed on the front camera only when the UseCase
+ * combination is exactly Preview + VideoCapture + ImageCapture.
+ */
+ private boolean shouldForceEnableStreamSharingForOnePlus12(@NonNull String cameraId,
+ @NonNull Collection<UseCase> appUseCases) {
+ if (!cameraId.equals("1") || appUseCases.size() != 3) {
+ return false;
+ }
+
+ boolean hasPreview = false;
+ boolean hasVideoCapture = false;
+ boolean hasImageCapture = false;
+
+ for (UseCase useCase : appUseCases) {
+ if (useCase instanceof Preview) {
+ hasPreview = true;
+ } else if (useCase instanceof ImageCapture) {
+ hasImageCapture = true;
+ } else {
+ if (useCase.getCurrentConfig().containsOption(OPTION_CAPTURE_TYPE)) {
+ hasVideoCapture = useCase.getCurrentConfig().getCaptureType()
+ == UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE;
+ }
+ }
+ }
+
+ return hasPreview && hasVideoCapture && hasImageCapture;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java
new file mode 100644
index 0000000..21209cb
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.workaround;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.core.internal.compat.quirk.ImageCaptureFailedForSpecificCombinationQuirk;
+
+import java.util.Collection;
+
+/**
+ * Workaround to check whether stream sharing should be forced enabled.
+ *
+ * @see ImageCaptureFailedForSpecificCombinationQuirk
+ */
+public class StreamSharingForceEnabler {
+ @Nullable
+ private final ImageCaptureFailedForSpecificCombinationQuirk mSpecificCombinationQuirk =
+ DeviceQuirks.get(ImageCaptureFailedForSpecificCombinationQuirk.class);
+
+ /**
+ * Returns whether stream sharing should be forced enabled.
+ */
+ public boolean shouldForceEnableStreamSharing(@NonNull String cameraId,
+ @NonNull Collection<UseCase> appUseCases) {
+ if (mSpecificCombinationQuirk != null) {
+ return mSpecificCombinationQuirk.shouldForceEnableStreamSharing(cameraId, appUseCases);
+ }
+
+ return false;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
index 1cdc173..3820ac5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
@@ -37,6 +37,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.arch.core.util.Function;
+import androidx.camera.core.CameraXThreads;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceOutput;
@@ -110,7 +111,7 @@
*/
DefaultSurfaceProcessor(@NonNull DynamicRange dynamicRange,
@NonNull Map<InputFormat, ShaderProvider> shaderProviderOverrides) {
- mGlThread = new HandlerThread("GL Thread");
+ mGlThread = new HandlerThread(CameraXThreads.TAG + "GL Thread");
mGlThread.start();
mGlHandler = new Handler(mGlThread.getLooper());
mGlExecutor = CameraXExecutors.newHandlerExecutor(mGlHandler);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/concurrent/DualSurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/concurrent/DualSurfaceProcessor.java
index 3f5be70..443b757 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/concurrent/DualSurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/concurrent/DualSurfaceProcessor.java
@@ -27,6 +27,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
+import androidx.camera.core.CameraXThreads;
import androidx.camera.core.CompositionSettings;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.Logger;
@@ -92,7 +93,7 @@
@NonNull Map<InputFormat, ShaderProvider> shaderProviderOverrides,
@NonNull CompositionSettings primaryCompositionSettings,
@NonNull CompositionSettings secondaryCompositionSettings) {
- mGlThread = new HandlerThread("GL Thread");
+ mGlThread = new HandlerThread(CameraXThreads.TAG + "GL Thread");
mGlThread.start();
mGlHandler = new Handler(mGlThread.getLooper());
mGlExecutor = CameraXExecutors.newHandlerExecutor(mGlHandler);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt
new file mode 100644
index 0000000..e7e0ee7
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.workaround
+
+import android.os.Build
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+private const val PREVIEW = 0x1
+private const val IMAGE_CAPTURE = 0x2
+private const val VIDEO_CAPTURE = 0x4
+private const val IMAGE_ANALYSIS = 0x8
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamSharingForceEnablerTest(
+ private val brand: String,
+ private val model: String,
+ private val cameraId: String,
+ private val useCaseCombination: Int,
+ private val shouldEnableStreamSharing: Boolean
+) {
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(
+ name = "brand={0}, model={1}, cameraId={2}, useCases={3}, result={4}"
+ )
+ fun data() =
+ mutableListOf<Array<Any?>>().apply {
+ add(
+ arrayOf(
+ "OnePlus",
+ "cph2583",
+ "0",
+ PREVIEW or IMAGE_CAPTURE or VIDEO_CAPTURE,
+ false
+ )
+ )
+ add(
+ arrayOf(
+ "OnePlus",
+ "cph2583",
+ "1",
+ PREVIEW or IMAGE_CAPTURE or VIDEO_CAPTURE,
+ true
+ )
+ )
+ add(
+ arrayOf(
+ "OnePlus",
+ "cph2583",
+ "1",
+ PREVIEW or IMAGE_CAPTURE or VIDEO_CAPTURE or IMAGE_ANALYSIS,
+ false
+ )
+ )
+ add(arrayOf("", "", "1", PREVIEW or IMAGE_CAPTURE or VIDEO_CAPTURE, false))
+ }
+ }
+
+ @Test
+ fun shouldForceEnableStreamSharing() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
+
+ assertThat(
+ StreamSharingForceEnabler()
+ .shouldForceEnableStreamSharing(cameraId, createUseCases(useCaseCombination))
+ )
+ .isEqualTo(shouldEnableStreamSharing)
+ }
+
+ private fun createUseCases(useCaseCombination: Int): Collection<UseCase> {
+ val useCases = mutableListOf<UseCase>()
+
+ if (useCaseCombination and PREVIEW != 0) {
+ useCases.add(Preview.Builder().build())
+ }
+ if (useCaseCombination and IMAGE_CAPTURE != 0) {
+ useCases.add(ImageCapture.Builder().build())
+ }
+ if (useCaseCombination and VIDEO_CAPTURE != 0) {
+ useCases.add(
+ FakeUseCaseConfig.Builder()
+ .setCaptureType(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE)
+ .build()
+ )
+ }
+ if (useCaseCombination and IMAGE_ANALYSIS != 0) {
+ useCases.add(ImageAnalysis.Builder().build())
+ }
+
+ return useCases
+ }
+}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
index 16d4260..c35e89e 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/advanced/BokehAdvancedExtenderImpl.java
@@ -105,7 +105,6 @@
}
@Override
- @NonNull
public boolean isCaptureProcessProgressAvailable() {
throw new RuntimeException("Stub, replace with implementation.");
}
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index 0688972..2f44ee0 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -735,14 +735,6 @@
}
@Test
- fun cannotConfigureTwice() {
- ProcessCameraProvider.configureInstance(FakeAppConfig.create())
- assertThrows<IllegalStateException> {
- ProcessCameraProvider.configureInstance(FakeAppConfig.create())
- }
- }
-
- @Test
fun shutdown_clearsPreviousConfiguration() {
ProcessCameraProvider.configureInstance(FakeAppConfig.create())
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProvider.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProvider.kt
index 33cc057..08c1e72 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProvider.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProvider.kt
@@ -15,11 +15,15 @@
*/
package androidx.camera.lifecycle
+import android.content.Context
import android.content.pm.PackageManager
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraProvider
import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
import androidx.camera.core.CompositionSettings
import androidx.camera.core.ConcurrentCamera
import androidx.camera.core.ConcurrentCamera.SingleCameraConfig
@@ -28,21 +32,27 @@
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.concurrent.futures.await
+import androidx.core.util.Preconditions
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
+import com.google.common.util.concurrent.ListenableFuture
/**
- * Provides access to a camera which has has its opening and closing controlled by a
- * [LifecycleOwner].
+ * Provides access to a camera which has its opening and closing controlled by a [LifecycleOwner].
*/
-internal interface LifecycleCameraProvider : CameraProvider {
+// TODO: Remove the annotation when LifecycleCameraProvider is ready to be public.
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface LifecycleCameraProvider : CameraProvider {
/**
* Returns `true` if the [UseCase] is bound to a lifecycle. Otherwise returns `false`.
*
* After binding a use case, use cases remain bound until the lifecycle reaches a
* [Lifecycle.State.DESTROYED] state or if is unbound by calls to [unbind] or [unbindAll].
*/
- fun isBound(useCase: UseCase): Boolean
+ public fun isBound(useCase: UseCase): Boolean
/**
* Unbinds all specified use cases from the lifecycle provider.
@@ -57,8 +67,9 @@
*
* @param useCases The collection of use cases to remove.
* @throws IllegalStateException If not called on main thread.
+ * @throws UnsupportedOperationException If called in concurrent mode.
*/
- fun unbind(vararg useCases: UseCase?)
+ public fun unbind(vararg useCases: UseCase?): Unit
/**
* Unbinds all use cases from the lifecycle provider and removes them from CameraX.
@@ -67,7 +78,7 @@
*
* @throws IllegalStateException If not called on main thread.
*/
- fun unbindAll()
+ public fun unbindAll(): Unit
/**
* Binds the collection of [UseCase] to a [LifecycleOwner].
@@ -124,7 +135,7 @@
* camera to be used for the given use cases.
* @throws UnsupportedOperationException If the camera is configured in concurrent mode.
*/
- fun bindToLifecycle(
+ public fun bindToLifecycle(
lifecycleOwner: LifecycleOwner,
cameraSelector: CameraSelector,
vararg useCases: UseCase?
@@ -142,7 +153,7 @@
*
* @throws UnsupportedOperationException If the camera is configured in concurrent mode.
*/
- fun bindToLifecycle(
+ public fun bindToLifecycle(
lifecycleOwner: LifecycleOwner,
cameraSelector: CameraSelector,
useCaseGroup: UseCaseGroup
@@ -169,8 +180,9 @@
*
* If the concurrent logical cameras are binding the same preview and video capture use cases,
* the concurrent cameras video recording will be supported. The concurrent camera preview
- * stream will be shared with video capture and record the concurrent cameras as a whole. The
- * [CompositionSettings] can be used to configure the position of each camera stream.
+ * stream will be shared with video capture and record the concurrent cameras streams as a
+ * composited stream. The [CompositionSettings] can be used to configure the position of each
+ * camera stream and different layouts can be built. See [CompositionSettings] for more details.
*
* If we want to open concurrent physical cameras, which are two front cameras or two back
* cameras, the device needs to support physical cameras and the capability could be checked via
@@ -206,5 +218,48 @@
* @see CameraInfo.isLogicalMultiCameraSupported
* @see CameraInfo.getPhysicalCameraInfos
*/
- fun bindToLifecycle(singleCameraConfigs: List<SingleCameraConfig?>): ConcurrentCamera
+ public fun bindToLifecycle(singleCameraConfigs: List<SingleCameraConfig?>): ConcurrentCamera
+
+ public companion object {
+ /**
+ * Creates a lifecycle camera provider instance.
+ *
+ * @param context The Application context.
+ * @param cameraXConfig The configuration options to configure the lifecycle camera
+ * provider. If not set, the default configuration will be used.
+ * @return The lifecycle camera provider instance.
+ */
+ @JvmOverloads
+ @JvmStatic
+ public suspend fun createInstance(
+ context: Context,
+ cameraXConfig: CameraXConfig? = null,
+ ): LifecycleCameraProvider {
+ return createInstanceAsync(context, cameraXConfig).await()
+ }
+
+ /**
+ * Creates a lifecycle camera provider instance asynchronously.
+ *
+ * @param context The Application context.
+ * @param cameraXConfig The configuration options to configure the lifecycle camera
+ * provider. If not set, the default configuration will be used.
+ * @return A [ListenableFuture] that will be completed when the lifecycle camera provider
+ * instance is initialized.
+ */
+ @JvmOverloads
+ @JvmStatic
+ public fun createInstanceAsync(
+ context: Context,
+ cameraXConfig: CameraXConfig? = null,
+ ): ListenableFuture<LifecycleCameraProvider> {
+ Preconditions.checkNotNull(context)
+ val lifecycleCameraProvider = LifecycleCameraProviderImpl()
+ return Futures.transform(
+ lifecycleCameraProvider.initAsync(context, cameraXConfig),
+ { lifecycleCameraProvider },
+ CameraXExecutors.directExecutor()
+ )
+ }
+ }
}
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
new file mode 100644
index 0000000..bb2d4f4
--- /dev/null
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
@@ -0,0 +1,711 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.lifecycle
+
+import android.content.Context
+import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
+import androidx.annotation.GuardedBy
+import androidx.annotation.MainThread
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.CameraFilter
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraInfoUnavailableException
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.CompositionSettings
+import androidx.camera.core.ConcurrentCamera
+import androidx.camera.core.ConcurrentCamera.SingleCameraConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.core.ViewPort
+import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT
+import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE
+import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED
+import androidx.camera.core.concurrent.CameraCoordinator.CameraOperatingMode
+import androidx.camera.core.impl.CameraConfig
+import androidx.camera.core.impl.CameraConfigs
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
+import androidx.camera.core.impl.RestrictedCameraInfo
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.ContextUtil
+import androidx.camera.core.impl.utils.Threads
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.utils.futures.FutureCallback
+import androidx.camera.core.impl.utils.futures.FutureChain
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.core.util.Preconditions
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.tracing.trace
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.Objects
+import java.util.Objects.requireNonNull
+
+/** Implementation of the [LifecycleCameraProvider] interface. */
+internal class LifecycleCameraProviderImpl : LifecycleCameraProvider {
+ private val lock = Any()
+ @GuardedBy("mLock") private var cameraXConfigProvider: CameraXConfig.Provider? = null
+ @GuardedBy("mLock") private var cameraXInitializeFuture: ListenableFuture<Void>? = null
+ @GuardedBy("mLock") private var cameraXShutdownFuture = Futures.immediateFuture<Void>(null)
+ private val lifecycleCameraRepository = LifecycleCameraRepository()
+ private var cameraX: CameraX? = null
+ private var context: Context? = null
+ @GuardedBy("mLock")
+ private val cameraInfoMap: MutableMap<CameraUseCaseAdapter.CameraId, RestrictedCameraInfo> =
+ HashMap()
+
+ internal fun initAsync(
+ context: Context,
+ cameraXConfig: CameraXConfig? = null
+ ): ListenableFuture<Void> {
+ synchronized(lock) {
+ if (cameraXInitializeFuture != null) {
+ return cameraXInitializeFuture as ListenableFuture<Void>
+ }
+ cameraXConfig?.let { configure(it) }
+ val cameraX = CameraX(context, cameraXConfigProvider)
+
+ cameraXInitializeFuture =
+ CallbackToFutureAdapter.getFuture { completer ->
+ synchronized(lock) {
+ val future: ListenableFuture<Void> =
+ FutureChain.from(cameraXShutdownFuture)
+ .transformAsync(
+ { cameraX.initializeFuture },
+ CameraXExecutors.directExecutor()
+ )
+ Futures.addCallback(
+ future,
+ object : FutureCallback<Void?> {
+ override fun onSuccess(result: Void?) {
+ [email protected] = cameraX
+ [email protected] =
+ ContextUtil.getApplicationContext(context)
+ completer.set(null)
+ }
+
+ override fun onFailure(t: Throwable) {
+ completer.setException(t)
+ }
+ },
+ CameraXExecutors.directExecutor()
+ )
+ }
+
+ "LifecycleCameraProvider-initialize"
+ }
+
+ return cameraXInitializeFuture as ListenableFuture<Void>
+ }
+ }
+
+ /**
+ * Configures the camera provider.
+ *
+ * The camera provider can only be configured once. Trying to configure it multiple times will
+ * throw an [IllegalStateException].
+ *
+ * @param cameraXConfig The CameraX configuration.
+ */
+ internal fun configure(cameraXConfig: CameraXConfig) =
+ trace("CX:configureInstanceInternal") {
+ synchronized(lock) {
+ Preconditions.checkNotNull(cameraXConfig)
+ Preconditions.checkState(
+ cameraXConfigProvider == null,
+ "CameraX has already been configured. To use a different configuration, " +
+ "shutdown() must be called."
+ )
+ cameraXConfigProvider = CameraXConfig.Provider { cameraXConfig }
+ }
+ }
+
+ override fun shutdownAsync(): ListenableFuture<Void> {
+ Threads.runOnMainSync {
+ unbindAll()
+ lifecycleCameraRepository.clear()
+ }
+
+ if (cameraX != null) {
+ cameraX!!.cameraFactory.cameraCoordinator.shutdown()
+ }
+
+ val shutdownFuture =
+ if (cameraX != null) cameraX!!.shutdown() else Futures.immediateFuture<Void>(null)
+
+ synchronized(lock) {
+ cameraXConfigProvider = null
+ cameraXInitializeFuture = null
+ cameraXShutdownFuture = shutdownFuture
+ cameraInfoMap.clear()
+ }
+ cameraX = null
+ context = null
+ return shutdownFuture
+ }
+
+ override fun isBound(useCase: UseCase): Boolean {
+ for (lifecycleCamera: LifecycleCamera in lifecycleCameraRepository.lifecycleCameras) {
+ if (lifecycleCamera.isBound(useCase)) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ @MainThread
+ override fun unbind(vararg useCases: UseCase?): Unit =
+ trace("CX:unbind") {
+ Threads.checkMainThread()
+
+ if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
+ throw UnsupportedOperationException(
+ "Unbind usecase is not supported in concurrent camera mode, call unbindAll() first."
+ )
+ }
+
+ lifecycleCameraRepository.unbind(listOf(*useCases))
+ }
+
+ @MainThread
+ override fun unbindAll(): Unit =
+ trace("CX:unbindAll") {
+ Threads.checkMainThread()
+ cameraOperatingMode = CAMERA_OPERATING_MODE_UNSPECIFIED
+ lifecycleCameraRepository.unbindAll()
+ }
+
+ @Throws(CameraInfoUnavailableException::class)
+ override fun hasCamera(cameraSelector: CameraSelector): Boolean =
+ trace("CX:hasCamera") {
+ try {
+ cameraSelector.select(cameraX!!.cameraRepository.cameras)
+ } catch (e: IllegalArgumentException) {
+ return@trace false
+ }
+
+ return@trace true
+ }
+
+ @MainThread
+ override fun bindToLifecycle(
+ lifecycleOwner: LifecycleOwner,
+ cameraSelector: CameraSelector,
+ vararg useCases: UseCase?
+ ): Camera =
+ trace("CX:bindToLifecycle") {
+ if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
+ throw UnsupportedOperationException(
+ "bindToLifecycle for single camera is not supported in concurrent camera mode, " +
+ "call unbindAll() first"
+ )
+ }
+ cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
+ val camera =
+ bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ null,
+ CompositionSettings.DEFAULT,
+ CompositionSettings.DEFAULT,
+ null,
+ emptyList<CameraEffect>(),
+ *useCases
+ )
+ return@trace camera
+ }
+
+ @MainThread
+ public override fun bindToLifecycle(
+ lifecycleOwner: LifecycleOwner,
+ cameraSelector: CameraSelector,
+ useCaseGroup: UseCaseGroup
+ ): Camera =
+ trace("CX:bindToLifecycle-UseCaseGroup") {
+ if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
+ throw UnsupportedOperationException(
+ "bindToLifecycle for single camera is not supported in concurrent camera mode, " +
+ "call unbindAll() first."
+ )
+ }
+ cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
+ val camera =
+ bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ null,
+ CompositionSettings.DEFAULT,
+ CompositionSettings.DEFAULT,
+ useCaseGroup.viewPort,
+ useCaseGroup.effects,
+ *useCaseGroup.useCases.toTypedArray<UseCase>()
+ )
+ return@trace camera
+ }
+
+ @MainThread
+ override fun bindToLifecycle(singleCameraConfigs: List<SingleCameraConfig?>): ConcurrentCamera =
+ trace("CX:bindToLifecycle-Concurrent") {
+ if (singleCameraConfigs.size < 2) {
+ throw IllegalArgumentException("Concurrent camera needs two camera configs.")
+ }
+
+ if (singleCameraConfigs.size > 2) {
+ throw IllegalArgumentException(
+ "Concurrent camera is only supporting two cameras at maximum."
+ )
+ }
+
+ val firstCameraConfig = singleCameraConfigs[0]!!
+ val secondCameraConfig = singleCameraConfigs[1]!!
+
+ val cameras: MutableList<Camera> = ArrayList()
+ if (
+ firstCameraConfig.cameraSelector.lensFacing ==
+ secondCameraConfig.cameraSelector.lensFacing
+ ) {
+ if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
+ throw UnsupportedOperationException(
+ "Camera is already running, call unbindAll() before binding more cameras."
+ )
+ }
+ if (
+ firstCameraConfig.lifecycleOwner != secondCameraConfig.lifecycleOwner ||
+ firstCameraConfig.useCaseGroup.viewPort !=
+ secondCameraConfig.useCaseGroup.viewPort ||
+ firstCameraConfig.useCaseGroup.effects !=
+ secondCameraConfig.useCaseGroup.effects
+ ) {
+ throw IllegalArgumentException(
+ "Two camera configs need to have the same lifecycle owner, view port and " +
+ "effects."
+ )
+ }
+ val lifecycleOwner = firstCameraConfig.lifecycleOwner
+ val cameraSelector = firstCameraConfig.cameraSelector
+ val viewPort = firstCameraConfig.useCaseGroup.viewPort
+ val effects = firstCameraConfig.useCaseGroup.effects
+ val useCases: MutableList<UseCase> = ArrayList()
+ for (config: SingleCameraConfig? in singleCameraConfigs) {
+ // Connect physical camera id with use case.
+ for (useCase: UseCase in config!!.useCaseGroup.useCases) {
+ config.cameraSelector.physicalCameraId?.let {
+ useCase.setPhysicalCameraId(it)
+ }
+ }
+ useCases.addAll(config.useCaseGroup.useCases)
+ }
+
+ cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
+ val camera =
+ bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ null,
+ CompositionSettings.DEFAULT,
+ CompositionSettings.DEFAULT,
+ viewPort,
+ effects,
+ *useCases.toTypedArray<UseCase>()
+ )
+ cameras.add(camera)
+ } else {
+ if (!context!!.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
+ throw UnsupportedOperationException(
+ "Concurrent camera is not supported on the device."
+ )
+ }
+
+ if (cameraOperatingMode == CAMERA_OPERATING_MODE_SINGLE) {
+ throw UnsupportedOperationException(
+ "Camera is already running, call unbindAll() before binding more cameras."
+ )
+ }
+
+ val cameraInfosToBind: MutableList<CameraInfo> = ArrayList()
+ val firstCameraInfo: CameraInfo
+ val secondCameraInfo: CameraInfo
+ try {
+ firstCameraInfo = getCameraInfo(firstCameraConfig.cameraSelector)
+ secondCameraInfo = getCameraInfo(secondCameraConfig.cameraSelector)
+ } catch (e: IllegalArgumentException) {
+ throw IllegalArgumentException("Invalid camera selectors in camera configs.")
+ }
+ cameraInfosToBind.add(firstCameraInfo)
+ cameraInfosToBind.add(secondCameraInfo)
+ if (
+ activeConcurrentCameraInfos.isNotEmpty() &&
+ cameraInfosToBind != activeConcurrentCameraInfos
+ ) {
+ throw UnsupportedOperationException(
+ "Cameras are already running, call unbindAll() before binding more cameras."
+ )
+ }
+
+ cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
+
+ // For dual camera video capture, we are only supporting two use cases:
+ // Preview + VideoCapture. If ImageCapture support is added, the validation logic
+ // will be updated accordingly.
+ var isDualCameraVideoCapture = false
+ if (
+ Objects.equals(
+ firstCameraConfig.useCaseGroup.useCases,
+ secondCameraConfig.useCaseGroup.useCases
+ ) && firstCameraConfig.useCaseGroup.useCases.size == 2
+ ) {
+ val useCase0 = firstCameraConfig.useCaseGroup.useCases[0]
+ val useCase1 = firstCameraConfig.useCaseGroup.useCases[1]
+ isDualCameraVideoCapture =
+ (isVideoCapture(useCase0) && isPreview(useCase1)) ||
+ (isPreview(useCase0) && isVideoCapture(useCase1))
+ }
+
+ if (isDualCameraVideoCapture) {
+ cameras.add(
+ bindToLifecycle(
+ firstCameraConfig.lifecycleOwner,
+ firstCameraConfig.cameraSelector,
+ secondCameraConfig.cameraSelector,
+ firstCameraConfig.compositionSettings,
+ secondCameraConfig.compositionSettings,
+ firstCameraConfig.useCaseGroup.viewPort,
+ firstCameraConfig.useCaseGroup.effects,
+ *firstCameraConfig.useCaseGroup.useCases.toTypedArray<UseCase>(),
+ )
+ )
+ } else {
+ for (config: SingleCameraConfig? in singleCameraConfigs) {
+ val camera =
+ bindToLifecycle(
+ config!!.lifecycleOwner,
+ config.cameraSelector,
+ null,
+ CompositionSettings.DEFAULT,
+ CompositionSettings.DEFAULT,
+ config.useCaseGroup.viewPort,
+ config.useCaseGroup.effects,
+ *config.useCaseGroup.useCases.toTypedArray<UseCase>()
+ )
+ cameras.add(camera)
+ }
+ }
+ activeConcurrentCameraInfos = cameraInfosToBind
+ }
+ return@trace ConcurrentCamera(cameras)
+ }
+
+ override val availableCameraInfos: List<CameraInfo>
+ get() =
+ trace("CX:getAvailableCameraInfos") {
+ val availableCameraInfos: MutableList<CameraInfo> = ArrayList()
+ val cameras: Set<CameraInternal> = cameraX!!.cameraRepository.cameras
+ for (camera: CameraInternal in cameras) {
+ availableCameraInfos.add(camera.cameraInfo)
+ }
+ return@trace availableCameraInfos
+ }
+
+ override val availableConcurrentCameraInfos: List<List<CameraInfo>>
+ get() =
+ trace("CX:getAvailableConcurrentCameraInfos") {
+ requireNonNull(cameraX)
+ requireNonNull(cameraX!!.cameraFactory.cameraCoordinator)
+ val concurrentCameraSelectorLists =
+ cameraX!!.cameraFactory.cameraCoordinator.concurrentCameraSelectors
+
+ val availableConcurrentCameraInfos: MutableList<List<CameraInfo>> = ArrayList()
+ for (cameraSelectors in concurrentCameraSelectorLists) {
+ val cameraInfos: MutableList<CameraInfo> = ArrayList()
+ for (cameraSelector in cameraSelectors) {
+ var cameraInfo: CameraInfo
+ try {
+ cameraInfo = getCameraInfo(cameraSelector)
+ } catch (e: IllegalArgumentException) {
+ continue
+ }
+ cameraInfos.add(cameraInfo)
+ }
+ availableConcurrentCameraInfos.add(cameraInfos)
+ }
+ return@trace availableConcurrentCameraInfos
+ }
+
+ override val isConcurrentCameraModeOn: Boolean
+ /**
+ * Returns whether there is a [ConcurrentCamera] bound.
+ *
+ * @return `true` if there is a [ConcurrentCamera] bound, otherwise `false`.
+ */
+ @MainThread get() = cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT
+
+ /**
+ * Binds [ViewPort] and a collection of [UseCase] to a [LifecycleOwner].
+ *
+ * The state of the lifecycle will determine when the cameras are open, started, stopped and
+ * closed. When started, the use cases receive camera data.
+ *
+ * Binding to a [LifecycleOwner] in state currently in [Lifecycle.State.STARTED] or greater will
+ * also initialize and start data capture. If the camera was already running this may cause a
+ * new initialization to occur temporarily stopping data from the camera before restarting it.
+ *
+ * Multiple use cases can be bound via adding them all to a single [bindToLifecycle] call, or by
+ * using multiple [bindToLifecycle] calls. Using a single call that includes all the use cases
+ * helps to set up a camera session correctly for all uses cases, such as by allowing
+ * determination of resolutions depending on all the use cases bound being bound. If the use
+ * cases are bound separately, it will find the supported resolution with the priority depending
+ * on the binding sequence. If the use cases are bound with a single call, it will find the
+ * supported resolution with the priority in sequence of [ImageCapture], [Preview] and then
+ * [ImageAnalysis]. The resolutions that can be supported depends on the camera device hardware
+ * level that there are some default guaranteed resolutions listed in
+ * [android.hardware.camera2.CameraDevice.createCaptureSession].
+ *
+ * Currently up to 3 use cases may be bound to a [Lifecycle] at any time. Exceeding capability
+ * of target camera device will throw an IllegalArgumentException.
+ *
+ * A [UseCase] should only be bound to a single lifecycle and camera selector a time. Attempting
+ * to bind a use case to a lifecycle when it is already bound to another lifecycle is an error,
+ * and the use case binding will not change. Attempting to bind the same use case to multiple
+ * camera selectors is also an error and will not change the binding.
+ *
+ * If different use cases are bound to different camera selectors that resolve to distinct
+ * cameras, but the same lifecycle, only one of the cameras will operate at a time. The
+ * non-operating camera will not become active until it is the only camera with use cases bound.
+ *
+ * The [Camera] returned is determined by the given camera selector, plus other internal
+ * requirements, possibly from use case configurations. The camera returned from
+ * [bindToLifecycle] may differ from the camera determined solely by a camera selector. If the
+ * camera selector can't resolve a camera under the requirements, an [IllegalArgumentException]
+ * will be thrown.
+ *
+ * Only [UseCase] bound to latest active [Lifecycle] can keep alive. [UseCase] bound to other
+ * [Lifecycle] will be stopped.
+ *
+ * @param lifecycleOwner The [LifecycleOwner] which controls the lifecycle transitions of the
+ * use cases.
+ * @param primaryCameraSelector The primary camera selector which determines the camera to use
+ * for set of use cases.
+ * @param secondaryCameraSelector The secondary camera selector in dual camera case.
+ * @param primaryCompositionSettings The composition settings for the primary camera.
+ * @param secondaryCompositionSettings The composition settings for the secondary camera.
+ * @param viewPort The viewPort which represents the visible camera sensor rect.
+ * @param effects The effects applied to the camera outputs.
+ * @param useCases The use cases to bind to a lifecycle.
+ * @return The [Camera] instance which is determined by the camera selector and internal
+ * requirements.
+ * @throws IllegalStateException If the use case has already been bound to another lifecycle or
+ * method is not called on main thread.
+ * @throws IllegalArgumentException If the provided camera selector is unable to resolve a
+ * camera to be used for the given use cases.
+ */
+ @Suppress("unused")
+ private fun bindToLifecycle(
+ lifecycleOwner: LifecycleOwner,
+ primaryCameraSelector: CameraSelector,
+ secondaryCameraSelector: CameraSelector?,
+ primaryCompositionSettings: CompositionSettings,
+ secondaryCompositionSettings: CompositionSettings,
+ viewPort: ViewPort?,
+ effects: List<CameraEffect?>,
+ vararg useCases: UseCase?
+ ): Camera =
+ trace("CX:bindToLifecycle-internal") {
+ Threads.checkMainThread()
+ // TODO(b/153096869): override UseCase's target rotation.
+
+ // Get the LifecycleCamera if existed.
+ val primaryCameraInternal =
+ primaryCameraSelector.select(cameraX!!.cameraRepository.cameras)
+ primaryCameraInternal.setPrimary(true)
+ val primaryRestrictedCameraInfo =
+ getCameraInfo(primaryCameraSelector) as RestrictedCameraInfo
+
+ var secondaryCameraInternal: CameraInternal? = null
+ var secondaryRestrictedCameraInfo: RestrictedCameraInfo? = null
+ if (secondaryCameraSelector != null) {
+ secondaryCameraInternal =
+ secondaryCameraSelector.select(cameraX!!.cameraRepository.cameras)
+ secondaryCameraInternal.setPrimary(false)
+ secondaryRestrictedCameraInfo =
+ getCameraInfo(secondaryCameraSelector) as RestrictedCameraInfo
+ }
+
+ var lifecycleCameraToBind =
+ lifecycleCameraRepository.getLifecycleCamera(
+ lifecycleOwner,
+ CameraUseCaseAdapter.generateCameraId(
+ primaryRestrictedCameraInfo,
+ secondaryRestrictedCameraInfo
+ )
+ )
+
+ // Check if there's another camera that has already been bound.
+ val lifecycleCameras = lifecycleCameraRepository.lifecycleCameras
+ useCases.filterNotNull().forEach { useCase ->
+ for (lifecycleCamera: LifecycleCamera in lifecycleCameras) {
+ if (
+ lifecycleCamera.isBound(useCase) && lifecycleCamera != lifecycleCameraToBind
+ ) {
+ throw IllegalStateException(
+ String.format(
+ "Use case %s already bound to a different lifecycle.",
+ useCase
+ )
+ )
+ }
+ }
+ }
+
+ // Create the LifecycleCamera if there's no existing one that can be used.
+ if (lifecycleCameraToBind == null) {
+ lifecycleCameraToBind =
+ lifecycleCameraRepository.createLifecycleCamera(
+ lifecycleOwner,
+ CameraUseCaseAdapter(
+ primaryCameraInternal,
+ secondaryCameraInternal,
+ primaryRestrictedCameraInfo,
+ secondaryRestrictedCameraInfo,
+ primaryCompositionSettings,
+ secondaryCompositionSettings,
+ cameraX!!.cameraFactory.cameraCoordinator,
+ cameraX!!.cameraDeviceSurfaceManager,
+ cameraX!!.defaultConfigFactory
+ )
+ )
+ }
+
+ if (useCases.isEmpty()) {
+ return@trace lifecycleCameraToBind!!
+ }
+
+ lifecycleCameraRepository.bindToLifecycleCamera(
+ lifecycleCameraToBind!!,
+ viewPort,
+ effects,
+ listOf(*useCases),
+ cameraX!!.cameraFactory.cameraCoordinator
+ )
+
+ return@trace lifecycleCameraToBind
+ }
+
+ override fun getCameraInfo(cameraSelector: CameraSelector): CameraInfo =
+ trace("CX:getCameraInfo") {
+ val cameraInfoInternal =
+ cameraSelector.select(cameraX!!.cameraRepository.cameras).cameraInfoInternal
+ val cameraConfig = getCameraConfig(cameraSelector, cameraInfoInternal)
+
+ val key =
+ CameraUseCaseAdapter.CameraId.create(
+ cameraInfoInternal.cameraId,
+ cameraConfig.compatibilityId
+ )
+ var restrictedCameraInfo: RestrictedCameraInfo?
+ synchronized(lock) {
+ restrictedCameraInfo = cameraInfoMap[key]
+ if (restrictedCameraInfo == null) {
+ restrictedCameraInfo = RestrictedCameraInfo(cameraInfoInternal, cameraConfig)
+ cameraInfoMap[key] = restrictedCameraInfo!!
+ }
+ }
+
+ return@trace restrictedCameraInfo!!
+ }
+
+ private fun isVideoCapture(useCase: UseCase): Boolean {
+ return useCase.currentConfig.containsOption(UseCaseConfig.OPTION_CAPTURE_TYPE) &&
+ useCase.currentConfig.captureType == CaptureType.VIDEO_CAPTURE
+ }
+
+ private fun isPreview(useCase: UseCase): Boolean {
+ return useCase is Preview
+ }
+
+ private fun getCameraConfig(
+ cameraSelector: CameraSelector,
+ cameraInfo: CameraInfo
+ ): CameraConfig {
+ var cameraConfig: CameraConfig? = null
+ for (cameraFilter: CameraFilter in cameraSelector.cameraFilterSet) {
+ if (cameraFilter.identifier != CameraFilter.DEFAULT_ID) {
+ val extendedCameraConfig =
+ ExtendedCameraConfigProviderStore.getConfigProvider(cameraFilter.identifier)
+ .getConfig(cameraInfo, (context)!!)
+ if (extendedCameraConfig == null) { // ignore IDs unrelated to camera configs.
+ continue
+ }
+
+ // Only allows one camera config now.
+ if (cameraConfig != null) {
+ throw IllegalArgumentException(
+ "Cannot apply multiple extended camera configs at the same time."
+ )
+ }
+ cameraConfig = extendedCameraConfig
+ }
+ }
+
+ if (cameraConfig == null) {
+ cameraConfig = CameraConfigs.defaultConfig()
+ }
+ return cameraConfig
+ }
+
+ @get:CameraOperatingMode
+ private var cameraOperatingMode: Int
+ get() {
+ if (cameraX == null) {
+ return CAMERA_OPERATING_MODE_UNSPECIFIED
+ }
+ return cameraX!!.cameraFactory.cameraCoordinator.cameraOperatingMode
+ }
+ private set(cameraOperatingMode) {
+ if (cameraX == null) {
+ return
+ }
+ cameraX!!.cameraFactory.cameraCoordinator.cameraOperatingMode = cameraOperatingMode
+ }
+
+ private var activeConcurrentCameraInfos: List<CameraInfo>
+ get() {
+ if (cameraX == null) {
+ return java.util.ArrayList()
+ }
+ return cameraX!!.cameraFactory.cameraCoordinator.activeConcurrentCameraInfos
+ }
+ private set(cameraInfos) {
+ if (cameraX == null) {
+ return
+ }
+ cameraX!!.cameraFactory.cameraCoordinator.activeConcurrentCameraInfos = cameraInfos
+ }
+
+ companion object {
+ private const val TAG = "LifecycleCameraProvider"
+ }
+}
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
index eb30744..9814e90 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
@@ -18,55 +18,24 @@
import android.app.Application
import android.content.Context
-import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
-import androidx.annotation.GuardedBy
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.camera.core.Camera
-import androidx.camera.core.CameraEffect
-import androidx.camera.core.CameraFilter
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraInfoUnavailableException
import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraX
import androidx.camera.core.CameraXConfig
-import androidx.camera.core.CompositionSettings
import androidx.camera.core.ConcurrentCamera
-import androidx.camera.core.ConcurrentCamera.SingleCameraConfig
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
import androidx.camera.core.InitializationException
-import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
-import androidx.camera.core.ViewPort
-import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT
-import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE
-import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED
-import androidx.camera.core.concurrent.CameraCoordinator.CameraOperatingMode
-import androidx.camera.core.impl.CameraConfig
-import androidx.camera.core.impl.CameraConfigs
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
-import androidx.camera.core.impl.RestrictedCameraInfo
-import androidx.camera.core.impl.UseCaseConfig
-import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
-import androidx.camera.core.impl.utils.ContextUtil
-import androidx.camera.core.impl.utils.Threads
import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.impl.utils.futures.FutureCallback
-import androidx.camera.core.impl.utils.futures.FutureChain
import androidx.camera.core.impl.utils.futures.Futures
-import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.lifecycle.ProcessCameraProvider.Companion.getInstance
-import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.core.util.Preconditions
-import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.tracing.trace
import com.google.common.util.concurrent.ListenableFuture
-import java.util.Objects
-import java.util.Objects.requireNonNull
/**
* A singleton which can be used to bind the lifecycle of cameras to any [LifecycleOwner] within an
@@ -82,684 +51,85 @@
*
* This is the standard provider for applications to use.
*/
-public class ProcessCameraProvider private constructor() : LifecycleCameraProvider {
- private val mLock = Any()
+// TODO: Remove the annotation when LifecycleCameraProvider is ready to be public.
+@Suppress("HiddenSuperclass")
+public class ProcessCameraProvider
+private constructor(private val lifecycleCameraProvider: LifecycleCameraProviderImpl) :
+ LifecycleCameraProvider {
- @GuardedBy("mLock") private var mCameraXConfigProvider: CameraXConfig.Provider? = null
-
- @GuardedBy("mLock") private var mCameraXInitializeFuture: ListenableFuture<CameraX>? = null
-
- @GuardedBy("mLock") private var mCameraXShutdownFuture = Futures.immediateFuture<Void>(null)
-
- private val mLifecycleCameraRepository = LifecycleCameraRepository()
- private var mCameraX: CameraX? = null
- private var mContext: Context? = null
-
- @GuardedBy("mLock")
- private val mCameraInfoMap: MutableMap<CameraUseCaseAdapter.CameraId, RestrictedCameraInfo> =
- HashMap()
-
- /**
- * Allows shutting down this ProcessCameraProvider instance so a new instance can be retrieved
- * by [getInstance].
- *
- * Once shutdownAsync is invoked, a new instance can be retrieved with [getInstance].
- *
- * This method should be used for testing purposes only. Along with [configureInstance], this
- * allows the process camera provider to be used in test suites which may need to initialize
- * CameraX in different ways in between tests.
- *
- * @return A [ListenableFuture] representing the shutdown status. Cancellation of this future is
- * a no-op.
- */
- @VisibleForTesting
- public fun shutdownAsync(): ListenableFuture<Void> {
- Threads.runOnMainSync {
- unbindAll()
- mLifecycleCameraRepository.clear()
- }
-
- if (mCameraX != null) {
- mCameraX!!.cameraFactory.cameraCoordinator.shutdown()
- }
-
- val shutdownFuture =
- if (mCameraX != null) mCameraX!!.shutdown() else Futures.immediateFuture<Void>(null)
-
- synchronized(mLock) {
- mCameraXConfigProvider = null
- mCameraXInitializeFuture = null
- mCameraXShutdownFuture = shutdownFuture
- mCameraInfoMap.clear()
- }
- mCameraX = null
- mContext = null
- return shutdownFuture
+ override fun isBound(useCase: UseCase): Boolean {
+ return lifecycleCameraProvider.isBound(useCase)
}
@MainThread
- public override fun bindToLifecycle(
+ override fun unbind(vararg useCases: UseCase?) {
+ return lifecycleCameraProvider.unbind(*useCases)
+ }
+
+ @MainThread
+ override fun unbindAll() {
+ return lifecycleCameraProvider.unbindAll()
+ }
+
+ @MainThread
+ override fun bindToLifecycle(
lifecycleOwner: LifecycleOwner,
cameraSelector: CameraSelector,
vararg useCases: UseCase?
- ): Camera =
- trace("CX:bindToLifecycle") {
- if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
- throw UnsupportedOperationException(
- "bindToLifecycle for single camera is not supported in concurrent camera mode, " +
- "call unbindAll() first"
- )
- }
- cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
- val camera =
- bindToLifecycle(
- lifecycleOwner,
- cameraSelector,
- null,
- CompositionSettings.DEFAULT,
- CompositionSettings.DEFAULT,
- null,
- emptyList<CameraEffect>(),
- *useCases
- )
- return@trace camera
- }
+ ): Camera {
+ return lifecycleCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
+ }
- /**
- * Binds a [UseCaseGroup] to a [LifecycleOwner].
- *
- * Similar to [bindToLifecycle], with the addition that the bound collection of [UseCase] share
- * parameters defined by [UseCaseGroup] such as consistent camera sensor rect across all
- * [UseCase]s.
- *
- * If one [UseCase] is in multiple [UseCaseGroup]s, it will be linked to the [UseCaseGroup] in
- * the latest [bindToLifecycle] call.
- *
- * @throws UnsupportedOperationException If the camera is configured in concurrent mode.
- */
@MainThread
- public override fun bindToLifecycle(
+ override fun bindToLifecycle(
lifecycleOwner: LifecycleOwner,
cameraSelector: CameraSelector,
useCaseGroup: UseCaseGroup
- ): Camera =
- trace("CX:bindToLifecycle-UseCaseGroup") {
- if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
- throw UnsupportedOperationException(
- "bindToLifecycle for single camera is not supported in concurrent camera mode, " +
- "call unbindAll() first."
- )
- }
- cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
- val camera =
- bindToLifecycle(
- lifecycleOwner,
- cameraSelector,
- null,
- CompositionSettings.DEFAULT,
- CompositionSettings.DEFAULT,
- useCaseGroup.viewPort,
- useCaseGroup.effects,
- *useCaseGroup.useCases.toTypedArray<UseCase>()
- )
- return@trace camera
- }
-
- @MainThread
- public override fun bindToLifecycle(
- singleCameraConfigs: List<SingleCameraConfig?>
- ): ConcurrentCamera =
- trace("CX:bindToLifecycle-Concurrent") {
- if (singleCameraConfigs.size < 2) {
- throw IllegalArgumentException("Concurrent camera needs two camera configs.")
- }
-
- if (singleCameraConfigs.size > 2) {
- throw IllegalArgumentException(
- "Concurrent camera is only supporting two cameras at maximum."
- )
- }
-
- val firstCameraConfig = singleCameraConfigs[0]!!
- val secondCameraConfig = singleCameraConfigs[1]!!
-
- val cameras: MutableList<Camera> = ArrayList()
- if (
- firstCameraConfig.cameraSelector.lensFacing ==
- secondCameraConfig.cameraSelector.lensFacing
- ) {
- if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
- throw UnsupportedOperationException(
- "Camera is already running, call unbindAll() before binding more cameras."
- )
- }
- if (
- firstCameraConfig.lifecycleOwner != secondCameraConfig.lifecycleOwner ||
- firstCameraConfig.useCaseGroup.viewPort !=
- secondCameraConfig.useCaseGroup.viewPort ||
- firstCameraConfig.useCaseGroup.effects !=
- secondCameraConfig.useCaseGroup.effects
- ) {
- throw IllegalArgumentException(
- "Two camera configs need to have the same lifecycle owner, view port and " +
- "effects."
- )
- }
- val lifecycleOwner = firstCameraConfig.lifecycleOwner
- val cameraSelector = firstCameraConfig.cameraSelector
- val viewPort = firstCameraConfig.useCaseGroup.viewPort
- val effects = firstCameraConfig.useCaseGroup.effects
- val useCases: MutableList<UseCase> = ArrayList()
- for (config: SingleCameraConfig? in singleCameraConfigs) {
- // Connect physical camera id with use case.
- for (useCase: UseCase in config!!.useCaseGroup.useCases) {
- config.cameraSelector.physicalCameraId?.let {
- useCase.setPhysicalCameraId(it)
- }
- }
- useCases.addAll(config.useCaseGroup.useCases)
- }
-
- cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
- val camera =
- bindToLifecycle(
- lifecycleOwner,
- cameraSelector,
- null,
- CompositionSettings.DEFAULT,
- CompositionSettings.DEFAULT,
- viewPort,
- effects,
- *useCases.toTypedArray<UseCase>()
- )
- cameras.add(camera)
- } else {
- if (!mContext!!.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
- throw UnsupportedOperationException(
- "Concurrent camera is not supported on the device."
- )
- }
-
- if (cameraOperatingMode == CAMERA_OPERATING_MODE_SINGLE) {
- throw UnsupportedOperationException(
- "Camera is already running, call unbindAll() before binding more cameras."
- )
- }
-
- val cameraInfosToBind: MutableList<CameraInfo> = ArrayList()
- val firstCameraInfo: CameraInfo
- val secondCameraInfo: CameraInfo
- try {
- firstCameraInfo = getCameraInfo(firstCameraConfig.cameraSelector)
- secondCameraInfo = getCameraInfo(secondCameraConfig.cameraSelector)
- } catch (e: IllegalArgumentException) {
- throw IllegalArgumentException("Invalid camera selectors in camera configs.")
- }
- cameraInfosToBind.add(firstCameraInfo)
- cameraInfosToBind.add(secondCameraInfo)
- if (
- activeConcurrentCameraInfos.isNotEmpty() &&
- cameraInfosToBind != activeConcurrentCameraInfos
- ) {
- throw UnsupportedOperationException(
- "Cameras are already running, call unbindAll() before binding more cameras."
- )
- }
-
- cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
-
- // For dual camera video capture, we are only supporting two use cases:
- // Preview + VideoCapture. If ImageCapture support is added, the validation logic
- // will be updated accordingly.
- var isDualCameraVideoCapture = false
- if (
- Objects.equals(
- firstCameraConfig.useCaseGroup.useCases,
- secondCameraConfig.useCaseGroup.useCases
- ) && firstCameraConfig.useCaseGroup.useCases.size == 2
- ) {
- val useCase0 = firstCameraConfig.useCaseGroup.useCases[0]
- val useCase1 = firstCameraConfig.useCaseGroup.useCases[1]
- isDualCameraVideoCapture =
- (isVideoCapture(useCase0) && isPreview(useCase1)) ||
- (isPreview(useCase0) && isVideoCapture(useCase1))
- }
-
- if (isDualCameraVideoCapture) {
- cameras.add(
- bindToLifecycle(
- firstCameraConfig.lifecycleOwner,
- firstCameraConfig.cameraSelector,
- secondCameraConfig.cameraSelector,
- firstCameraConfig.compositionSettings,
- secondCameraConfig.compositionSettings,
- firstCameraConfig.useCaseGroup.viewPort,
- firstCameraConfig.useCaseGroup.effects,
- *firstCameraConfig.useCaseGroup.useCases.toTypedArray<UseCase>(),
- )
- )
- } else {
- for (config: SingleCameraConfig? in singleCameraConfigs) {
- val camera =
- bindToLifecycle(
- config!!.lifecycleOwner,
- config.cameraSelector,
- null,
- CompositionSettings.DEFAULT,
- CompositionSettings.DEFAULT,
- config.useCaseGroup.viewPort,
- config.useCaseGroup.effects,
- *config.useCaseGroup.useCases.toTypedArray<UseCase>()
- )
- cameras.add(camera)
- }
- }
- activeConcurrentCameraInfos = cameraInfosToBind
- }
- return@trace ConcurrentCamera(cameras)
- }
-
- private fun isVideoCapture(useCase: UseCase): Boolean {
- return useCase.currentConfig.containsOption(UseCaseConfig.OPTION_CAPTURE_TYPE) &&
- useCase.currentConfig.captureType == CaptureType.VIDEO_CAPTURE
+ ): Camera {
+ return lifecycleCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
}
- private fun isPreview(useCase: UseCase): Boolean {
- return useCase is Preview
- }
-
- /**
- * Binds [ViewPort] and a collection of [UseCase] to a [LifecycleOwner].
- *
- * The state of the lifecycle will determine when the cameras are open, started, stopped and
- * closed. When started, the use cases receive camera data.
- *
- * Binding to a [LifecycleOwner] in state currently in [Lifecycle.State.STARTED] or greater will
- * also initialize and start data capture. If the camera was already running this may cause a
- * new initialization to occur temporarily stopping data from the camera before restarting it.
- *
- * Multiple use cases can be bound via adding them all to a single [bindToLifecycle] call, or by
- * using multiple [bindToLifecycle] calls. Using a single call that includes all the use cases
- * helps to set up a camera session correctly for all uses cases, such as by allowing
- * determination of resolutions depending on all the use cases bound being bound. If the use
- * cases are bound separately, it will find the supported resolution with the priority depending
- * on the binding sequence. If the use cases are bound with a single call, it will find the
- * supported resolution with the priority in sequence of [ImageCapture], [Preview] and then
- * [ImageAnalysis]. The resolutions that can be supported depends on the camera device hardware
- * level that there are some default guaranteed resolutions listed in
- * [android.hardware.camera2.CameraDevice.createCaptureSession].
- *
- * Currently up to 3 use cases may be bound to a [Lifecycle] at any time. Exceeding capability
- * of target camera device will throw an IllegalArgumentException.
- *
- * A [UseCase] should only be bound to a single lifecycle and camera selector a time. Attempting
- * to bind a use case to a lifecycle when it is already bound to another lifecycle is an error,
- * and the use case binding will not change. Attempting to bind the same use case to multiple
- * camera selectors is also an error and will not change the binding.
- *
- * If different use cases are bound to different camera selectors that resolve to distinct
- * cameras, but the same lifecycle, only one of the cameras will operate at a time. The
- * non-operating camera will not become active until it is the only camera with use cases bound.
- *
- * The [Camera] returned is determined by the given camera selector, plus other internal
- * requirements, possibly from use case configurations. The camera returned from
- * [bindToLifecycle] may differ from the camera determined solely by a camera selector. If the
- * camera selector can't resolve a camera under the requirements, an [IllegalArgumentException]
- * will be thrown.
- *
- * Only [UseCase] bound to latest active [Lifecycle] can keep alive. [UseCase] bound to other
- * [Lifecycle] will be stopped.
- *
- * @param lifecycleOwner The [LifecycleOwner] which controls the lifecycle transitions of the
- * use cases.
- * @param primaryCameraSelector The primary camera selector which determines the camera to use
- * for set of use cases.
- * @param secondaryCameraSelector The secondary camera selector in dual camera case.
- * @param primaryCompositionSettings The composition settings for the primary camera.
- * @param secondaryCompositionSettings The composition settings for the secondary camera.
- * @param viewPort The viewPort which represents the visible camera sensor rect.
- * @param effects The effects applied to the camera outputs.
- * @param useCases The use cases to bind to a lifecycle.
- * @return The [Camera] instance which is determined by the camera selector and internal
- * requirements.
- * @throws IllegalStateException If the use case has already been bound to another lifecycle or
- * method is not called on main thread.
- * @throws IllegalArgumentException If the provided camera selector is unable to resolve a
- * camera to be used for the given use cases.
- */
- @Suppress("unused")
- internal fun bindToLifecycle(
- lifecycleOwner: LifecycleOwner,
- primaryCameraSelector: CameraSelector,
- secondaryCameraSelector: CameraSelector?,
- primaryCompositionSettings: CompositionSettings,
- secondaryCompositionSettings: CompositionSettings,
- viewPort: ViewPort?,
- effects: List<CameraEffect?>,
- vararg useCases: UseCase?
- ): Camera =
- trace("CX:bindToLifecycle-internal") {
- Threads.checkMainThread()
- // TODO(b/153096869): override UseCase's target rotation.
-
- // Get the LifecycleCamera if existed.
- val primaryCameraInternal =
- primaryCameraSelector.select(mCameraX!!.cameraRepository.cameras)
- primaryCameraInternal.setPrimary(true)
- val primaryRestrictedCameraInfo =
- getCameraInfo(primaryCameraSelector) as RestrictedCameraInfo
-
- var secondaryCameraInternal: CameraInternal? = null
- var secondaryRestrictedCameraInfo: RestrictedCameraInfo? = null
- if (secondaryCameraSelector != null) {
- secondaryCameraInternal =
- secondaryCameraSelector.select(mCameraX!!.cameraRepository.cameras)
- secondaryCameraInternal.setPrimary(false)
- secondaryRestrictedCameraInfo =
- getCameraInfo(secondaryCameraSelector) as RestrictedCameraInfo
- }
-
- var lifecycleCameraToBind =
- mLifecycleCameraRepository.getLifecycleCamera(
- lifecycleOwner,
- CameraUseCaseAdapter.generateCameraId(
- primaryRestrictedCameraInfo,
- secondaryRestrictedCameraInfo
- )
- )
-
- // Check if there's another camera that has already been bound.
- val lifecycleCameras = mLifecycleCameraRepository.lifecycleCameras
- useCases.filterNotNull().forEach { useCase ->
- for (lifecycleCamera: LifecycleCamera in lifecycleCameras) {
- if (
- lifecycleCamera.isBound(useCase) && lifecycleCamera != lifecycleCameraToBind
- ) {
- throw IllegalStateException(
- String.format(
- "Use case %s already bound to a different lifecycle.",
- useCase
- )
- )
- }
- }
- }
-
- // Create the LifecycleCamera if there's no existing one that can be used.
- if (lifecycleCameraToBind == null) {
- lifecycleCameraToBind =
- mLifecycleCameraRepository.createLifecycleCamera(
- lifecycleOwner,
- CameraUseCaseAdapter(
- primaryCameraInternal,
- secondaryCameraInternal,
- primaryRestrictedCameraInfo,
- secondaryRestrictedCameraInfo,
- primaryCompositionSettings,
- secondaryCompositionSettings,
- mCameraX!!.cameraFactory.cameraCoordinator,
- mCameraX!!.cameraDeviceSurfaceManager,
- mCameraX!!.defaultConfigFactory
- )
- )
- }
-
- if (useCases.isEmpty()) {
- return@trace lifecycleCameraToBind!!
- }
-
- mLifecycleCameraRepository.bindToLifecycleCamera(
- lifecycleCameraToBind!!,
- viewPort,
- effects,
- listOf(*useCases),
- mCameraX!!.cameraFactory.cameraCoordinator
- )
-
- return@trace lifecycleCameraToBind
- }
-
- public override fun isBound(useCase: UseCase): Boolean {
- for (lifecycleCamera: LifecycleCamera in mLifecycleCameraRepository.lifecycleCameras) {
- if (lifecycleCamera.isBound(useCase)) {
- return true
- }
- }
-
- return false
- }
-
- /**
- * Unbinds all specified use cases from the lifecycle.
- *
- * This will initiate a close of every open camera which has zero [UseCase] associated with it
- * at the end of this call.
- *
- * If a use case in the argument list is not bound, then it is simply ignored.
- *
- * After unbinding a UseCase, the UseCase can be and bound to another [Lifecycle] however
- * listeners and settings should be reset by the application.
- *
- * @param useCases The collection of use cases to remove.
- * @throws IllegalStateException If not called on main thread.
- * @throws UnsupportedOperationException If called in concurrent mode.
- */
@MainThread
- public override fun unbind(vararg useCases: UseCase?): Unit =
- trace("CX:unbind") {
- Threads.checkMainThread()
-
- if (cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT) {
- throw UnsupportedOperationException(
- "Unbind usecase is not supported in concurrent camera mode, call unbindAll() first."
- )
- }
-
- mLifecycleCameraRepository.unbind(listOf(*useCases))
- }
-
- @MainThread
- public override fun unbindAll(): Unit =
- trace("CX:unbindAll") {
- Threads.checkMainThread()
- cameraOperatingMode = CAMERA_OPERATING_MODE_UNSPECIFIED
- mLifecycleCameraRepository.unbindAll()
- }
-
- @Throws(CameraInfoUnavailableException::class)
- override fun hasCamera(cameraSelector: CameraSelector): Boolean =
- trace("CX:hasCamera") {
- try {
- cameraSelector.select(mCameraX!!.cameraRepository.cameras)
- } catch (e: IllegalArgumentException) {
- return@trace false
- }
-
- return@trace true
- }
+ override fun bindToLifecycle(
+ singleCameraConfigs: List<ConcurrentCamera.SingleCameraConfig?>
+ ): ConcurrentCamera {
+ return lifecycleCameraProvider.bindToLifecycle(singleCameraConfigs)
+ }
override val availableCameraInfos: List<CameraInfo>
- get() =
- trace("CX:getAvailableCameraInfos") {
- val availableCameraInfos: MutableList<CameraInfo> = ArrayList()
- val cameras: Set<CameraInternal> = mCameraX!!.cameraRepository.cameras
- for (camera: CameraInternal in cameras) {
- availableCameraInfos.add(camera.cameraInfo)
- }
- return@trace availableCameraInfos
- }
+ get() = lifecycleCameraProvider.availableCameraInfos
final override val availableConcurrentCameraInfos: List<List<CameraInfo>>
- get() =
- trace("CX:getAvailableConcurrentCameraInfos") {
- requireNonNull(mCameraX)
- requireNonNull(mCameraX!!.cameraFactory.cameraCoordinator)
- val concurrentCameraSelectorLists =
- mCameraX!!.cameraFactory.cameraCoordinator.concurrentCameraSelectors
-
- val availableConcurrentCameraInfos: MutableList<List<CameraInfo>> = ArrayList()
- for (cameraSelectors in concurrentCameraSelectorLists) {
- val cameraInfos: MutableList<CameraInfo> = ArrayList()
- for (cameraSelector in cameraSelectors) {
- var cameraInfo: CameraInfo
- try {
- cameraInfo = getCameraInfo(cameraSelector)
- } catch (e: IllegalArgumentException) {
- continue
- }
- cameraInfos.add(cameraInfo)
- }
- availableConcurrentCameraInfos.add(cameraInfos)
- }
- return@trace availableConcurrentCameraInfos
- }
-
- override fun getCameraInfo(cameraSelector: CameraSelector): CameraInfo =
- trace("CX:getCameraInfo") {
- val cameraInfoInternal =
- cameraSelector.select(mCameraX!!.cameraRepository.cameras).cameraInfoInternal
- val cameraConfig = getCameraConfig(cameraSelector, cameraInfoInternal)
-
- val key =
- CameraUseCaseAdapter.CameraId.create(
- cameraInfoInternal.cameraId,
- cameraConfig.compatibilityId
- )
- var restrictedCameraInfo: RestrictedCameraInfo?
- synchronized(mLock) {
- restrictedCameraInfo = mCameraInfoMap[key]
- if (restrictedCameraInfo == null) {
- restrictedCameraInfo = RestrictedCameraInfo(cameraInfoInternal, cameraConfig)
- mCameraInfoMap[key] = restrictedCameraInfo!!
- }
- }
-
- return@trace restrictedCameraInfo!!
- }
+ get() = lifecycleCameraProvider.availableConcurrentCameraInfos
final override val isConcurrentCameraModeOn: Boolean
- @MainThread get() = cameraOperatingMode == CAMERA_OPERATING_MODE_CONCURRENT
+ @MainThread get() = lifecycleCameraProvider.isConcurrentCameraModeOn
- private fun getOrCreateCameraXInstance(context: Context): ListenableFuture<CameraX> {
- synchronized(mLock) {
- if (mCameraXInitializeFuture != null) {
- return mCameraXInitializeFuture as ListenableFuture<CameraX>
- }
- val cameraX = CameraX(context, mCameraXConfigProvider)
-
- mCameraXInitializeFuture =
- CallbackToFutureAdapter.getFuture { completer ->
- synchronized(mLock) {
- val future: ListenableFuture<Void> =
- FutureChain.from(mCameraXShutdownFuture)
- .transformAsync(
- { cameraX.initializeFuture },
- CameraXExecutors.directExecutor()
- )
- Futures.addCallback(
- future,
- object : FutureCallback<Void?> {
- override fun onSuccess(result: Void?) {
- completer.set(cameraX)
- }
-
- override fun onFailure(t: Throwable) {
- completer.setException(t)
- }
- },
- CameraXExecutors.directExecutor()
- )
- }
-
- "ProcessCameraProvider-initializeCameraX"
- }
-
- return mCameraXInitializeFuture as ListenableFuture<CameraX>
- }
+ @Throws(CameraInfoUnavailableException::class)
+ override fun hasCamera(cameraSelector: CameraSelector): Boolean {
+ return lifecycleCameraProvider.hasCamera(cameraSelector)
}
- private fun configureInstanceInternal(cameraXConfig: CameraXConfig) =
- trace("CX:configureInstanceInternal") {
- synchronized(mLock) {
- Preconditions.checkNotNull(cameraXConfig)
- Preconditions.checkState(
- mCameraXConfigProvider == null,
- "CameraX has already been configured. To use a different configuration, " +
- "shutdown() must be called."
- )
- mCameraXConfigProvider = CameraXConfig.Provider { cameraXConfig }
- }
- }
-
- private fun getCameraConfig(
- cameraSelector: CameraSelector,
- cameraInfo: CameraInfo
- ): CameraConfig {
- var cameraConfig: CameraConfig? = null
- for (cameraFilter: CameraFilter in cameraSelector.cameraFilterSet) {
- if (cameraFilter.identifier != CameraFilter.DEFAULT_ID) {
- val extendedCameraConfig =
- ExtendedCameraConfigProviderStore.getConfigProvider(cameraFilter.identifier)
- .getConfig(cameraInfo, (mContext)!!)
- if (extendedCameraConfig == null) { // ignore IDs unrelated to camera configs.
- continue
- }
-
- // Only allows one camera config now.
- if (cameraConfig != null) {
- throw IllegalArgumentException(
- "Cannot apply multiple extended camera configs at the same time."
- )
- }
- cameraConfig = extendedCameraConfig
- }
- }
-
- if (cameraConfig == null) {
- cameraConfig = CameraConfigs.defaultConfig()
- }
- return cameraConfig
+ override fun getCameraInfo(cameraSelector: CameraSelector): CameraInfo {
+ return lifecycleCameraProvider.getCameraInfo(cameraSelector)
}
- private fun setCameraX(cameraX: CameraX) {
- mCameraX = cameraX
+ // TODO: Remove the annotation when LifecycleCameraProvider is ready to be public.
+ @VisibleForTesting
+ override fun shutdownAsync(): ListenableFuture<Void> {
+ return lifecycleCameraProvider.shutdownAsync()
}
- private fun setContext(context: Context) {
- mContext = context
+ private fun initAsync(context: Context): ListenableFuture<Void> {
+ return lifecycleCameraProvider.initAsync(context, null)
}
- @get:CameraOperatingMode
- private var cameraOperatingMode: Int
- get() {
- if (mCameraX == null) {
- return CAMERA_OPERATING_MODE_UNSPECIFIED
- }
- return mCameraX!!.cameraFactory.cameraCoordinator.cameraOperatingMode
- }
- private set(cameraOperatingMode) {
- if (mCameraX == null) {
- return
- }
- mCameraX!!.cameraFactory.cameraCoordinator.cameraOperatingMode = cameraOperatingMode
- }
-
- private var activeConcurrentCameraInfos: List<CameraInfo>
- get() {
- if (mCameraX == null) {
- return java.util.ArrayList()
- }
- return mCameraX!!.cameraFactory.cameraCoordinator.activeConcurrentCameraInfos
- }
- private set(cameraInfos) {
- if (mCameraX == null) {
- return
- }
- mCameraX!!.cameraFactory.cameraCoordinator.activeConcurrentCameraInfos = cameraInfos
- }
+ private fun configure(cameraXConfig: CameraXConfig) {
+ return lifecycleCameraProvider.configure(cameraXConfig)
+ }
public companion object {
- private val sAppInstance = ProcessCameraProvider()
+ private val sAppInstance = ProcessCameraProvider(LifecycleCameraProviderImpl())
/**
* Retrieves the ProcessCameraProvider associated with the current process.
@@ -800,12 +170,8 @@
public fun getInstance(context: Context): ListenableFuture<ProcessCameraProvider> {
Preconditions.checkNotNull(context)
return Futures.transform(
- sAppInstance.getOrCreateCameraXInstance(context),
- { cameraX ->
- sAppInstance.setCameraX(cameraX)
- sAppInstance.setContext(ContextUtil.getApplicationContext(context))
- sAppInstance
- },
+ sAppInstance.initAsync(context),
+ { sAppInstance },
CameraXExecutors.directExecutor()
)
}
@@ -833,7 +199,7 @@
* method from library code is not recommended** as the application owner should ultimately
* be in control of singleton configuration.
*
- * @param cameraXConfig configuration options for the singleton process camera provider
+ * @param cameraXConfig The configuration options for the singleton process camera provider
* instance.
* @throws IllegalStateException If the camera provider has already been configured by a
* previous call to `configureInstance()` or [getInstance].
@@ -841,6 +207,6 @@
@JvmStatic
@ExperimentalCameraProviderConfiguration
public fun configureInstance(cameraXConfig: CameraXConfig): Unit =
- trace("CX:configureInstance") { sAppInstance.configureInstanceInternal(cameraXConfig) }
+ trace("CX:configureInstance") { sAppInstance.configure(cameraXConfig) }
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java
index e4414b3..9814167 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java
@@ -52,6 +52,13 @@
}
/**
+ * Checks if the current device is emulator with API 21.
+ */
+ public static boolean isEmulator(int apiLevel) {
+ return Build.VERSION.SDK_INT == apiLevel && isEmulator();
+ }
+
+ /**
* Skips the test if the current device is emulator that doesn't support video recording.
*/
public static void skipVideoRecordingTestIfNotSupportedByEmulator() {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/InternalTestConvenience.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/InternalTestConvenience.kt
new file mode 100644
index 0000000..5587529
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/InternalTestConvenience.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl
+
+import android.app.Activity
+import androidx.camera.core.Logger
+import androidx.test.core.app.ActivityScenario
+
+/**
+ * Utility object to hold the convenience functions for internal testing.
+ *
+ * This should never be publicly available directly.
+ */
+public object InternalTestConvenience {
+ public const val LOG_TAG: String = "InternalTestConvenience"
+
+ /**
+ * Executes the given [block] function on this [ActivityScenario] resource and finally closes it
+ * without throwing in some cases for the convenience of camera tests.
+ *
+ * [ActivityScenario.close] may throw an exception in some cases due to bugs usually unrelated
+ * to a camera test. This function suppresses the exceptions in such cases for the convenience
+ * of tests where issues related to clearing up resources aren't related.
+ *
+ * @param block a function to process this resource.
+ * @return the result of [block] function invoked on this resource.
+ */
+ public inline fun <A : Activity, R> ActivityScenario<A>.useInCameraTest(
+ block: (ActivityScenario<A>) -> R,
+ ): R {
+ try {
+ return block(this)
+ } finally {
+ try {
+ close()
+ } catch (e: Throwable) {
+ if (AndroidUtil.isEmulator(28)) {
+ Logger.w(LOG_TAG, "Suppressed exception from ActivityScenario.close()", e)
+ } else {
+ // rethrow in case it's not a known issue
+ throw e
+ }
+ }
+ }
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageCaptureCallback.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt
similarity index 64%
rename from camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageCaptureCallback.kt
rename to camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt
index b600a40..135f64c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageCaptureCallback.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageCapturedCallback.kt
@@ -25,14 +25,25 @@
import androidx.camera.core.impl.utils.Exif
import com.google.common.truth.Truth
import java.io.ByteArrayInputStream
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeoutOrNull
-private const val CAPTURE_TIMEOUT = 15_000.toLong() // 15 seconds
+/**
+ * A fake implementation of the [ImageCapture.OnImageCapturedCallback] that is used for test.
+ *
+ * @param captureCount Number of captures to wait for.
+ * @property closeImageOnSuccess Whether to close images immediately on [onCaptureSuccess]
+ * callbacks. This is true by default. If set to false, it is the user's responsibility to close
+ * the images.
+ */
+public class FakeOnImageCapturedCallback(
+ captureCount: Int = 1,
+ private val closeImageOnSuccess: Boolean = true
+) : ImageCapture.OnImageCapturedCallback() {
+ public data class CapturedImage(val image: ImageProxy, val properties: ImageProperties)
-/** A fake implementation of the [ImageCapture.OnImageCapturedCallback] and used for test. */
-public class FakeImageCaptureCallback(captureCount: Int = 1) :
- ImageCapture.OnImageCapturedCallback() {
/** Data class of various image properties which are tested. */
public data class ImageProperties(
val size: Size? = null,
@@ -43,20 +54,34 @@
)
private val latch = CountdownDeferred(captureCount)
- public val results: MutableList<ImageProperties> = mutableListOf()
+
+ /**
+ * List of [CapturedImage] obtained in [onCaptureSuccess] callback.
+ *
+ * If [closeImageOnSuccess] is true, the [CapturedImage.image] will be closed as soon as
+ * `onCaptureSuccess` is invoked. Otherwise, it will be the user's responsibility to close the
+ * images.
+ */
+ public val results: MutableList<CapturedImage> = mutableListOf()
public val errors: MutableList<ImageCaptureException> = mutableListOf()
override fun onCaptureSuccess(image: ImageProxy) {
results.add(
- ImageProperties(
- size = Size(image.width, image.height),
- format = image.format,
- rotationDegrees = image.imageInfo.rotationDegrees,
- cropRect = image.cropRect,
- exif = getExif(image),
+ CapturedImage(
+ image = image,
+ properties =
+ ImageProperties(
+ size = Size(image.width, image.height),
+ format = image.format,
+ rotationDegrees = image.imageInfo.rotationDegrees,
+ cropRect = image.cropRect,
+ exif = getExif(image),
+ )
)
)
- image.close()
+ if (closeImageOnSuccess) {
+ image.close()
+ }
latch.countDown()
}
@@ -76,12 +101,12 @@
return null
}
- public suspend fun awaitCaptures(timeout: Long = CAPTURE_TIMEOUT) {
+ public suspend fun awaitCaptures(timeout: Duration = CAPTURE_TIMEOUT) {
Truth.assertThat(withTimeoutOrNull(timeout) { latch.await() }).isNotNull()
}
public suspend fun awaitCapturesAndAssert(
- timeout: Long = CAPTURE_TIMEOUT,
+ timeout: Duration = CAPTURE_TIMEOUT,
capturedImagesCount: Int = 0,
errorsCount: Int = 0
) {
@@ -110,4 +135,8 @@
deferredItems.forEach { it.await() }
}
}
+
+ public companion object {
+ private val CAPTURE_TIMEOUT = 15.seconds
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageSavedCallback.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageSavedCallback.kt
new file mode 100644
index 0000000..4ddec70
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeOnImageSavedCallback.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl.fakes
+
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import com.google.common.truth.Truth
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.withTimeoutOrNull
+
+/**
+ * A fake implementation of the [ImageCapture.OnImageCapturedCallback] that is used for test.
+ *
+ * @param captureCount Number of captures to wait for.
+ */
+public class FakeOnImageSavedCallback(captureCount: Int = 1) : ImageCapture.OnImageSavedCallback {
+ private val latch = CountdownDeferred(captureCount)
+ public val results: MutableList<ImageCapture.OutputFileResults> = mutableListOf()
+ public val errors: MutableList<ImageCaptureException> = mutableListOf()
+
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ results.add(outputFileResults)
+ latch.countDown()
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ errors.add(exception)
+ latch.countDown()
+ }
+
+ public suspend fun awaitCaptures(timeout: Duration = CAPTURE_TIMEOUT) {
+ Truth.assertThat(withTimeoutOrNull(timeout) { latch.await() }).isNotNull()
+ }
+
+ public suspend fun awaitCapturesAndAssert(
+ timeout: Duration = CAPTURE_TIMEOUT,
+ capturedImagesCount: Int = 0,
+ errorsCount: Int = 0
+ ) {
+ Truth.assertThat(withTimeoutOrNull(timeout) { latch.await() }).isNotNull()
+ Truth.assertThat(results.size).isEqualTo(capturedImagesCount)
+ Truth.assertThat(errors.size).isEqualTo(errorsCount)
+ }
+
+ private class CountdownDeferred(val count: Int) {
+
+ private val deferredItems =
+ mutableListOf<CompletableDeferred<Unit>>().apply {
+ repeat(count) { add(CompletableDeferred()) }
+ }
+ private var index = 0
+
+ fun countDown() {
+ if (index < count) {
+ deferredItems[index++].complete(Unit)
+ } else {
+ throw IllegalStateException("Countdown already finished")
+ }
+ }
+
+ suspend fun await() {
+ deferredItems.forEach { it.await() }
+ }
+ }
+
+ public companion object {
+ private val CAPTURE_TIMEOUT = 15.seconds
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/testrule/PreTestRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/testrule/PreTestRule.kt
new file mode 100644
index 0000000..6c6c87c
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/testrule/PreTestRule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl.testrule
+
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/** A simple [TestRule] that executes the provided code block before test is started. */
+public class PreTestRule(private val preRunBlock: () -> Unit) : TestRule {
+ override fun apply(base: Statement, description: Description): Statement {
+ preRunBlock()
+ return base
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/Recording.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/Recording.kt
index a1d2fa5..cbe00b35 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/Recording.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/Recording.kt
@@ -55,6 +55,7 @@
private val recorder: Recorder,
private val outputOptions: OutputOptions,
private val withAudio: Boolean,
+ private val initialAudioMuted: Boolean,
private val asPersistentRecording: Boolean,
private val recordingStopStrategy: (androidx.camera.video.Recording, Recorder) -> Unit,
private val callbackExecutor: Executor,
@@ -73,7 +74,7 @@
else -> throw AssertionError()
}.apply {
if (withAudio) {
- withAudioEnabled()
+ withAudioEnabled(initialAudioMuted)
}
if (asPersistentRecording) {
asPersistentRecording()
@@ -221,7 +222,7 @@
return this
}
- private fun verifyMute(muted: Boolean) {
+ public fun verifyMute(muted: Boolean) {
// TODO(b/274862085): Change to verify the status events consecutively having MUTED state
// by adding the utility to MockConsumer.
try {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/RecordingSession.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/RecordingSession.kt
index f8866da..014de9f 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/RecordingSession.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/video/RecordingSession.kt
@@ -50,6 +50,7 @@
recorder: Recorder = defaults.recorder,
outputOptions: OutputOptions = defaults.outputOptionsProvider.invoke(),
withAudio: Boolean = defaults.withAudio,
+ initialAudioMuted: Boolean = false,
asPersistentRecording: Boolean = false,
): Recording {
return Recording(
@@ -57,6 +58,7 @@
recorder = recorder,
outputOptions = outputOptions,
withAudio = withAudio,
+ initialAudioMuted = initialAudioMuted,
asPersistentRecording = asPersistentRecording,
recordingStopStrategy = defaults.recordingStopStrategy,
callbackExecutor = defaults.callbackExecutor,
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index beba6c4..8721077 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -84,8 +84,9 @@
public final class PendingRecording {
method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public androidx.camera.video.PendingRecording asPersistentRecording();
- method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor listenerExecutor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent> listener);
method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled();
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled(optional boolean initialMuted);
}
public class Quality {
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index beba6c4..8721077 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -84,8 +84,9 @@
public final class PendingRecording {
method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public androidx.camera.video.PendingRecording asPersistentRecording();
- method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor listenerExecutor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent> listener);
method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled();
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled(optional boolean initialMuted);
}
public class Quality {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
deleted file mode 100644
index c8a2dbb..0000000
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
+++ /dev/null
@@ -1,195 +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.camera.video
-
-import android.content.Context
-import android.media.MediaCodec
-import android.media.MediaCodecInfo
-import android.os.Build
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.pipe.integration.CameraPipeConfig
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
-import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
-import androidx.camera.core.CameraXConfig
-import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
-import androidx.camera.testing.impl.CameraPipeConfigTestRule
-import androidx.camera.testing.impl.CameraUtil
-import androidx.camera.testing.impl.CameraXUtil
-import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy
-import androidx.camera.video.internal.compat.quirk.DeviceQuirks
-import androidx.camera.video.internal.compat.quirk.MediaCodecInfoReportIncorrectInfoQuirk
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.collect.Range
-import com.google.common.truth.Truth.assertWithMessage
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-/**
- * Test used to find out compatibility issue.
- *
- * All tests in this file should always pass. Every time we want to add a new test, it means there
- * is also a corresponding quirk in the codebase and we want to find more devices with the same root
- * cause. Tests should use [assumeTrue] to skip related quirks so that the problematic device will
- * pass the test. Once a new failure is found in the mobile harness test results, we should add the
- * device to the relevant quirk to pass the test.
- */
-@SmallTest
-@RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = 21)
-class DeviceCompatibilityTest(
- private val implName: String,
- private val cameraConfig: CameraXConfig,
-) {
-
- private val context: Context = ApplicationProvider.getApplicationContext()
- private val zeroRange by lazy { android.util.Range.create(0, 0) }
-
- @get:Rule
- val cameraPipeConfigTestRule =
- CameraPipeConfigTestRule(
- active = implName == CameraPipeConfig::class.simpleName,
- )
-
- @get:Rule
- val cameraRule =
- CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
- CameraUtil.PreTestCameraIdList(cameraConfig)
- )
-
- @Before
- fun setup() {
- CameraXUtil.initialize(context, cameraConfig).get()
- }
-
- @After
- fun tearDown() {
- CameraXUtil.shutdown().get(10, TimeUnit.SECONDS)
- }
-
- @Test
- fun mediaCodecInfoShouldSupportEncoderProfilesSizes() {
- assumeTrue(DeviceQuirks.get(MediaCodecInfoReportIncorrectInfoQuirk::class.java) == null)
-
- // Arrange: Collect all supported profiles from default back/front camera.
- val supportedProfiles = mutableListOf<VideoValidatedEncoderProfilesProxy>()
- supportedProfiles.addAll(getSupportedProfiles(DEFAULT_BACK_CAMERA))
- supportedProfiles.addAll(getSupportedProfiles(DEFAULT_FRONT_CAMERA))
- assumeTrue(supportedProfiles.isNotEmpty())
-
- supportedProfiles.forEach { profile ->
- // Arrange: Find the codec and its video capabilities.
- // If mime is null, skip the test instead of failing it since this isn't the purpose
- // of the test.
- val videoProfile = profile.defaultVideoProfile
- val mime = videoProfile.mediaType
- if (mime == VideoProfileProxy.MEDIA_TYPE_NONE) {
- return@forEach
- }
- val capabilities =
- MediaCodec.createEncoderByType(mime).let { codec ->
- try {
- codec.codecInfo.getCapabilitiesForType(mime).videoCapabilities
- } finally {
- codec.release()
- }
- }
-
- // Act.
- val (width, height) = videoProfile.width to videoProfile.height
- // Pass if VideoCapabilities.isSizeSupported() is true
- if (capabilities.isSizeSupported(width, height)) {
- return@forEach
- }
-
- val supportedWidths = capabilities.supportedWidths
- val supportedHeights = capabilities.supportedHeights
- val supportedWidthsForHeight = capabilities.getWidthsForHeightQuietly(height)
- val supportedHeightForWidth = capabilities.getHeightsForWidthQuietly(width)
-
- // Assert.
- val msg =
- "Build.BRAND: ${Build.BRAND}, Build.MODEL: ${Build.MODEL} " +
- "mime: $mime, size: ${width}x$height is not in " +
- "supported widths $supportedWidths/$supportedWidthsForHeight " +
- "or heights $supportedHeights/$supportedHeightForWidth, " +
- "the width/height alignment is " +
- "${capabilities.widthAlignment}/${capabilities.heightAlignment}."
- assertWithMessage(msg).that(width).isIn(supportedWidths.toClosed())
- assertWithMessage(msg).that(height).isIn(supportedHeights.toClosed())
- assertWithMessage(msg).that(width).isIn(supportedWidthsForHeight.toClosed())
- assertWithMessage(msg).that(height).isIn(supportedHeightForWidth.toClosed())
- }
- }
-
- private fun getSupportedProfiles(
- cameraSelector: CameraSelector
- ): List<VideoValidatedEncoderProfilesProxy> {
- if (!CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!)) {
- return emptyList()
- }
-
- val cameraInfo = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo
- val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
-
- return videoCapabilities.supportedDynamicRanges.flatMap { dynamicRange ->
- videoCapabilities.getSupportedQualities(dynamicRange).map { quality ->
- videoCapabilities.getProfiles(quality, dynamicRange)!!
- }
- }
- }
-
- private fun android.util.Range<Int>.toClosed() = Range.closed(lower, upper)
-
- private fun MediaCodecInfo.VideoCapabilities.getWidthsForHeightQuietly(
- height: Int
- ): android.util.Range<Int> {
- return try {
- getSupportedWidthsFor(height)
- } catch (e: IllegalArgumentException) {
- zeroRange
- }
- }
-
- private fun MediaCodecInfo.VideoCapabilities.getHeightsForWidthQuietly(
- width: Int
- ): android.util.Range<Int> {
- return try {
- getSupportedHeightsFor(width)
- } catch (e: IllegalArgumentException) {
- zeroRange
- }
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun data() =
- listOf(
- arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
- arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
- )
- }
-}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index d2b1913..7afd920 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -771,6 +771,19 @@
}
@Test
+ fun mute_withInitialMuted() {
+ // Arrange.
+ val recording = recordingSession.createRecording(initialAudioMuted = true)
+
+ // Act.
+ recording.startAndVerify()
+
+ // Assert.
+ recording.verifyMute(true)
+ recording.stopAndVerify()
+ }
+
+ @Test
fun mute_noOpIfAudioDisabled() {
// Arrange.
val recording = recordingSession.createRecording(withAudio = false)
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
deleted file mode 100644
index bbcc840..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.video;
-
-import android.Manifest;
-import android.content.Context;
-
-import androidx.annotation.CheckResult;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresPermission;
-import androidx.camera.core.impl.utils.ContextUtil;
-import androidx.core.content.PermissionChecker;
-import androidx.core.util.Consumer;
-import androidx.core.util.Preconditions;
-
-import java.util.concurrent.Executor;
-
-/**
- * A recording that can be started at a future time.
- *
- * <p>A pending recording allows for configuration of a recording before it is started. Once a
- * pending recording is started with {@link #start(Executor, Consumer)}, any changes to the pending
- * recording will not affect the actual recording; any modifications to the recording will need
- * to occur through the controls of the {@link Recording} class returned by
- * {@link #start(Executor, Consumer)}.
- *
- * <p>A pending recording can be created using one of the {@link Recorder} methods for starting a
- * recording such as {@link Recorder#prepareRecording(Context, MediaStoreOutputOptions)}.
-
- * <p>There may be more settings that can only be changed per-recorder instead of per-recording,
- * because it requires expensive operations like reconfiguring the camera. For those settings, use
- * the {@link Recorder.Builder} methods to configure before creating the {@link Recorder}
- * instance, then create the pending recording with it.
- */
-public final class PendingRecording {
-
- private final Context mContext;
- private final Recorder mRecorder;
- private final OutputOptions mOutputOptions;
- private Consumer<VideoRecordEvent> mEventListener;
- private Executor mListenerExecutor;
- private boolean mAudioEnabled = false;
- private boolean mIsPersistent = false;
-
- PendingRecording(@NonNull Context context, @NonNull Recorder recorder,
- @NonNull OutputOptions options) {
- // Application context is sufficient for all our needs, so store that to avoid leaking
- // unused resources. For attribution, ContextUtil.getApplicationContext() will retain the
- // attribution tag from the original context.
- mContext = ContextUtil.getApplicationContext(context);
- mRecorder = recorder;
- mOutputOptions = options;
- }
-
- /**
- * Returns an application context which was retrieved from the {@link Context} used to
- * create this object.
- */
- @NonNull
- Context getApplicationContext() {
- return mContext;
- }
-
- @NonNull
- Recorder getRecorder() {
- return mRecorder;
- }
-
- @NonNull
- OutputOptions getOutputOptions() {
- return mOutputOptions;
- }
-
- @Nullable
- Executor getListenerExecutor() {
- return mListenerExecutor;
- }
-
- @Nullable
- Consumer<VideoRecordEvent> getEventListener() {
- return mEventListener;
- }
-
- boolean isAudioEnabled() {
- return mAudioEnabled;
- }
-
- boolean isPersistent() {
- return mIsPersistent;
- }
-
- /**
- * Enables audio to be recorded for this recording.
- *
- * <p>This method must be called prior to {@link #start(Executor, Consumer)} to enable audio
- * in the recording. If this method is not called, the {@link Recording} generated by
- * {@link #start(Executor, Consumer)} will not contain audio, and
- * {@link AudioStats#getAudioState()} will always return
- * {@link AudioStats#AUDIO_STATE_DISABLED} for all {@link RecordingStats} send to the listener
- * set passed to {@link #start(Executor, Consumer)}.
- *
- * <p>Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
- * permission; without it, recording will fail at {@link #start(Executor, Consumer)} with an
- * {@link IllegalStateException}.
- *
- * @return this pending recording
- * @throws IllegalStateException if the {@link Recorder} this recording is associated to
- * doesn't support audio.
- * @throws SecurityException if the {@link Manifest.permission#RECORD_AUDIO} permission
- * is denied for the current application.
- */
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- @NonNull
- public PendingRecording withAudioEnabled() {
- // Check permissions and throw a security exception if RECORD_AUDIO is not granted.
- if (PermissionChecker.checkSelfPermission(mContext, Manifest.permission.RECORD_AUDIO)
- == PermissionChecker.PERMISSION_DENIED) {
- throw new SecurityException("Attempted to enable audio for recording but application "
- + "does not have RECORD_AUDIO permission granted.");
- }
- Preconditions.checkState(mRecorder.isAudioSupported(), "The Recorder this recording is "
- + "associated to doesn't support audio.");
- mAudioEnabled = true;
- return this;
- }
-
- /**
- * Configures the recording to be a persistent recording.
- *
- * <p>A persistent recording will only be stopped by explicitly calling
- * {@link Recording#stop()} or {@link Recording#close()} and will ignore events that would
- * normally cause recording to stop, such as lifecycle events or explicit unbinding of a
- * {@link VideoCapture} use case that the recording's {@link Recorder} is attached to.
- *
- * <p>Even though lifecycle events or explicit unbinding use cases won't stop a persistent
- * recording, it will still stop the camera from producing data, resulting in the in-progress
- * persistent recording stopping getting data until the camera stream is activated again. For
- * example, when the activity goes into background, the recording will keep waiting for new
- * data to be recorded until the activity is back to foreground.
- *
- * <p>A {@link Recorder} instance is recommended to be associated with a single
- * {@link VideoCapture} instance, especially when using persistent recording. Otherwise, there
- * might be unexpected behavior. Any in-progress persistent recording created from the same
- * {@link Recorder} should be stopped before starting a new recording, even if the
- * {@link Recorder} is associated with a different {@link VideoCapture}.
- *
- * <p>To switch to a different camera stream while a recording is in progress, first create
- * the recording as persistent recording, then rebind the {@link VideoCapture} it's
- * associated with to a different camera. The implementation may be like:
- * <pre>{@code
- * // Prepare the Recorder and VideoCapture, then bind the VideoCapture to the back camera.
- * Recorder recorder = Recorder.Builder().build();
- * VideoCapture videoCapture = VideoCapture.withOutput(recorder);
- * cameraProvider.bindToLifecycle(
- * lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, videoCapture);
- *
- * // Prepare the persistent recording and start it.
- * Recording recording = recorder
- * .prepareRecording(context, outputOptions)
- * .asPersistentRecording()
- * .start(eventExecutor, eventListener);
- *
- * // Record from the back camera for a period of time.
- *
- * // Rebind the VideoCapture to the front camera.
- * cameraProvider.unbindAll();
- * cameraProvider.bindToLifecycle(
- * lifecycleOwner, CameraSelector.DEFAULT_FRONT_CAMERA, videoCapture);
- *
- * // Record from the front camera for a period of time.
- *
- * // Stop the recording explicitly.
- * recording.stop();
- * }</pre>
- *
- * <p>The audio data will still be recorded after the {@link VideoCapture} is unbound.
- * {@link Recording#pause() Pause} the recording first and {@link Recording#resume() resume} it
- * later to stop recording audio while rebinding use cases.
- *
- * <p>If the recording is unable to receive data from the new camera, possibly because of
- * incompatible surface combination, an exception will be thrown when binding to lifecycle.
- */
- @ExperimentalPersistentRecording
- @NonNull
- public PendingRecording asPersistentRecording() {
- mIsPersistent = true;
- return this;
- }
-
- /**
- * Starts the recording, making it an active recording.
- *
- * <p>Only a single recording can be active at a time, so if another recording is active,
- * this will throw an {@link IllegalStateException}.
- *
- * <p>If there are no errors starting the recording, the returned {@link Recording}
- * can be used to {@link Recording#pause() pause}, {@link Recording#resume() resume},
- * or {@link Recording#stop() stop} the recording.
- *
- * <p>Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
- * be the first event sent to the provided event listener.
- *
- * <p>If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
- * will be the first event sent to the provided listener, and information about the error can
- * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method. The returned
- * {@link Recording} will be in a finalized state, and all controls will be no-ops.
- *
- * <p>If the returned {@link Recording} is garbage collected, the recording will be
- * automatically stopped. A reference to the active recording must be maintained as long as
- * the recording needs to be active. If the recording is garbage collected, the
- * {@link VideoRecordEvent.Finalize} event will contain error
- * {@link VideoRecordEvent.Finalize#ERROR_RECORDING_GARBAGE_COLLECTED}.
- *
- * <p>The {@link Recording} will be stopped automatically if the {@link VideoCapture} its
- * {@link Recorder} is attached to is unbound unless it's created
- * {@link #asPersistentRecording() as a persistent recording}.
- *
- * @throws IllegalStateException if the associated Recorder currently has an unfinished
- * active recording.
- * @param listenerExecutor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
- */
- @NonNull
- @CheckResult
- public Recording start(
- @NonNull Executor listenerExecutor,
- @NonNull Consumer<VideoRecordEvent> listener) {
- Preconditions.checkNotNull(listenerExecutor, "Listener Executor can't be null.");
- Preconditions.checkNotNull(listener, "Event listener can't be null");
- mListenerExecutor = listenerExecutor;
- mEventListener = listener;
- return mRecorder.start(this);
- }
-}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.kt b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.kt
new file mode 100644
index 0000000..c05952a
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video
+
+import android.Manifest
+import android.content.Context
+import androidx.annotation.CheckResult
+import androidx.annotation.RequiresPermission
+import androidx.camera.core.impl.utils.ContextUtil
+import androidx.core.content.PermissionChecker
+import androidx.core.util.Consumer
+import androidx.core.util.Preconditions
+import java.util.concurrent.Executor
+
+/**
+ * A recording that can be started at a future time.
+ *
+ * A pending recording allows for configuration of a recording before it is started. Once a pending
+ * recording is started with [start], any changes to the pending recording will not affect the
+ * actual recording; any modifications to the recording will need to occur through the controls of
+ * the [Recording] class returned by [start].
+ *
+ * A pending recording can be created using one of the [Recorder] methods for starting a recording
+ * such as [Recorder.prepareRecording].
+ *
+ * There may be more settings that can only be changed per-recorder instead of per-recording,
+ * because it requires expensive operations like reconfiguring the camera. For those settings, use
+ * the [Recorder.Builder] methods to configure before creating the [Recorder] instance, then create
+ * the pending recording with it.
+ */
+public class PendingRecording
+internal constructor(
+ context: Context,
+ private val recorder: Recorder,
+ private val outputOptions: OutputOptions
+) {
+ // Application context is sufficient for all our needs, so store that to avoid leaking
+ // unused resources. For attribution, ContextUtil.getApplicationContext() will retain the
+ // attribution tag from the original context.
+ private val applicationContext: Context = ContextUtil.getApplicationContext(context)
+ private var eventListener: Consumer<VideoRecordEvent>? = null
+ private var listenerExecutor: Executor? = null
+ private var isAudioEnabled: Boolean = false
+ private var isAudioInitialMuted: Boolean = false
+ private var isPersistent: Boolean = false
+
+ /**
+ * Returns an application context which was retrieved from the [Context] used to create this
+ * object.
+ */
+ @JvmName("getApplicationContext")
+ internal fun getApplicationContext(): Context = applicationContext
+
+ @JvmName("getRecorder") internal fun getRecorder(): Recorder = recorder
+
+ @JvmName("getOutputOptions") internal fun getOutputOptions(): OutputOptions = outputOptions
+
+ @JvmName("getListenerExecutor") internal fun getListenerExecutor(): Executor? = listenerExecutor
+
+ @JvmName("getEventListener")
+ internal fun getEventListener(): Consumer<VideoRecordEvent>? = eventListener
+
+ @JvmName("isAudioEnabled") internal fun isAudioEnabled(): Boolean = isAudioEnabled
+
+ @JvmName("isAudioInitialMuted")
+ internal fun isAudioInitialMuted(): Boolean = isAudioInitialMuted
+
+ @JvmName("isPersistent") internal fun isPersistent(): Boolean = isPersistent
+
+ /**
+ * Enables audio to be recorded for this recording.
+ *
+ * This method must be called prior to [start] to enable audio in the recording. If this method
+ * is not called, the [Recording] generated by [start] will not contain audio, and
+ * [AudioStats.getAudioState] will always return [AudioStats.AUDIO_STATE_DISABLED] for all
+ * [RecordingStats] send to the listener set passed to [start].
+ *
+ * Recording with audio requires the [android.Manifest.permission.RECORD_AUDIO] permission;
+ * without it, recording will fail at [start] with an [IllegalStateException].
+ *
+ * @param initialMuted (Optional) The initial mute state of the recording. Defaults to `false`
+ * (un-muted). After the recording is started, the mute state can be changed by calling
+ * [Recording.mute].
+ * @return this pending recording
+ * @throws IllegalStateException if the [Recorder] this recording is associated to doesn't
+ * support audio.
+ * @throws SecurityException if the [Manifest.permission.RECORD_AUDIO] permission is denied for
+ * the current application.
+ */
+ @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+ @JvmOverloads
+ public fun withAudioEnabled(initialMuted: Boolean = false): PendingRecording {
+ // Check permissions and throw a security exception if RECORD_AUDIO is not granted.
+ if (
+ PermissionChecker.checkSelfPermission(
+ applicationContext,
+ Manifest.permission.RECORD_AUDIO
+ ) == PermissionChecker.PERMISSION_DENIED
+ ) {
+ throw SecurityException(
+ "Attempted to enable audio for recording but application " +
+ "does not have RECORD_AUDIO permission granted."
+ )
+ }
+ Preconditions.checkState(
+ recorder.isAudioSupported,
+ "The Recorder this recording is " + "associated to doesn't support audio."
+ )
+ isAudioEnabled = true
+ isAudioInitialMuted = initialMuted
+ return this
+ }
+
+ /**
+ * Configures the recording to be a persistent recording.
+ *
+ * A persistent recording will only be stopped by explicitly calling [Recording.stop] or
+ * [Recording.close] and will ignore events that would normally cause recording to stop, such as
+ * lifecycle events or explicit unbinding of a [VideoCapture] use case that the recording's
+ * [Recorder] is attached to.
+ *
+ * Even though lifecycle events or explicit unbinding use cases won't stop a persistent
+ * recording, it will still stop the camera from producing data, resulting in the in-progress
+ * persistent recording stopping getting data until the camera stream is activated again. For
+ * example, when the activity goes into background, the recording will keep waiting for new data
+ * to be recorded until the activity is back to foreground.
+ *
+ * A [Recorder] instance is recommended to be associated with a single [VideoCapture] instance,
+ * especially when using persistent recording. Otherwise, there might be unexpected behavior.
+ * Any in-progress persistent recording created from the same [Recorder] should be stopped
+ * before starting a new recording, even if the [Recorder] is associated with a different
+ * [VideoCapture].
+ *
+ * To switch to a different camera stream while a recording is in progress, first create the
+ * recording as persistent recording, then rebind the [VideoCapture] it's associated with to a
+ * different camera. The implementation may be like:
+ * ```
+ * // Prepare the Recorder and VideoCapture, then bind the VideoCapture to the back camera.
+ * Recorder recorder = Recorder.Builder().build();
+ * VideoCapture videoCapture = VideoCapture.withOutput(recorder);
+ * cameraProvider.bindToLifecycle(
+ * lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, videoCapture);
+ *
+ * // Prepare the persistent recording and start it.
+ * Recording recording = recorder
+ * .prepareRecording(context, outputOptions)
+ * .asPersistentRecording()
+ * .start(eventExecutor, eventListener);
+ *
+ * // Record from the back camera for a period of time.
+ *
+ * // Rebind the VideoCapture to the front camera.
+ * cameraProvider.unbindAll();
+ * cameraProvider.bindToLifecycle(
+ * lifecycleOwner, CameraSelector.DEFAULT_FRONT_CAMERA, videoCapture);
+ *
+ *
+ * // Record from the front camera for a period of time.
+ *
+ * // Stop the recording explicitly.
+ * recording.stop();
+ * ```
+ *
+ * The audio data will still be recorded after the [VideoCapture] is unbound.
+ * [Pause][Recording.pause] the recording first and [resume][Recording.resume] it later to stop
+ * recording audio while rebinding use cases.
+ *
+ * If the recording is unable to receive data from the new camera, possibly because of
+ * incompatible surface combination, an exception will be thrown when binding to lifecycle.
+ */
+ @ExperimentalPersistentRecording
+ public fun asPersistentRecording(): PendingRecording {
+ isPersistent = true
+ return this
+ }
+
+ /**
+ * Starts the recording, making it an active recording.
+ *
+ * Only a single recording can be active at a time, so if another recording is active, this will
+ * throw an [IllegalStateException].
+ *
+ * If there are no errors starting the recording, the returned [Recording] can be used to
+ * [pause][Recording.pause], [resume][Recording.resume], or [stop][Recording.stop] the
+ * recording.
+ *
+ * Upon successfully starting the recording, a [VideoRecordEvent.Start] event will be the first
+ * event sent to the provided event listener.
+ *
+ * If errors occur while starting the recording, a [VideoRecordEvent.Finalize] event will be the
+ * first event sent to the provided listener, and information about the error can be found in
+ * that event's [VideoRecordEvent.Finalize.getError] method. The returned [Recording] will be in
+ * a finalized state, and all controls will be no-ops.
+ *
+ * If the returned [Recording] is garbage collected, the recording will be automatically
+ * stopped. A reference to the active recording must be maintained as long as the recording
+ * needs to be active. If the recording is garbage collected, the [VideoRecordEvent.Finalize]
+ * event will contain error [VideoRecordEvent.Finalize.ERROR_RECORDING_GARBAGE_COLLECTED].
+ *
+ * The [Recording] will be stopped automatically if the [VideoCapture] its [Recorder] is
+ * attached to is unbound unless it's created
+ * [as a persistent recording][asPersistentRecording].
+ *
+ * @param listenerExecutor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
+ * @throws IllegalStateException if the associated Recorder currently has an unfinished active
+ * recording.
+ */
+ @CheckResult
+ public fun start(listenerExecutor: Executor, listener: Consumer<VideoRecordEvent>): Recording {
+ Preconditions.checkNotNull(listenerExecutor, "Listener Executor can't be null.")
+ Preconditions.checkNotNull(listener, "Event listener can't be null")
+ this.listenerExecutor = listenerExecutor
+ eventListener = listener
+ return recorder.start(this)
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index 1250761..a79d7c5 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -2968,7 +2968,7 @@
@NonNull
static RecordingRecord from(@NonNull PendingRecording pendingRecording, long recordingId) {
- return new AutoValue_Recorder_RecordingRecord(
+ RecordingRecord recordingRecord = new AutoValue_Recorder_RecordingRecord(
pendingRecording.getOutputOptions(),
pendingRecording.getListenerExecutor(),
pendingRecording.getEventListener(),
@@ -2976,6 +2976,8 @@
pendingRecording.isPersistent(),
recordingId
);
+ recordingRecord.mute(pendingRecording.isAudioInitialMuted());
+ return recordingRecord;
}
@NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recording.java b/camera/camera-video/src/main/java/androidx/camera/video/Recording.java
index 953c2ef..3484029 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recording.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recording.java
@@ -178,7 +178,8 @@
*
* <p>The output file will contain an audio track even the whole recording is muted. Create a
* recording without calling {@link PendingRecording#withAudioEnabled()} to record a file
- * with no audio track.
+ * with no audio track. To set the initial mute state of the recording, use
+ * {@link PendingRecording#withAudioEnabled(boolean)}.
*
* <p>Muting or unmuting a recording that isn't created
* {@link PendingRecording#withAudioEnabled()} with audio enabled is no-op.
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 0775266..8e4f80d 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -140,6 +140,11 @@
SizeCannotEncodeVideoQuirk.load())) {
quirks.add(new SizeCannotEncodeVideoQuirk());
}
+ if (quirkSettings.shouldEnableQuirk(
+ PreviewBlackScreenQuirk.class,
+ PreviewBlackScreenQuirk.load())) {
+ quirks.add(new PreviewBlackScreenQuirk());
+ }
return quirks;
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewBlackScreenQuirk.kt b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewBlackScreenQuirk.kt
new file mode 100644
index 0000000..67ed713
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewBlackScreenQuirk.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.camera.core.internal.compat.quirk.SurfaceProcessingQuirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: b/361477717
+ * - Description: Quirk indicates Preview is black screen when binding with VideoCapture.
+ * - Device(s): Motorola Edge 20 Fusion.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+public class PreviewBlackScreenQuirk : SurfaceProcessingQuirk {
+
+ public companion object {
+
+ @JvmStatic
+ public fun load(): Boolean {
+ return isMotorolaEdge20Fusion
+ }
+
+ private val isMotorolaEdge20Fusion: Boolean =
+ Build.BRAND.equals("motorola", ignoreCase = true) &&
+ Build.MODEL.equals("motorola edge 20 fusion", ignoreCase = true)
+ }
+}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
index 70193ca..71558a6 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -35,6 +35,7 @@
import androidx.camera.testing.impl.CoreAppTestUtil.ForegroundOccupiedError
import androidx.camera.testing.impl.fakes.FakeActivity
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.testrule.PreTestRule
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.FileDescriptorOutputOptions
import androidx.camera.video.FileOutputOptions
@@ -145,23 +146,29 @@
}
}
- @get:Rule
+ @get:Rule(order = 0)
+ val skipRule: TestRule = PreTestRule {
+ skipVideoRecordingTestIfNotSupportedByEmulator()
+ skipTestWithSurfaceProcessingOnCuttlefishApi30()
+ }
+
+ @get:Rule(order = 1)
val cameraRule: TestRule =
CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
)
- @get:Rule
- val activityRule: ActivityScenarioRule<FakeActivity> =
- ActivityScenarioRule(FakeActivity::class.java)
-
- @get:Rule
+ @get:Rule(order = 2)
val permissionRule: GrantPermissionRule =
GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO
)
+ @get:Rule(order = 3)
+ val activityRule: ActivityScenarioRule<FakeActivity> =
+ ActivityScenarioRule(FakeActivity::class.java)
+
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context: Context = ApplicationProvider.getApplicationContext()
private val audioEnabled = AudioConfig.create(true)
@@ -210,9 +217,6 @@
@Before
fun setUp() {
- skipVideoRecordingTestIfNotSupportedByEmulator()
- skipTestWithSurfaceProcessingOnCuttlefishApi30()
-
initialLifecycleOwner()
initialPreviewView()
initialController()
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 346e8e9..4c1e375 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -84,8 +84,8 @@
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.camera.testing.impl.SurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
-import androidx.camera.testing.impl.fakes.FakeImageCaptureCallback
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.fakes.FakeOnImageCapturedCallback
import androidx.camera.testing.impl.fakes.FakeSessionProcessor
import androidx.camera.testing.impl.mocks.MockScreenFlash
import androidx.camera.video.Recorder
@@ -103,6 +103,8 @@
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.abs
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
@@ -126,7 +128,7 @@
private val BACK_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
private val FRONT_SELECTOR = CameraSelector.DEFAULT_FRONT_CAMERA
private const val BACK_LENS_FACING = CameraSelector.LENS_FACING_BACK
-private const val CAPTURE_TIMEOUT = 15_000.toLong() // 15 seconds
+private val CAPTURE_TIMEOUT = 15.seconds
private const val TOLERANCE = 1e-3f
private val EXIF_GAINMAP_PATTERNS =
listOf(
@@ -231,14 +233,14 @@
}
// Act.
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Assert.
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
var sizeEnvelope = imageProperties.size
// Some devices may not be able to fit the requested resolution. In this case, the returned
@@ -311,13 +313,13 @@
if (camera.cameraInfo.isZslSupported) {
val numImages = 5
- val callback = FakeImageCaptureCallback(captureCount = numImages)
+ val callback = FakeOnImageCapturedCallback(captureCount = numImages)
for (i in 0 until numImages) {
useCase.takePicture(mainExecutor, callback)
}
callback.awaitCapturesAndAssert(
- timeout = numImages * CAPTURE_TIMEOUT,
+ timeout = CAPTURE_TIMEOUT.times(numImages),
capturedImagesCount = numImages
)
}
@@ -401,12 +403,12 @@
}
// Act.
- val callback = FakeImageCaptureCallback(captureCount = numImages)
+ val callback = FakeOnImageCapturedCallback(captureCount = numImages)
repeat(numImages) { useCase.takePicture(mainExecutor, callback) }
// Assert.
callback.awaitCapturesAndAssert(
- timeout = numImages * CAPTURE_TIMEOUT,
+ timeout = CAPTURE_TIMEOUT.times(numImages),
capturedImagesCount = numImages
)
}
@@ -463,7 +465,7 @@
}
// Act.
- val callback = FakeImageCaptureCallback()
+ val callback = FakeOnImageCapturedCallback()
imageCapture.takePicture(mainExecutor, callback)
// Assert.
@@ -823,7 +825,7 @@
// Wait for the signal that all the images have been saved.
callback.awaitCapturesAndAssert(
- timeout = numImages * CAPTURE_TIMEOUT,
+ timeout = CAPTURE_TIMEOUT.times(numImages),
savedImagesCount = numImages
)
}
@@ -882,7 +884,7 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
@@ -913,14 +915,14 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
assertThat(imageProperties.format).isEqualTo(ImageFormat.RAW10)
}
@@ -1037,13 +1039,13 @@
// directly know onStateAttached() callback has been received. Therefore, taking a
// picture and waiting for the capture success callback to know the use case's
// onStateAttached() callback has been received.
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val callback2 = FakeImageCaptureCallback(captureCount = 3)
+ val callback2 = FakeOnImageCapturedCallback(captureCount = 3)
imageCapture.takePicture(mainExecutor, callback2)
imageCapture.takePicture(mainExecutor, callback2)
imageCapture.takePicture(mainExecutor, callback2)
@@ -1066,7 +1068,7 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
- val callback = FakeImageCaptureCallback(captureCount = 3)
+ val callback = FakeOnImageCapturedCallback(captureCount = 3)
imageCapture.takePicture(mainExecutor, callback)
imageCapture.takePicture(mainExecutor, callback)
imageCapture.takePicture(mainExecutor, callback)
@@ -1094,7 +1096,7 @@
@Test
fun takePictureReturnsErrorNO_CAMERA_whenNotBound() = runBlocking {
val imageCapture = ImageCapture.Builder().build()
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
imageCapture.takePicture(mainExecutor, callback)
@@ -1243,7 +1245,7 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
@@ -1253,7 +1255,7 @@
// same as original one.
val expectedCroppingRatio = Rational(DEFAULT_RESOLUTION.width, DEFAULT_RESOLUTION.height)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
val cropRect = imageProperties.cropRect
// Rotate the captured ImageProxy's crop rect into the coordinate space of the final
@@ -1280,7 +1282,7 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
// Checks camera device sensor degrees to set target cropping aspect ratio match the
// sensor orientation.
@@ -1298,7 +1300,7 @@
// After target rotation is updated, the result cropping aspect ratio should still the
// same as original one.
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
val cropRect = imageProperties.cropRect
// Rotate the captured ImageProxy's crop rect into the coordinate space of the final
@@ -1353,7 +1355,7 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCaseGroup)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
@@ -1362,7 +1364,7 @@
// After target rotation is updated, the result cropping aspect ratio should still the
// same as original one.
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
val cropRect = imageProperties.cropRect
// Rotate the captured ImageProxy's crop rect into the coordinate space of the final
@@ -1447,13 +1449,13 @@
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
val cropRect = imageProperties.cropRect
val cropRectAspectRatio = Rational(cropRect!!.height(), cropRect.width())
@@ -1610,13 +1612,13 @@
)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees)
@@ -1683,13 +1685,13 @@
)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees)
.isEqualTo(camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation))
@@ -1721,13 +1723,13 @@
)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees)
@@ -1771,13 +1773,13 @@
)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees)
@@ -1819,13 +1821,13 @@
)
}
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
// Check the output image rotation degrees value is correct.
if (isRotationOptionSupportedDevice()) {
@@ -1981,7 +1983,7 @@
.isTrue()
// Act.
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
withContext(Dispatchers.Main) {
// Test the reproduce step in b/235119898
cameraProvider.unbind(preview)
@@ -2008,7 +2010,7 @@
}
// wait for camera to start by taking a picture
- val callback1 = FakeImageCaptureCallback(captureCount = 1)
+ val callback1 = FakeOnImageCapturedCallback(captureCount = 1)
imageCapture.takePicture(mainExecutor, callback1)
try {
callback1.awaitCapturesAndAssert(capturedImagesCount = 1)
@@ -2017,7 +2019,7 @@
}
// Act.
- val callback2 = FakeImageCaptureCallback(captureCount = 1)
+ val callback2 = FakeOnImageCapturedCallback(captureCount = 1)
withContext(Dispatchers.Main) {
cameraProvider.unbind(videoCapture)
imageCapture.takePicture(mainExecutor, callback2)
@@ -2169,13 +2171,13 @@
assertThat(imageCapture.resolutionInfo!!.resolution).isEqualTo(maxHighResolutionOutputSize)
- val callback = FakeImageCaptureCallback(captureCount = 1)
+ val callback = FakeOnImageCapturedCallback(captureCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
- val imageProperties = callback.results.first()
+ val imageProperties = callback.results.first().properties
assertThat(imageProperties.size).isEqualTo(maxHighResolutionOutputSize)
}
@@ -2313,7 +2315,7 @@
}
suspend fun awaitCapturesAndAssert(
- timeout: Long = CAPTURE_TIMEOUT,
+ timeout: Duration = CAPTURE_TIMEOUT,
savedImagesCount: Int = 0,
errorsCount: Int = 0
) {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
index 8687aa3..445c32b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
@@ -29,6 +29,7 @@
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
import androidx.camera.testing.impl.CoreAppTestUtil
+import androidx.camera.testing.impl.InternalTestConvenience.useInCameraTest
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onIdle
@@ -126,7 +127,7 @@
@Test
fun testFlashToggleButton() {
- ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ ActivityScenario.launch<CameraXActivity>(launchIntent).useInCameraTest { scenario ->
// Arrange.
WaitForViewToShow(R.id.constraintLayout).wait()
assumeTrue(isButtonEnabled(R.id.flash_toggle))
@@ -151,7 +152,7 @@
@Test
fun testTorchToggleButton() {
- ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ ActivityScenario.launch<CameraXActivity>(launchIntent).useInCameraTest { scenario ->
WaitForViewToShow(R.id.constraintLayout).wait()
assumeTrue(isButtonEnabled(R.id.torch_toggle))
val cameraInfo = scenario.withActivity { cameraInfo!! }
@@ -171,7 +172,7 @@
"Ignore the camera switch test since there's no front camera.",
CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)
)
- ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ ActivityScenario.launch<CameraXActivity>(launchIntent).useInCameraTest { scenario ->
WaitForViewToShow(R.id.direction_toggle).wait()
assertThat(scenario.withActivity { preview }).isNotNull()
for (i in 0..4) {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
index a1c2869..3b491b0 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -17,9 +17,11 @@
import android.Manifest
import android.content.Context
-import android.hardware.camera2.CameraCaptureSession
-import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.TotalCaptureResult
+import android.graphics.SurfaceTexture
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import android.view.Surface
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.Camera
@@ -33,12 +35,12 @@
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
-import androidx.camera.integration.core.util.CameraPipeUtil
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
-import androidx.camera.testing.impl.SurfaceTextureProvider.createSurfaceTextureProvider
+import androidx.camera.testing.impl.GLUtil
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.camera.testing.impl.video.AudioChecker
@@ -98,6 +100,8 @@
@get:Rule val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
companion object {
+ private const val TAG = "UseCaseCombinationTest"
+
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data() =
@@ -448,22 +452,12 @@
imageAnalysisMonitor.waitForImageAnalysis()
}
- private fun initPreview(monitor: PreviewMonitor?, setSurfaceProvider: Boolean = true): Preview {
- return Preview.Builder()
- .setTargetName("Preview")
- .also {
- monitor?.let { monitor ->
- CameraPipeUtil.setCameraCaptureSessionCallback(implName, it, monitor)
- }
+ private fun initPreview(monitor: PreviewMonitor, setSurfaceProvider: Boolean = true): Preview {
+ return Preview.Builder().setTargetName("Preview").build().apply {
+ if (setSurfaceProvider) {
+ instrumentation.runOnMainSync { surfaceProvider = monitor.getSurfaceProvider() }
}
- .build()
- .apply {
- if (setSurfaceProvider) {
- instrumentation.runOnMainSync {
- surfaceProvider = createSurfaceTextureProvider()
- }
- }
- }
+ }
}
private fun initImageAnalysis(analyzer: ImageAnalysis.Analyzer?): ImageAnalysis {
@@ -501,8 +495,51 @@
.isTrue()
}
- class PreviewMonitor : CameraCaptureSession.CaptureCallback() {
+ class PreviewMonitor {
private var countDown: CountDownLatch? = null
+ private val surfaceProvider =
+ Preview.SurfaceProvider { request ->
+ val lock = Any()
+ var surfaceTextureReleased = false
+ val surfaceTexture = SurfaceTexture(0)
+ surfaceTexture.setDefaultBufferSize(
+ request.resolution.width,
+ request.resolution.height
+ )
+ surfaceTexture.detachFromGLContext()
+ surfaceTexture.attachToGLContext(GLUtil.getTexIdFromGLContext())
+ val frameUpdateThread = HandlerThread("frameUpdateThread").apply { start() }
+
+ surfaceTexture.setOnFrameAvailableListener(
+ {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ synchronized(lock) {
+ if (!surfaceTextureReleased) {
+ try {
+ surfaceTexture.updateTexImage()
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "updateTexImage failed!")
+ }
+ }
+ }
+ }
+ countDown?.countDown()
+ },
+ Handler(frameUpdateThread.getLooper())
+ )
+
+ val surface = Surface(surfaceTexture)
+ request.provideSurface(surface, CameraXExecutors.directExecutor()) {
+ synchronized(lock) {
+ surfaceTextureReleased = true
+ surface.release()
+ surfaceTexture.release()
+ frameUpdateThread.quitSafely()
+ }
+ }
+ }
+
+ fun getSurfaceProvider(): Preview.SurfaceProvider = surfaceProvider
fun waitForStream(count: Int = 10, timeMillis: Long = TimeUnit.SECONDS.toMillis(5)) {
Truth.assertWithMessage("Preview doesn't start")
@@ -540,14 +577,6 @@
countDown
}!!
.await(timeSeconds, TimeUnit.SECONDS)
-
- override fun onCaptureCompleted(
- session: CameraCaptureSession,
- request: CaptureRequest,
- result: TotalCaptureResult
- ) {
- synchronized(this) { countDown?.countDown() }
- }
}
class AnalysisMonitor : ImageAnalysis.Analyzer {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
new file mode 100644
index 0000000..af52c44
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core.fakecamera
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.fakes.FakeAppConfig
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.fakes.FakeOnImageCapturedCallback
+import androidx.camera.testing.impl.fakes.FakeOnImageSavedCallback
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Tests using a fake camera instead of real camera by replacing the camera-camera2 layer with
+ * camera-testing layer.
+ *
+ * They are aimed to ensure that integration between camera-core and camera-testing work seamlessly.
+ */
+@RunWith(Parameterized::class)
+class ImageCaptureTest(
+ @CameraSelector.LensFacing private val lensFacing: Int,
+) {
+ @get:Rule
+ val temporaryFolder =
+ TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var camera: FakeCamera
+ private lateinit var cameraControl: FakeCameraControl
+ private lateinit var imageCapture: ImageCapture
+
+ @Before
+ fun setup() = runBlocking {
+ cameraProvider = getFakeConfigCameraProvider(context)
+ imageCapture = bindImageCapture()
+ }
+
+ @After
+ fun tearDown() = runBlocking {
+ if (::cameraProvider.isInitialized) {
+ withContext(Dispatchers.Main) { cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS] }
+ }
+ }
+
+ // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
+ // reflected there too
+ @Test
+ fun canSubmitTakePictureRequest(): Unit = runBlocking {
+ val countDownLatch = CountDownLatch(1)
+ cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
+
+ assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue()
+ }
+
+ // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
+ // reflected there too
+ @Ignore("b/318314454")
+ @Test
+ fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
+ val callback = FakeOnImageCapturedCallback()
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ callback.results.first().image.toBitmap()
+ }
+
+ // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
+ // reflected there too
+ @Ignore("b/318314454")
+ @Test
+ fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
+ val saveLocation = temporaryFolder.newFile()
+ val previousLength = saveLocation.length()
+ val callback = FakeOnImageSavedCallback()
+
+ imageCapture.takePicture(
+ ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
+ CameraXExecutors.directExecutor(),
+ callback
+ )
+
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ assertThat(saveLocation.length()).isGreaterThan(previousLength)
+ }
+
+ // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
+ // need to be reflected there too
+ @Ignore("b/318314454")
+ @Test
+ fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
+ val initialCount = getMediaStoreCameraXImageCount()
+ val callback = FakeOnImageSavedCallback()
+ imageCapture.takePicture(
+ createMediaStoreOutputOptions(),
+ CameraXExecutors.directExecutor(),
+ callback
+ )
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ assertThat(getMediaStoreCameraXImageCount()).isEqualTo(initialCount + 1)
+ }
+
+ private suspend fun bindImageCapture(): ImageCapture {
+ val imageCapture = ImageCapture.Builder().build()
+
+ withContext(Dispatchers.Main) {
+ cameraProvider.bindToLifecycle(
+ FakeLifecycleOwner().apply { startAndResume() },
+ CameraSelector.Builder().requireLensFacing(lensFacing).build(),
+ imageCapture
+ )
+ }
+
+ camera =
+ when (lensFacing) {
+ CameraSelector.LENS_FACING_BACK -> FakeAppConfig.getBackCamera()
+ CameraSelector.LENS_FACING_FRONT -> FakeAppConfig.getFrontCamera()
+ else -> throw AssertionError("Unsupported lens facing: $lensFacing")
+ }
+ cameraControl = camera.cameraControl as FakeCameraControl
+
+ return imageCapture
+ }
+
+ private fun getFakeConfigCameraProvider(context: Context): ProcessCameraProvider {
+ var cameraProvider: ProcessCameraProvider? = null
+ val latch = CountDownLatch(1)
+ ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+ ProcessCameraProvider.getInstance(context)
+ .addListener(
+ {
+ cameraProvider = ProcessCameraProvider.getInstance(context).get()
+ latch.countDown()
+ },
+ CameraXExecutors.directExecutor()
+ )
+
+ Truth.assertWithMessage("ProcessCameraProvider.getInstance timed out!")
+ .that(latch.await(5, TimeUnit.SECONDS))
+ .isTrue()
+
+ return cameraProvider!!
+ }
+
+ private fun createMediaStoreOutputOptions(): ImageCapture.OutputFileOptions {
+ // Create time stamped name and MediaStore entry.
+ val name =
+ FILENAME_PREFIX +
+ SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ .format(System.currentTimeMillis())
+ val contentValues =
+ ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, name)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
+ }
+ }
+
+ // Create output options object which contains file + metadata
+ return ImageCapture.OutputFileOptions.Builder(
+ context.contentResolver,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+ .build()
+ }
+
+ private fun getMediaStoreCameraXImageCount(): Int {
+ val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME)
+ val selection = "${MediaStore.Images.Media.DISPLAY_NAME} LIKE ?"
+ val selectionArgs = arrayOf("$FILENAME_PREFIX%")
+
+ val query =
+ ApplicationProvider.getApplicationContext<Context>()
+ .contentResolver
+ .query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ null
+ )
+
+ return query?.use { cursor -> cursor.count } ?: 0
+ }
+
+ companion object {
+ private const val FILENAME_PREFIX = "cameraXPhoto"
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "LensFacing = {0}")
+ fun data() =
+ listOf(
+ arrayOf(CameraSelector.LENS_FACING_BACK),
+ arrayOf(CameraSelector.LENS_FACING_FRONT),
+ )
+ }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index e630a5e..c8427ff 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -362,6 +362,7 @@
private RecordUi mRecordUi;
private DynamicRangeUi mDynamicRangeUi;
private Quality mVideoQuality;
+ private boolean mAudioMuted = false;
private DynamicRange mDynamicRange = DynamicRange.SDR;
private @ImageCapture.OutputFormat int mImageOutputFormat = OUTPUT_FORMAT_JPEG;
private Set<DynamicRange> mDisplaySupportedHighDynamicRanges = Collections.emptySet();
@@ -694,7 +695,7 @@
pendingRecording.asPersistentRecording();
}
mActiveRecording = pendingRecording
- .withAudioEnabled()
+ .withAudioEnabled(mAudioMuted)
.start(ContextCompat.getMainExecutor(CameraXActivity.this),
mVideoRecordEventListener);
mRecordUi.setState(RecordUi.State.RECORDING);
@@ -779,6 +780,17 @@
popup.show();
});
+
+ Runnable buttonMuteUpdater = () -> mRecordUi.getButtonMute().setImageResource(
+ mAudioMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic_on);
+ buttonMuteUpdater.run();
+ mRecordUi.getButtonMute().setOnClickListener(view -> {
+ mAudioMuted = !mAudioMuted;
+ if (mActiveRecording != null) {
+ mActiveRecording.mute(mAudioMuted);
+ }
+ buttonMuteUpdater.run();
+ });
}
private void setUpDynamicRangeButton() {
@@ -1533,6 +1545,7 @@
findViewById(R.id.video_stats),
findViewById(R.id.video_quality),
findViewById(R.id.video_persistent),
+ findViewById(R.id.video_mute),
(newState) -> updateDynamicRangeUiState()
);
@@ -2302,19 +2315,21 @@
private final TextView mTextStats;
private final Button mButtonQuality;
private final ToggleButton mButtonPersistent;
+ private final ImageButton mButtonMute;
private boolean mEnabled = false;
private State mState = State.IDLE;
private final Consumer<State> mNewStateConsumer;
RecordUi(@NonNull Button buttonRecord, @NonNull Button buttonPause,
@NonNull TextView textStats, @NonNull Button buttonQuality,
- @NonNull ToggleButton buttonPersistent,
+ @NonNull ToggleButton buttonPersistent, @NonNull ImageButton buttonMute,
@NonNull Consumer<State> onNewState) {
mButtonRecord = buttonRecord;
mButtonPause = buttonPause;
mTextStats = textStats;
mButtonQuality = buttonQuality;
mButtonPersistent = buttonPersistent;
+ mButtonMute = buttonMute;
mNewStateConsumer = onNewState;
}
@@ -2325,6 +2340,7 @@
mTextStats.setVisibility(View.VISIBLE);
mButtonQuality.setVisibility(View.VISIBLE);
mButtonPersistent.setVisibility(View.VISIBLE);
+ mButtonMute.setVisibility(View.VISIBLE);
updateUi();
} else {
mButtonRecord.setText("Record");
@@ -2333,6 +2349,7 @@
mButtonQuality.setVisibility(View.INVISIBLE);
mTextStats.setVisibility(View.GONE);
mButtonPersistent.setVisibility(View.INVISIBLE);
+ mButtonMute.setVisibility(View.INVISIBLE);
}
}
@@ -2354,6 +2371,7 @@
mButtonPause.setVisibility(View.GONE);
mTextStats.setVisibility(View.GONE);
mButtonPersistent.setVisibility(View.GONE);
+ mButtonMute.setVisibility(View.GONE);
}
private void updateUi() {
@@ -2367,6 +2385,7 @@
mButtonPause.setText("Pause");
mButtonPause.setVisibility(View.INVISIBLE);
mButtonPersistent.setEnabled(true);
+ mButtonMute.setEnabled(true);
mButtonQuality.setEnabled(true);
break;
case RECORDING:
@@ -2375,6 +2394,7 @@
mButtonPause.setText("Pause");
mButtonPause.setVisibility(View.VISIBLE);
mButtonPersistent.setEnabled(false);
+ mButtonMute.setEnabled(true);
mButtonQuality.setEnabled(false);
break;
case STOPPING:
@@ -2383,6 +2403,7 @@
mButtonPause.setText("Pause");
mButtonPause.setVisibility(View.INVISIBLE);
mButtonPersistent.setEnabled(false);
+ mButtonMute.setEnabled(false);
mButtonQuality.setEnabled(true);
break;
case PAUSED:
@@ -2391,6 +2412,7 @@
mButtonPause.setText("Resume");
mButtonPause.setVisibility(View.VISIBLE);
mButtonPersistent.setEnabled(false);
+ mButtonMute.setEnabled(true);
mButtonQuality.setEnabled(true);
break;
}
@@ -2416,6 +2438,10 @@
ToggleButton getButtonPersistent() {
return mButtonPersistent;
}
+
+ ImageButton getButtonMute() {
+ return mButtonMute;
+ }
}
Preview getPreview() {
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_mic_off.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_mic_off.xml
new file mode 100644
index 0000000..26250b9
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_mic_off.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17L15,5c0,-1.66 -1.34,-3 -3,-3s-3,1.34 -3,3v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01L9.01,11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73L4.27,3z" />
+</vector>
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_mic_on.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_mic_on.xml
new file mode 100644
index 0000000..c0a5a06
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_mic_on.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
+</vector>
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
index 369ddd8..c65182e 100644
--- a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
@@ -145,6 +145,23 @@
app:layout_constraintTop_toBottomOf="@id/video_quality"
app:layout_constraintRight_toRightOf="parent" />
+ <ImageButton
+ android:id="@+id/video_mute"
+ android:layout_width="46dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="1dp"
+ android:layout_marginRight="5dp"
+ android:padding="5dp"
+ android:scaleType="fitXY"
+ android:adjustViewBounds="true"
+ android:visibility="invisible"
+ android:src="@drawable/ic_mic_on"
+ android:background="@drawable/round_toggle_button"
+ android:clickable="true"
+ android:focusable="true"
+ app:layout_constraintTop_toBottomOf="@id/video_persistent"
+ app:layout_constraintRight_toRightOf="parent" />
+
<ToggleButton
android:id="@+id/VideoToggle"
android:layout_width="wrap_content"
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index b09987b..459187c 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -16,9 +16,10 @@
package androidx.camera.integration.core
+import android.content.ContentValues
import android.content.Context
import android.os.Build
-import android.os.Looper
+import android.provider.MediaStore
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -27,13 +28,17 @@
import androidx.camera.testing.fakes.FakeAppConfig
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraControl
-import androidx.camera.testing.impl.fakes.FakeImageCaptureCallback
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.fakes.FakeOnImageCapturedCallback
+import androidx.camera.testing.impl.fakes.FakeOnImageSavedCallback
import androidx.test.core.app.ApplicationProvider
import androidx.testutils.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
+import java.text.SimpleDateFormat
+import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -42,9 +47,9 @@
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@@ -59,26 +64,20 @@
@get:Rule val mainDispatcherRule = MainDispatcherRule(testDispatcher)
+ @get:Rule
+ val temporaryFolder =
+ TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var camera: FakeCamera
private lateinit var cameraControl: FakeCameraControl
private lateinit var imageCapture: ImageCapture
- companion object {
- @JvmStatic
- @ParameterizedRobolectricTestRunner.Parameters(name = "LensFacing = {0}")
- fun data() =
- listOf(
- arrayOf(CameraSelector.LENS_FACING_BACK),
- arrayOf(CameraSelector.LENS_FACING_FRONT),
- )
- }
-
@Before
fun setup() {
+ cameraProvider = getFakeConfigCameraProvider(context)
imageCapture = bindImageCapture()
- assertThat(imageCapture).isNotNull()
}
@After
@@ -88,28 +87,64 @@
}
}
+ // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
+ // need to be reflected there too
@Test
fun canSubmitTakePictureRequest(): Unit = runTest {
val countDownLatch = CountDownLatch(1)
cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
- imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeImageCaptureCallback())
+ imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
assertThat(countDownLatch.await(3, TimeUnit.SECONDS)).isTrue()
}
- @Ignore("TODO: b/318314454")
+ // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
+ // need to be reflected there too
+ @Ignore("b/318314454")
@Test
- fun canTakeImage(): Unit = runTest {
- val callback = FakeImageCaptureCallback()
+ fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
+ val callback = FakeOnImageCapturedCallback()
imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
- shadowOf(Looper.getMainLooper()).idle()
- callback.awaitCaptures()
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ callback.results.first().image.toBitmap()
+ }
+
+ // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
+ // need to be reflected there too
+ @Ignore("b/318314454")
+ @Test
+ fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
+ val saveLocation = temporaryFolder.newFile()
+ val previousLength = saveLocation.length()
+ val callback = FakeOnImageSavedCallback()
+
+ imageCapture.takePicture(
+ ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
+ CameraXExecutors.directExecutor(),
+ callback
+ )
+
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ assertThat(saveLocation.length()).isGreaterThan(previousLength)
+ }
+
+ // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
+ // need to be reflected there too
+ @Ignore("b/318314454")
+ @Test
+ fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
+ val callback = FakeOnImageSavedCallback()
+ imageCapture.takePicture(
+ createMediaStoreOutputOptions(),
+ CameraXExecutors.directExecutor(),
+ callback
+ )
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+ assertThat(callback.results.first().savedUri).isNotNull()
}
private fun bindImageCapture(): ImageCapture {
- cameraProvider = getFakeConfigCameraProvider(context)
-
val imageCapture = ImageCapture.Builder().build()
cameraProvider.bindToLifecycle(
@@ -127,4 +162,40 @@
return imageCapture
}
+
+ private fun createMediaStoreOutputOptions(): ImageCapture.OutputFileOptions {
+ // Create time stamped name and MediaStore entry.
+ val name =
+ FILENAME_PREFIX +
+ SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ .format(System.currentTimeMillis())
+ val contentValues =
+ ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, name)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
+ }
+ }
+
+ // Create output options object which contains file + metadata
+ return ImageCapture.OutputFileOptions.Builder(
+ context.contentResolver,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+ .build()
+ }
+
+ companion object {
+ private const val FILENAME_PREFIX = "cameraXPhoto"
+
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "LensFacing = {0}")
+ fun data() =
+ listOf(
+ arrayOf(CameraSelector.LENS_FACING_BACK),
+ arrayOf(CameraSelector.LENS_FACING_FRONT),
+ )
+ }
}
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index edc8649..2466066 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -101,7 +101,6 @@
androidTestImplementation("androidx.annotation:annotation-experimental:1.4.1")
androidTestImplementation(project(":concurrent:concurrent-futures"))
androidTestImplementation(project(":concurrent:concurrent-futures-ktx"))
- androidTestImplementation(project(":window:window"))
// Testing framework
androidTestImplementation(libs.testExtJunit)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisBaseTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisBaseTest.kt
index 57a3e02..e476f60 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisBaseTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisBaseTest.kt
@@ -25,6 +25,7 @@
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
import androidx.camera.testing.impl.CoreAppTestUtil
+import androidx.camera.testing.impl.InternalTestConvenience.useInCameraTest
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
@@ -110,7 +111,7 @@
rotate: ActivityScenario<A>.() -> Unit,
) {
val activityScenario: ActivityScenario<A> = launchActivity(lensFacing, cameraXConfig)
- activityScenario.use { scenario ->
+ activityScenario.useInCameraTest { scenario ->
// Wait until the camera is set up and analysis starts receiving frames
scenario.waitOnCameraFrames()
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
index 79731b2..886d1b5 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
@@ -33,6 +33,7 @@
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
import androidx.camera.testing.impl.CoreAppTestUtil
+import androidx.camera.testing.impl.InternalTestConvenience.useInCameraTest
import androidx.camera.view.PreviewView
import androidx.lifecycle.Lifecycle.State
import androidx.test.core.app.ActivityScenario
@@ -149,7 +150,7 @@
// The test makes sure the camera PreviewView is in the streaming state.
@Test
fun testPreviewViewUpdateAfterStopResume() {
- launchActivity(lensFacing, cameraXConfig).use { scenario ->
+ launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
@@ -168,7 +169,7 @@
fun testPreviewViewUpdateAfterSwitch() {
assumeFalse(shouldSkipTest()) // b/331933633
- launchActivity(lensFacing, cameraXConfig).use { scenario ->
+ launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
@@ -198,7 +199,7 @@
@Test
fun testPreviewViewUpdateAfterSwitchAndStop_ResumeAndSwitchBack() {
- launchActivity(lensFacing, cameraXConfig).use { scenario ->
+ launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
index 331ed44..b7f221e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
@@ -29,6 +29,7 @@
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
import androidx.camera.testing.impl.CoreAppTestUtil
+import androidx.camera.testing.impl.InternalTestConvenience.useInCameraTest
import androidx.camera.view.PreviewView
import androidx.lifecycle.Lifecycle.State
import androidx.test.core.app.ActivityScenario
@@ -135,7 +136,7 @@
// The test makes sure the camera PreviewView is in the streaming state.
@Test
fun testPreviewViewUpdateAfterStopResume() {
- launchActivity(lensFacing, cameraXConfig).use { scenario ->
+ launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
@@ -152,7 +153,7 @@
// The test makes sure the TextureView surface texture keeps the same after switch.
@Test
fun testPreviewViewUpdateAfterSwitch() {
- launchActivity(lensFacing, cameraXConfig).use { scenario ->
+ launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
@@ -175,7 +176,7 @@
)
@Test
fun testPreviewViewUpdateAfterSwitchOutAndStop_ResumeAndSwitchBack() {
- launchActivity(lensFacing, cameraXConfig).use { scenario ->
+ launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/rotations/CameraActivity.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/rotations/CameraActivity.kt
index 3df52f0..c9252ee 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/rotations/CameraActivity.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/rotations/CameraActivity.kt
@@ -347,7 +347,7 @@
const val IMAGE_CAPTURE_MODE_OUTPUT_STREAM = 2
const val IMAGE_CAPTURE_MODE_MEDIA_STORE = 3
- private const val TAG = "MainActivity"
+ private const val TAG = "CameraActivity"
private const val REQUEST_CODE_PERMISSIONS = 20
val PERMISSIONS =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
diff --git a/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderScreenshotTest.kt b/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderScreenshotTest.kt
index f38064c..9e6c1f6 100644
--- a/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderScreenshotTest.kt
+++ b/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderScreenshotTest.kt
@@ -20,16 +20,20 @@
import androidx.camera.testing.impl.SurfaceUtil
import androidx.camera.viewfinder.surface.ImplementationMode
import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.outlined.Face
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas as ComposeCanvas
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.nativeCanvas
@@ -47,6 +51,7 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.matchers.MSSIMMatcher
import kotlin.math.abs
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
@@ -107,6 +112,160 @@
assertImplementationDrawsUpright(testParams)
}
+ @Test
+ fun embeddedImplementationDrawsUpright_fromHorizontallyMirroredSource() = runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 0,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from90Degree_HorizontallyMirroredSource() = runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 90,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from180Degree_HorizontallyMirroredSource() =
+ runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 180,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from270Degree_HorizontallyMirroredSource() =
+ runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 270,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_fromVerticallyMirroredSource() = runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 0,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from90Degree_VerticallyMirroredSource() = runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 90,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from180Degree_VerticallyMirroredSource() = runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 180,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from270Degree_VerticallyMirroredSource() = runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 270,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_fromVerticallyAndHorizontallyMirroredSource() =
+ runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 0,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from90Degree_VerticallyAndHorizontallyMirroredSource() =
+ runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 90,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from180Degree_VerticallyAndHorizontallyMirroredSource() =
+ runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 180,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
+ @Test
+ fun embeddedImplementationDrawsUpright_from270Degree_VerticallyAndHorizontallyMirroredSource() =
+ runBlocking {
+ val testParams =
+ ViewfinderTestParams(
+ sourceRotation = 270,
+ implementationMode = ImplementationMode.EMBEDDED,
+ isMirroredHorizontally = true,
+ isMirroredVertically = true
+ )
+
+ assertImplementationDrawsUpright(testParams)
+ }
+
@Ignore("b/338466761")
@Test
fun externalImplementationDrawsUpright_from0DegreeSource() = runBlocking {
@@ -157,35 +316,83 @@
private fun assertImplementationDrawsUpright(testParams: ViewfinderTestParams) {
val surfaceRequest = ViewfinderSurfaceRequest.Builder(testParams.sourceResolution).build()
+ val coordinateTransformer = MutableCoordinateTransformer()
composeTestRule.setContent {
Viewfinder(
modifier = Modifier.size(testParams.viewfinderSize).testTag(VIEWFINDER_TAG),
surfaceRequest = surfaceRequest,
transformationInfo = testParams.transformationInfo,
- implementationMode = testParams.implementationMode
+ implementationMode = testParams.implementationMode,
+ coordinateTransformer = coordinateTransformer
)
- DrawFaceToSurface(testParams = testParams, surfaceRequest = surfaceRequest)
+ val touchCoordinates = Offset(200f, 200f)
+
+ // Draw touch coordinate on top of Viewfinder
+ val imageVec = Icons.Filled.Add
+ val painter = rememberVectorPainter(image = imageVec)
+ val density = LocalDensity.current
+ Canvas(modifier = Modifier.size(testParams.viewfinderSize)) {
+ val imageSize =
+ with(density) {
+ Size(imageVec.defaultWidth.toPx(), imageVec.defaultHeight.toPx())
+ }
+ withTransform({
+ translate(
+ left = touchCoordinates.x - imageSize.width / 2f,
+ top = touchCoordinates.y - imageSize.height / 2f
+ )
+ }) {
+ with(painter) {
+ draw(size = imageSize, colorFilter = ColorFilter.tint(Color.Green))
+ }
+ }
+ }
+
+ // Fill Viewfinder buffer with content
+ DrawFaceToSurface(
+ testParams = testParams,
+ surfaceRequest = surfaceRequest,
+ coordinateTransformer = coordinateTransformer,
+ touchCoordinates = touchCoordinates
+ )
}
composeTestRule
.onNodeWithTag(VIEWFINDER_TAG)
.captureToImage()
- .assertAgainstGolden(screenshotRule, "upright_face")
+ .assertAgainstGolden(
+ rule = screenshotRule,
+ goldenIdentifier = "upright_face_with_mapped_touch_point",
+ // Tuned to find a 1px difference in mapped touch coordinates.
+ // May need to split out touch coordinate mapping into its own
+ // screenshot test if this becomes flaky.
+ matcher = MSSIMMatcher(threshold = 0.9995)
+ )
}
+ /** This emulates the camera sensor. */
@RequiresApi(26)
@Composable
private fun DrawFaceToSurface(
testParams: ViewfinderTestParams,
- surfaceRequest: ViewfinderSurfaceRequest
+ surfaceRequest: ViewfinderSurfaceRequest,
+ coordinateTransformer: CoordinateTransformer,
+ touchCoordinates: Offset?
) {
val imageVec = Icons.Outlined.Face
val painter = rememberVectorPainter(image = imageVec)
val density = LocalDensity.current
LaunchedEffect(Unit) {
val surface = surfaceRequest.getSurface()
- SurfaceUtil.setBuffersTransform(surface, testParams.sourceRotation.toTransformEnum())
+ SurfaceUtil.setBuffersTransform(
+ surface,
+ toTransformEnum(
+ sourceRotation = testParams.sourceRotation,
+ horizontalMirror = testParams.isMirroredHorizontally,
+ verticalMirror = testParams.isMirroredVertically
+ )
+ )
val resolution = testParams.sourceResolution
val canvas = ComposeCanvas(surface.lockHardwareCanvas())
try {
@@ -197,8 +404,24 @@
) {
val rotation = testParams.sourceRotation
val iconSize = imageVec.calcFitSize(size, rotation, density)
+ val mirrorX =
+ when (testParams.isMirroredHorizontally) {
+ true -> -1.0f
+ false -> 1.0f
+ }
+ val flipY =
+ when (testParams.isMirroredVertically) {
+ true -> -1.0f
+ false -> 1.0f
+ }
+
drawRect(Color.Gray)
+
+ // For drawing the face, we need to emulate how the real world
+ // would project onto the sensor. So we must apply the reverse rotation
+ // and mirroring.
withTransform({
+ scale(mirrorX, flipY)
rotate(degrees = -rotation.toFloat())
translate(
left = (size.width - iconSize.width) / 2f,
@@ -207,6 +430,18 @@
}) {
with(painter) { draw(iconSize) }
}
+
+ // For drawing the touch coordinates, we are already in the "sensor"
+ // coordinates. No need to apply any transformations.
+ touchCoordinates?.let {
+ with(coordinateTransformer) {
+ drawCircle(
+ radius = 25f,
+ color = Color.Red,
+ center = touchCoordinates.transform()
+ )
+ }
+ }
}
} finally {
surface.unlockCanvasAndPost(canvas.nativeCanvas)
@@ -231,17 +466,36 @@
private fun Size.swapDimens(): Size = Size(height, width)
- private fun Int.toTransformEnum(): Int {
- return when (this) {
- 0 -> SurfaceUtil.TRANSFORM_IDENTITY
- 90 -> SurfaceUtil.TRANSFORM_ROTATE_90
- 180 -> SurfaceUtil.TRANSFORM_ROTATE_180
- 270 -> SurfaceUtil.TRANSFORM_ROTATE_270
- else ->
- throw IllegalArgumentException(
- "Rotation value $this does not correspond to valid transform"
- )
- }
+ private fun toTransformEnum(
+ sourceRotation: Int,
+ horizontalMirror: Boolean,
+ verticalMirror: Boolean
+ ): Int {
+ val rotationTransform =
+ when (sourceRotation) {
+ 0 -> SurfaceUtil.TRANSFORM_IDENTITY
+ 90 -> SurfaceUtil.TRANSFORM_ROTATE_90
+ 180 -> SurfaceUtil.TRANSFORM_ROTATE_180
+ 270 -> SurfaceUtil.TRANSFORM_ROTATE_270
+ else ->
+ throw IllegalArgumentException(
+ "Rotation value $this does not correspond to valid transform"
+ )
+ }
+
+ val horizontalMirrorTransform =
+ when (horizontalMirror) {
+ true -> SurfaceUtil.TRANSFORM_MIRROR_HORIZONTAL
+ false -> SurfaceUtil.TRANSFORM_IDENTITY
+ }
+
+ val verticalMirrorTransform =
+ when (verticalMirror) {
+ true -> SurfaceUtil.TRANSFORM_MIRROR_VERTICAL
+ false -> SurfaceUtil.TRANSFORM_IDENTITY
+ }
+
+ return (horizontalMirrorTransform or verticalMirrorTransform) xor rotationTransform
}
}
diff --git a/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTest.kt b/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTest.kt
index 9a165ac..3feb673 100644
--- a/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTest.kt
+++ b/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTest.kt
@@ -108,11 +108,12 @@
transformationInfo =
TransformationInfo(
sourceRotation = 0,
+ isSourceMirroredHorizontally = false,
+ isSourceMirroredVertically = false,
cropRectLeft = 0,
- cropRectRight = 270,
cropRectTop = 0,
- cropRectBottom = 480,
- shouldMirror = false
+ cropRectRight = 270,
+ cropRectBottom = 480
),
implementationMode = ImplementationMode.EXTERNAL,
coordinateTransformer = coordinateTransformer
diff --git a/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTestParams.kt b/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTestParams.kt
index b99b9ea..a41aa5d 100644
--- a/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTestParams.kt
+++ b/camera/viewfinder/viewfinder-compose/src/androidTest/kotlin/androidx/camera/viewfinder/compose/ViewfinderTestParams.kt
@@ -34,14 +34,17 @@
else -> throw IllegalArgumentException("Invalid source rotation: $sourceRotation")
},
val implementationMode: ImplementationMode = ImplementationMode.EXTERNAL,
+ val isMirroredHorizontally: Boolean = false,
+ val isMirroredVertically: Boolean = false,
val transformationInfo: TransformationInfo =
TransformationInfo(
sourceRotation = sourceRotation,
+ isSourceMirroredHorizontally = isMirroredHorizontally,
+ isSourceMirroredVertically = isMirroredVertically,
cropRectLeft = 0,
cropRectTop = 0,
cropRectRight = sourceResolution.width,
cropRectBottom = sourceResolution.height,
- shouldMirror = false
)
) {
companion object {
diff --git a/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-compose-documentation.md b/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-compose-documentation.md
index 10d4659..18eb197 100644
--- a/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-compose-documentation.md
+++ b/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-compose-documentation.md
@@ -1,7 +1,7 @@
# Module root
-CameraX ViewFinder Compose
+Camera Viewfinder Compose
# Package androidx.camera.viewfinder.compose
-Library providing a composable ViewFinder
+Standalone Composable Viewfinder for Camera
diff --git a/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/compose/internal/SurfaceTransformationUtil.kt b/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/compose/internal/SurfaceTransformationUtil.kt
index 3efcc08..bda4100 100644
--- a/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/compose/internal/SurfaceTransformationUtil.kt
+++ b/camera/viewfinder/viewfinder-compose/src/main/java/androidx/camera/viewfinder/compose/internal/SurfaceTransformationUtil.kt
@@ -124,20 +124,12 @@
transformationInfo.sourceRotation
)
- if (transformationInfo.shouldMirror) {
- if (TransformUtil.is90or270(transformationInfo.sourceRotation)) {
- // If the rotation is 90/270, the Surface should be flipped vertically.
- // +---+ 90 +---+ 270 +---+
- // | ^ | --> | < | | > |
- // +---+ +---+ +---+
- matrix.preScale(1f, -1f, surfaceCropRect.centerX(), surfaceCropRect.centerY())
- } else {
- // If the rotation is 0/180, the Surface should be flipped horizontally.
- // +---+ 0 +---+ 180 +---+
- // | ^ | --> | ^ | | v |
- // +---+ +---+ +---+
- matrix.preScale(-1f, 1f, surfaceCropRect.centerX(), surfaceCropRect.centerY())
- }
+ if (transformationInfo.isSourceMirroredHorizontally) {
+ matrix.preScale(-1f, 1f, surfaceCropRect.centerX(), surfaceCropRect.centerY())
+ }
+
+ if (transformationInfo.isSourceMirroredVertically) {
+ matrix.preScale(1f, -1f, surfaceCropRect.centerX(), surfaceCropRect.centerY())
}
return matrix
}
diff --git a/camera/viewfinder/viewfinder-core/api/current.txt b/camera/viewfinder/viewfinder-core/api/current.txt
index 31ea519..b9da3f9 100644
--- a/camera/viewfinder/viewfinder-core/api/current.txt
+++ b/camera/viewfinder/viewfinder-core/api/current.txt
@@ -63,18 +63,20 @@
}
public final class TransformationInfo {
- ctor public TransformationInfo(int sourceRotation, int cropRectLeft, int cropRectTop, int cropRectRight, int cropRectBottom, boolean shouldMirror);
+ ctor public TransformationInfo(int sourceRotation, boolean isSourceMirroredHorizontally, boolean isSourceMirroredVertically, int cropRectLeft, int cropRectTop, int cropRectRight, int cropRectBottom);
method public int getCropRectBottom();
method public int getCropRectLeft();
method public int getCropRectRight();
method public int getCropRectTop();
method public int getSourceRotation();
- method public boolean shouldMirror();
+ method public boolean isSourceMirroredHorizontally();
+ method public boolean isSourceMirroredVertically();
property public final int cropRectBottom;
property public final int cropRectLeft;
property public final int cropRectRight;
property public final int cropRectTop;
- property public final boolean shouldMirror;
+ property public final boolean isSourceMirroredHorizontally;
+ property public final boolean isSourceMirroredVertically;
property public final int sourceRotation;
}
diff --git a/camera/viewfinder/viewfinder-core/api/restricted_current.txt b/camera/viewfinder/viewfinder-core/api/restricted_current.txt
index 31ea519..b9da3f9 100644
--- a/camera/viewfinder/viewfinder-core/api/restricted_current.txt
+++ b/camera/viewfinder/viewfinder-core/api/restricted_current.txt
@@ -63,18 +63,20 @@
}
public final class TransformationInfo {
- ctor public TransformationInfo(int sourceRotation, int cropRectLeft, int cropRectTop, int cropRectRight, int cropRectBottom, boolean shouldMirror);
+ ctor public TransformationInfo(int sourceRotation, boolean isSourceMirroredHorizontally, boolean isSourceMirroredVertically, int cropRectLeft, int cropRectTop, int cropRectRight, int cropRectBottom);
method public int getCropRectBottom();
method public int getCropRectLeft();
method public int getCropRectRight();
method public int getCropRectTop();
method public int getSourceRotation();
- method public boolean shouldMirror();
+ method public boolean isSourceMirroredHorizontally();
+ method public boolean isSourceMirroredVertically();
property public final int cropRectBottom;
property public final int cropRectLeft;
property public final int cropRectRight;
property public final int cropRectTop;
- property public final boolean shouldMirror;
+ property public final boolean isSourceMirroredHorizontally;
+ property public final boolean isSourceMirroredVertically;
property public final int sourceRotation;
}
diff --git a/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-core-documentation.md b/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-core-documentation.md
index fe8fd60..6f4318c 100644
--- a/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-core-documentation.md
+++ b/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/androidx-camera-viewfinder-viewfinder-core-documentation.md
@@ -1,7 +1,7 @@
# Module root
-CameraX ViewFinder Core
+Camera Viewfinder Core
# Package androidx.camera.viewfinder
-Library providing core dependencies for ViewFinder
+Core dependencies for Viewfinder
diff --git a/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/TransformationInfo.kt b/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/TransformationInfo.kt
index fae6085..b95b7be 100644
--- a/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/TransformationInfo.kt
+++ b/camera/viewfinder/viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/TransformationInfo.kt
@@ -19,7 +19,7 @@
/**
* Transformation information associated with the preview output.
*
- * This information can be used to transform the Surface of a ViewFinder to be suitable to be
+ * This information can be used to transform the Surface of a Viewfinder to be suitable to be
* displayed.
*/
class TransformationInfo(
@@ -27,6 +27,35 @@
/** Rotation of the source, relative to the device's natural rotation. */
val sourceRotation: Int,
+ /**
+ * Indicates whether the source has been mirrored horizontally.
+ *
+ * This is common if the source comes from a camera that is front-facing.
+ *
+ * It is not common for both [isSourceMirroredHorizontally] and [isSourceMirroredVertically] to
+ * be set to `true`. This is equivalent to [sourceRotation] being rotated by an additional 180
+ * degrees.
+ *
+ * @see android.hardware.camera2.params.OutputConfiguration.MIRROR_MODE_AUTO
+ * @see android.hardware.camera2.params.OutputConfiguration.MIRROR_MODE_H
+ * @see androidx.camera.core.SurfaceRequest.TransformationInfo.isMirroring
+ */
+ val isSourceMirroredHorizontally: Boolean,
+
+ /**
+ * Indicates whether the source has been mirrored vertically.
+ *
+ * It is not common for a camera source to be mirror vertically, and typically
+ * [isSourceMirroredHorizontally] will be the appropriate property.
+ *
+ * It is not common for both [isSourceMirroredHorizontally] and [isSourceMirroredVertically] to
+ * be set to `true`. This is equivalent to [sourceRotation] being rotated by an additional 180
+ * degrees.
+ *
+ * @see android.hardware.camera2.params.OutputConfiguration.MIRROR_MODE_V
+ */
+ val isSourceMirroredVertically: Boolean,
+
/** Left offset of the cropRect in pixels */
val cropRectLeft: Int,
@@ -38,29 +67,30 @@
/** Bottom offset of the cropRect in pixels */
val cropRectBottom: Int,
- @get:JvmName("shouldMirror") val shouldMirror: Boolean,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TransformationInfo) return false
if (sourceRotation != other.sourceRotation) return false
+ if (isSourceMirroredHorizontally != other.isSourceMirroredHorizontally) return false
+ if (isSourceMirroredVertically != other.isSourceMirroredVertically) return false
if (cropRectLeft != other.cropRectLeft) return false
if (cropRectTop != other.cropRectTop) return false
if (cropRectRight != other.cropRectRight) return false
if (cropRectBottom != other.cropRectBottom) return false
- if (shouldMirror != other.shouldMirror) return false
return true
}
override fun hashCode(): Int {
var result = sourceRotation
+ result = 31 * result + isSourceMirroredHorizontally.hashCode()
+ result = 31 * result + isSourceMirroredVertically.hashCode()
result = 31 * result + cropRectLeft
result = 31 * result + cropRectTop
result = 31 * result + cropRectRight
result = 31 * result + cropRectBottom
- result = 31 * result + shouldMirror.hashCode()
return result
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 2959671..1f975fd2 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -32,8 +32,6 @@
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.CarText;
import androidx.car.app.model.Item;
-import androidx.car.app.model.Row;
-import androidx.car.app.model.Template;
import androidx.car.app.model.constraints.ActionsConstraints;
import androidx.car.app.utils.CollectionUtils;
import androidx.core.app.Person;
@@ -199,17 +197,9 @@
}
/**
- * Returns whether this item should be included in an indexed list.
+ * Returns whether this item can be included in indexed lists.
*
- * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
- * partition, or filter a list. Indexing is generally used for features called "Accelerators",
- * which allow a user to quickly find a particular {@link Item} in a long list.
- *
- * <p>To exclude a single item from indexed lists and accelerator features, use
- * {@link Row.Builder#setIndexable(boolean)}.
- *
- * <p>To enable/disable accelerators for the entire list, see the API for the particular
- * list-like {@link Template} that you are using.
+ * @see Builder#setIndexable(boolean)
*/
@ExperimentalCarApi
public boolean isIndexable() {
@@ -343,7 +333,26 @@
return this;
}
- /** @see #isIndexable */
+ /**
+ * Sets whether this item can be included in indexed lists. By default, this is set to
+ * {@code true}.
+ *
+ * <p>The host creates indexed lists to help users navigate through long lists more easily
+ * by sorting, filtering, or some other means.
+ *
+ * <p>For example, a media app may, by default, show a user's playlists sorted by date
+ * created. If the app provides these playlists via the {@code SectionedItemTemplate} and
+ * enables {@code #isAlphabeticalIndexingAllowed}, the user will be able to jump to their
+ * playlists that start with the letter "H". When this happens, the list is reconstructed
+ * and sorted alphabetically, then shown to the user, jumping down to the letter "H". If
+ * the item is set to {@code #setIndexable(false)}, the item will not show up in this newly
+ * sorted list.
+ *
+ * <p>Individual items can be set to be included or excluded from filtered lists, but it's
+ * also possible to enable/disable the creation of filtered lists as a whole via the
+ * template's API (eg. {@code SectionedItemTemplate
+ * .Builder#setAlphabeticalIndexingAllowed(Boolean)}).
+ */
@ExperimentalCarApi
@NonNull
public Builder setIndexable(boolean indexable) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 0645006..7e1e705 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -157,17 +157,9 @@
}
/**
- * Returns whether this item should be included in an indexed list.
+ * Returns whether this item can be included in indexed lists.
*
- * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
- * partition, or filter a list. Indexing is generally used for features called "Accelerators",
- * which allow a user to quickly find a particular {@link Item} in a long list.
- *
- * <p>To exclude a single item from indexed lists and accelerator features, use
- * {@link Row.Builder#setIndexable(boolean)}.
- *
- * <p>To enable/disable accelerators for the entire list, see the API for the particular
- * list-like {@link Template} that you are using.
+ * @see Builder#setIndexable(boolean)
*/
@ExperimentalCarApi
public boolean isIndexable() {
@@ -261,7 +253,7 @@
boolean mIsLoading;
@Nullable
Badge mBadge;
- boolean mIndexable;
+ boolean mIndexable = true;
/**
* Sets whether the item is in a loading state.
@@ -454,7 +446,27 @@
return this;
}
- /** @see #isIndexable */
+ /**
+ * Sets whether this item can be included in indexed lists. By default, this is set to
+ * {@code true}.
+ *
+ * <p>The host creates indexed lists to help users navigate through long lists more easily
+ * by sorting, filtering, or some other means.
+ *
+ * <p>For example, a media app may, by default, show a user's playlists sorted by date
+ * created. If the app provides these playlists via the {@code SectionedItemTemplate} and
+ * enables {@code #isAlphabeticalIndexingAllowed}, the user will be able to select a letter
+ * on a keyboard to jump to their playlists that start with that letter. When this happens,
+ * the list is reconstructed and sorted alphabetically, then shown to the user, jumping down
+ * to the letter. Items that are set to {@code #setIndexable(false)}, do not show up in this
+ * new sorted list. Sticking with the media example, a media app may choose to hide things
+ * like "autogenerated playlists" from the list and only keep user created playlists.
+ *
+ * <p>Individual items can be set to be included or excluded from filtered lists, but it's
+ * also possible to enable/disable the creation of filtered lists as a whole via the
+ * template's API (eg. {@code SectionedItemTemplate
+ * .Builder#setAlphabeticalIndexingAllowed(Boolean)}).
+ */
@ExperimentalCarApi
@NonNull
public Builder setIndexable(boolean indexable) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 46835b3..1ab8b4f 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -249,17 +249,9 @@
}
/**
- * Returns whether this item should be included in an indexed list.
+ * Returns whether this item can be included in indexed lists.
*
- * <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to sort,
- * partition, or filter a list. Indexing is generally used for features called "Accelerators",
- * which allow a user to quickly find a particular {@link Item} in a long list.
- *
- * <p>To exclude a single item from indexed lists and accelerator features, use
- * {@link Row.Builder#setIndexable(boolean)}.
- *
- * <p>To enable/disable accelerators for the entire list, see the API for the particular
- * list-like {@link Template} that you are using.
+ * @see Builder#setIndexable(boolean)
*/
@ExperimentalCarApi
public boolean isIndexable() {
@@ -694,7 +686,27 @@
return this;
}
- /** @see #isIndexable */
+ /**
+ * Sets whether this item can be included in indexed lists. By default, this is set to
+ * {@code true}.
+ *
+ * <p>The host creates indexed lists to help users navigate through long lists more easily
+ * by sorting, filtering, or some other means.
+ *
+ * <p>For example, a media app may, by default, show a user's playlists sorted by date
+ * created. If the app provides these playlists via the {@code SectionedItemTemplate} and
+ * enables {@code #isAlphabeticalIndexingAllowed}, the user will be able to select a letter
+ * on a keyboard to jump to their playlists that start with that letter. When this happens,
+ * the list is reconstructed and sorted alphabetically, then shown to the user, jumping down
+ * to the letter. Items that are set to {@code #setIndexable(false)}, do not show up in this
+ * new sorted list. Sticking with the media example, a media app may choose to hide things
+ * like "autogenerated playlists" from the list and only keep user created playlists.
+ *
+ * <p>Individual items can be set to be included or excluded from filtered lists, but it's
+ * also possible to enable/disable the creation of filtered lists as a whole via the
+ * template's API (eg. {@code SectionedItemTemplate
+ * .Builder#setAlphabeticalIndexingAllowed(Boolean)}).
+ */
@ExperimentalCarApi
@NonNull
public Builder setIndexable(boolean indexable) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java
index 9f1b19c0..694b82d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SectionedItemTemplate.java
@@ -262,7 +262,7 @@
/**
* Sets whether or not this template is in a loading state. If passed {@code true}, sections
- * cannot be added to the template.
+ * cannot be added to the template. By default, this is {@code false}.
*/
@NonNull
@CanIgnoreReturnValue
@@ -272,15 +272,22 @@
}
/**
- * Sets whether this list can be indexed alphabetically, by item title.
+ * Sets whether this list can be indexed alphabetically, by item title. By default, this
+ * is {@code false}.
*
* <p>"Indexing" refers to the process of examining list contents (e.g. item titles) to
- * sort,
- * partition, or filter a list. Indexing is generally used for features called
- * "Accelerators",
- * which allow a user to quickly find a particular {@link Item} in a long list.
+ * sort, partition, or filter a list. Indexing is generally used for features called
+ * "Accelerators", which allow a user to quickly find a particular {@link Item} in a long
+ * list.
*
- * <p>To exclude a single item from indexing, see the relevant item's API.
+ * <p>For example, a media app may, by default, show a user's playlists sorted by date
+ * created. If the app provides these playlists via the {@code SectionedItemTemplate} and
+ * enables {@link #isAlphabeticalIndexingAllowed}, the user will be able to jump to their
+ * playlists that start with the letter "H". When this happens, the list is reconstructed
+ * and sorted alphabetically, then shown to the user, jumping down to the letter "H".
+ *
+ * <p>Individual items may be excluded from the list by setting their {@code #isIndexable}
+ * field to {@code false}.
*/
@NonNull
@CanIgnoreReturnValue
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index 40c5ca2..688cf79 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -95,11 +95,14 @@
}
public abstract sealed class DoubleList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(double element);
method public final boolean containsAll(androidx.collection.DoubleList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
method public final double elementAt(@IntRange(from=0L) int index);
method public final inline double elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Double> defaultValue);
@@ -116,12 +119,12 @@
method public final operator double get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(double element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -137,11 +140,11 @@
method public final double last();
method public final inline double last(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
method public final int lastIndexOf(double element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
}
public final class DoubleListKt {
@@ -274,11 +277,14 @@
}
public abstract sealed class FloatList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(float element);
method public final boolean containsAll(androidx.collection.FloatList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final float elementAt(@IntRange(from=0L) int index);
method public final inline float elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Float> defaultValue);
@@ -295,12 +301,12 @@
method public final operator float get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(float element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -316,11 +322,11 @@
method public final float last();
method public final inline float last(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final int lastIndexOf(float element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
}
public final class FloatListKt {
@@ -602,11 +608,14 @@
}
public abstract sealed class IntList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(int element);
method public final boolean containsAll(androidx.collection.IntList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final int elementAt(@IntRange(from=0L) int index);
method public final inline int elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Integer> defaultValue);
@@ -623,12 +632,12 @@
method public final operator int get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(int element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -644,11 +653,11 @@
method public final int last();
method public final inline int last(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final int lastIndexOf(int element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
}
public final class IntListKt {
@@ -919,11 +928,14 @@
}
public abstract sealed class LongList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(long element);
method public final boolean containsAll(androidx.collection.LongList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final long elementAt(@IntRange(from=0L) int index);
method public final inline long elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> defaultValue);
@@ -940,12 +952,12 @@
method public final operator long get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(long element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -961,11 +973,11 @@
method public final long last();
method public final inline long last(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final int lastIndexOf(long element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
}
public final class LongListKt {
@@ -1213,8 +1225,8 @@
ctor public MutableDoubleList(optional int initialCapacity);
method public boolean add(double element);
method public void add(@IntRange(from=0L) int index, double element);
- method public boolean addAll(androidx.collection.DoubleList elements);
- method public boolean addAll(double[] elements);
+ method public inline boolean addAll(androidx.collection.DoubleList elements);
+ method public inline boolean addAll(double[] elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.DoubleList elements);
method public boolean addAll(@IntRange(from=0L) int index, double[] elements);
method public void clear();
@@ -1223,9 +1235,9 @@
method public operator void minusAssign(androidx.collection.DoubleList elements);
method public inline operator void minusAssign(double element);
method public operator void minusAssign(double[] elements);
- method public operator void plusAssign(androidx.collection.DoubleList elements);
+ method public inline operator void plusAssign(androidx.collection.DoubleList elements);
method public inline operator void plusAssign(double element);
- method public operator void plusAssign(double[] elements);
+ method public inline operator void plusAssign(double[] elements);
method public boolean remove(double element);
method public boolean removeAll(androidx.collection.DoubleList elements);
method public boolean removeAll(double[] elements);
@@ -1285,8 +1297,8 @@
ctor public MutableFloatList(optional int initialCapacity);
method public boolean add(float element);
method public void add(@IntRange(from=0L) int index, float element);
- method public boolean addAll(androidx.collection.FloatList elements);
- method public boolean addAll(float[] elements);
+ method public inline boolean addAll(androidx.collection.FloatList elements);
+ method public inline boolean addAll(float[] elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.FloatList elements);
method public boolean addAll(@IntRange(from=0L) int index, float[] elements);
method public void clear();
@@ -1295,9 +1307,9 @@
method public operator void minusAssign(androidx.collection.FloatList elements);
method public inline operator void minusAssign(float element);
method public operator void minusAssign(float[] elements);
- method public operator void plusAssign(androidx.collection.FloatList elements);
+ method public inline operator void plusAssign(androidx.collection.FloatList elements);
method public inline operator void plusAssign(float element);
- method public operator void plusAssign(float[] elements);
+ method public inline operator void plusAssign(float[] elements);
method public boolean remove(float element);
method public boolean removeAll(androidx.collection.FloatList elements);
method public boolean removeAll(float[] elements);
@@ -1415,19 +1427,19 @@
ctor public MutableIntList(optional int initialCapacity);
method public boolean add(int element);
method public void add(@IntRange(from=0L) int index, int element);
- method public boolean addAll(androidx.collection.IntList elements);
+ method public inline boolean addAll(androidx.collection.IntList elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.IntList elements);
method public boolean addAll(@IntRange(from=0L) int index, int[] elements);
- method public boolean addAll(int[] elements);
+ method public inline boolean addAll(int[] elements);
method public void clear();
method public void ensureCapacity(int capacity);
method public inline int getCapacity();
method public operator void minusAssign(androidx.collection.IntList elements);
method public inline operator void minusAssign(int element);
method public operator void minusAssign(int[] elements);
- method public operator void plusAssign(androidx.collection.IntList elements);
+ method public inline operator void plusAssign(androidx.collection.IntList elements);
method public inline operator void plusAssign(int element);
- method public operator void plusAssign(int[] elements);
+ method public inline operator void plusAssign(int[] elements);
method public boolean remove(int element);
method public boolean removeAll(androidx.collection.IntList elements);
method public boolean removeAll(int[] elements);
@@ -1545,19 +1557,19 @@
ctor public MutableLongList(optional int initialCapacity);
method public void add(@IntRange(from=0L) int index, long element);
method public boolean add(long element);
- method public boolean addAll(androidx.collection.LongList elements);
+ method public inline boolean addAll(androidx.collection.LongList elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.LongList elements);
method public boolean addAll(@IntRange(from=0L) int index, long[] elements);
- method public boolean addAll(long[] elements);
+ method public inline boolean addAll(long[] elements);
method public void clear();
method public void ensureCapacity(int capacity);
method public inline int getCapacity();
method public operator void minusAssign(androidx.collection.LongList elements);
method public inline operator void minusAssign(long element);
method public operator void minusAssign(long[] elements);
- method public operator void plusAssign(androidx.collection.LongList elements);
+ method public inline operator void plusAssign(androidx.collection.LongList elements);
method public inline operator void plusAssign(long element);
- method public operator void plusAssign(long[] elements);
+ method public inline operator void plusAssign(long[] elements);
method public boolean remove(long element);
method public boolean removeAll(androidx.collection.LongList elements);
method public boolean removeAll(long[] elements);
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index 0cd8d39..b5fcbad 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -95,11 +95,14 @@
}
public abstract sealed class DoubleList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(double element);
method public final boolean containsAll(androidx.collection.DoubleList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
method public final double elementAt(@IntRange(from=0L) int index);
method public final inline double elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Double> defaultValue);
@@ -116,12 +119,12 @@
method public final operator double get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(double element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -137,11 +140,11 @@
method public final double last();
method public final inline double last(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
method public final int lastIndexOf(double element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Double,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
field @kotlin.PublishedApi internal int _size;
field @kotlin.PublishedApi internal double[] content;
}
@@ -286,11 +289,14 @@
}
public abstract sealed class FloatList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(float element);
method public final boolean containsAll(androidx.collection.FloatList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final float elementAt(@IntRange(from=0L) int index);
method public final inline float elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Float> defaultValue);
@@ -307,12 +313,12 @@
method public final operator float get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(float element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -328,11 +334,11 @@
method public final float last();
method public final inline float last(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
method public final int lastIndexOf(float element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
field @kotlin.PublishedApi internal int _size;
field @kotlin.PublishedApi internal float[] content;
}
@@ -638,11 +644,14 @@
}
public abstract sealed class IntList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(int element);
method public final boolean containsAll(androidx.collection.IntList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final int elementAt(@IntRange(from=0L) int index);
method public final inline int elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Integer> defaultValue);
@@ -659,12 +668,12 @@
method public final operator int get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(int element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -680,11 +689,11 @@
method public final int last();
method public final inline int last(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
method public final int lastIndexOf(int element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
field @kotlin.PublishedApi internal int _size;
field @kotlin.PublishedApi internal int[] content;
}
@@ -979,11 +988,14 @@
}
public abstract sealed class LongList {
- method public final boolean any();
+ method public final inline boolean any();
method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
+ method public final int binarySearch(int element);
+ method public final int binarySearch(int element, optional int fromIndex);
+ method public final int binarySearch(int element, optional int fromIndex, optional int toIndex);
method public final operator boolean contains(long element);
method public final boolean containsAll(androidx.collection.LongList elements);
- method public final int count();
+ method public final inline int count();
method public final inline int count(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final long elementAt(@IntRange(from=0L) int index);
method public final inline long elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> defaultValue);
@@ -1000,12 +1012,12 @@
method public final operator long get(@IntRange(from=0L) int index);
method public final inline kotlin.ranges.IntRange getIndices();
method @IntRange(from=-1L) public final inline int getLastIndex();
- method @IntRange(from=0L) public final int getSize();
+ method @IntRange(from=0L) public final inline int getSize();
method public final int indexOf(long element);
method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
- method public final boolean isEmpty();
- method public final boolean isNotEmpty();
+ method public final inline boolean isEmpty();
+ method public final inline boolean isNotEmpty();
method public final String joinToString();
method public final String joinToString(optional CharSequence separator);
method public final String joinToString(optional CharSequence separator, optional CharSequence prefix);
@@ -1021,11 +1033,11 @@
method public final long last();
method public final inline long last(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
method public final int lastIndexOf(long element);
- method public final boolean none();
+ method public final inline boolean none();
method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
property public final inline kotlin.ranges.IntRange indices;
property @IntRange(from=-1L) public final inline int lastIndex;
- property @IntRange(from=0L) public final int size;
+ property @IntRange(from=0L) public final inline int size;
field @kotlin.PublishedApi internal int _size;
field @kotlin.PublishedApi internal long[] content;
}
@@ -1287,8 +1299,8 @@
ctor public MutableDoubleList(optional int initialCapacity);
method public boolean add(double element);
method public void add(@IntRange(from=0L) int index, double element);
- method public boolean addAll(androidx.collection.DoubleList elements);
- method public boolean addAll(double[] elements);
+ method public inline boolean addAll(androidx.collection.DoubleList elements);
+ method public inline boolean addAll(double[] elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.DoubleList elements);
method public boolean addAll(@IntRange(from=0L) int index, double[] elements);
method public void clear();
@@ -1297,9 +1309,9 @@
method public operator void minusAssign(androidx.collection.DoubleList elements);
method public inline operator void minusAssign(double element);
method public operator void minusAssign(double[] elements);
- method public operator void plusAssign(androidx.collection.DoubleList elements);
+ method public inline operator void plusAssign(androidx.collection.DoubleList elements);
method public inline operator void plusAssign(double element);
- method public operator void plusAssign(double[] elements);
+ method public inline operator void plusAssign(double[] elements);
method public boolean remove(double element);
method public boolean removeAll(androidx.collection.DoubleList elements);
method public boolean removeAll(double[] elements);
@@ -1361,8 +1373,8 @@
ctor public MutableFloatList(optional int initialCapacity);
method public boolean add(float element);
method public void add(@IntRange(from=0L) int index, float element);
- method public boolean addAll(androidx.collection.FloatList elements);
- method public boolean addAll(float[] elements);
+ method public inline boolean addAll(androidx.collection.FloatList elements);
+ method public inline boolean addAll(float[] elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.FloatList elements);
method public boolean addAll(@IntRange(from=0L) int index, float[] elements);
method public void clear();
@@ -1371,9 +1383,9 @@
method public operator void minusAssign(androidx.collection.FloatList elements);
method public inline operator void minusAssign(float element);
method public operator void minusAssign(float[] elements);
- method public operator void plusAssign(androidx.collection.FloatList elements);
+ method public inline operator void plusAssign(androidx.collection.FloatList elements);
method public inline operator void plusAssign(float element);
- method public operator void plusAssign(float[] elements);
+ method public inline operator void plusAssign(float[] elements);
method public boolean remove(float element);
method public boolean removeAll(androidx.collection.FloatList elements);
method public boolean removeAll(float[] elements);
@@ -1495,19 +1507,19 @@
ctor public MutableIntList(optional int initialCapacity);
method public boolean add(int element);
method public void add(@IntRange(from=0L) int index, int element);
- method public boolean addAll(androidx.collection.IntList elements);
+ method public inline boolean addAll(androidx.collection.IntList elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.IntList elements);
method public boolean addAll(@IntRange(from=0L) int index, int[] elements);
- method public boolean addAll(int[] elements);
+ method public inline boolean addAll(int[] elements);
method public void clear();
method public void ensureCapacity(int capacity);
method public inline int getCapacity();
method public operator void minusAssign(androidx.collection.IntList elements);
method public inline operator void minusAssign(int element);
method public operator void minusAssign(int[] elements);
- method public operator void plusAssign(androidx.collection.IntList elements);
+ method public inline operator void plusAssign(androidx.collection.IntList elements);
method public inline operator void plusAssign(int element);
- method public operator void plusAssign(int[] elements);
+ method public inline operator void plusAssign(int[] elements);
method public boolean remove(int element);
method public boolean removeAll(androidx.collection.IntList elements);
method public boolean removeAll(int[] elements);
@@ -1629,19 +1641,19 @@
ctor public MutableLongList(optional int initialCapacity);
method public void add(@IntRange(from=0L) int index, long element);
method public boolean add(long element);
- method public boolean addAll(androidx.collection.LongList elements);
+ method public inline boolean addAll(androidx.collection.LongList elements);
method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.LongList elements);
method public boolean addAll(@IntRange(from=0L) int index, long[] elements);
- method public boolean addAll(long[] elements);
+ method public inline boolean addAll(long[] elements);
method public void clear();
method public void ensureCapacity(int capacity);
method public inline int getCapacity();
method public operator void minusAssign(androidx.collection.LongList elements);
method public inline operator void minusAssign(long element);
method public operator void minusAssign(long[] elements);
- method public operator void plusAssign(androidx.collection.LongList elements);
+ method public inline operator void plusAssign(androidx.collection.LongList elements);
method public inline operator void plusAssign(long element);
- method public operator void plusAssign(long[] elements);
+ method public inline operator void plusAssign(long[] elements);
method public boolean remove(long element);
method public boolean removeAll(androidx.collection.LongList elements);
method public boolean removeAll(long[] elements);
diff --git a/collection/collection/bcv/native/current.txt b/collection/collection/bcv/native/current.txt
index a189609..215b18d 100644
--- a/collection/collection/bcv/native/current.txt
+++ b/collection/collection/bcv/native/current.txt
@@ -468,16 +468,12 @@
final fun add(kotlin/Double): kotlin/Boolean // androidx.collection/MutableDoubleList.add|add(kotlin.Double){}[0]
final fun add(kotlin/Int, kotlin/Double) // androidx.collection/MutableDoubleList.add|add(kotlin.Int;kotlin.Double){}[0]
- final fun addAll(androidx.collection/DoubleList): kotlin/Boolean // androidx.collection/MutableDoubleList.addAll|addAll(androidx.collection.DoubleList){}[0]
- final fun addAll(kotlin/DoubleArray): kotlin/Boolean // androidx.collection/MutableDoubleList.addAll|addAll(kotlin.DoubleArray){}[0]
final fun addAll(kotlin/Int, androidx.collection/DoubleList): kotlin/Boolean // androidx.collection/MutableDoubleList.addAll|addAll(kotlin.Int;androidx.collection.DoubleList){}[0]
final fun addAll(kotlin/Int, kotlin/DoubleArray): kotlin/Boolean // androidx.collection/MutableDoubleList.addAll|addAll(kotlin.Int;kotlin.DoubleArray){}[0]
final fun clear() // androidx.collection/MutableDoubleList.clear|clear(){}[0]
final fun ensureCapacity(kotlin/Int) // androidx.collection/MutableDoubleList.ensureCapacity|ensureCapacity(kotlin.Int){}[0]
final fun minusAssign(androidx.collection/DoubleList) // androidx.collection/MutableDoubleList.minusAssign|minusAssign(androidx.collection.DoubleList){}[0]
final fun minusAssign(kotlin/DoubleArray) // androidx.collection/MutableDoubleList.minusAssign|minusAssign(kotlin.DoubleArray){}[0]
- final fun plusAssign(androidx.collection/DoubleList) // androidx.collection/MutableDoubleList.plusAssign|plusAssign(androidx.collection.DoubleList){}[0]
- final fun plusAssign(kotlin/DoubleArray) // androidx.collection/MutableDoubleList.plusAssign|plusAssign(kotlin.DoubleArray){}[0]
final fun remove(kotlin/Double): kotlin/Boolean // androidx.collection/MutableDoubleList.remove|remove(kotlin.Double){}[0]
final fun removeAll(androidx.collection/DoubleList): kotlin/Boolean // androidx.collection/MutableDoubleList.removeAll|removeAll(androidx.collection.DoubleList){}[0]
final fun removeAll(kotlin/DoubleArray): kotlin/Boolean // androidx.collection/MutableDoubleList.removeAll|removeAll(kotlin.DoubleArray){}[0]
@@ -489,8 +485,12 @@
final fun sort() // androidx.collection/MutableDoubleList.sort|sort(){}[0]
final fun sortDescending() // androidx.collection/MutableDoubleList.sortDescending|sortDescending(){}[0]
final fun trim(kotlin/Int = ...) // androidx.collection/MutableDoubleList.trim|trim(kotlin.Int){}[0]
+ final inline fun addAll(androidx.collection/DoubleList): kotlin/Boolean // androidx.collection/MutableDoubleList.addAll|addAll(androidx.collection.DoubleList){}[0]
+ final inline fun addAll(kotlin/DoubleArray): kotlin/Boolean // androidx.collection/MutableDoubleList.addAll|addAll(kotlin.DoubleArray){}[0]
final inline fun minusAssign(kotlin/Double) // androidx.collection/MutableDoubleList.minusAssign|minusAssign(kotlin.Double){}[0]
+ final inline fun plusAssign(androidx.collection/DoubleList) // androidx.collection/MutableDoubleList.plusAssign|plusAssign(androidx.collection.DoubleList){}[0]
final inline fun plusAssign(kotlin/Double) // androidx.collection/MutableDoubleList.plusAssign|plusAssign(kotlin.Double){}[0]
+ final inline fun plusAssign(kotlin/DoubleArray) // androidx.collection/MutableDoubleList.plusAssign|plusAssign(kotlin.DoubleArray){}[0]
}
final class androidx.collection/MutableFloatFloatMap : androidx.collection/FloatFloatMap { // androidx.collection/MutableFloatFloatMap|null[0]
@@ -543,16 +543,12 @@
final fun add(kotlin/Float): kotlin/Boolean // androidx.collection/MutableFloatList.add|add(kotlin.Float){}[0]
final fun add(kotlin/Int, kotlin/Float) // androidx.collection/MutableFloatList.add|add(kotlin.Int;kotlin.Float){}[0]
- final fun addAll(androidx.collection/FloatList): kotlin/Boolean // androidx.collection/MutableFloatList.addAll|addAll(androidx.collection.FloatList){}[0]
- final fun addAll(kotlin/FloatArray): kotlin/Boolean // androidx.collection/MutableFloatList.addAll|addAll(kotlin.FloatArray){}[0]
final fun addAll(kotlin/Int, androidx.collection/FloatList): kotlin/Boolean // androidx.collection/MutableFloatList.addAll|addAll(kotlin.Int;androidx.collection.FloatList){}[0]
final fun addAll(kotlin/Int, kotlin/FloatArray): kotlin/Boolean // androidx.collection/MutableFloatList.addAll|addAll(kotlin.Int;kotlin.FloatArray){}[0]
final fun clear() // androidx.collection/MutableFloatList.clear|clear(){}[0]
final fun ensureCapacity(kotlin/Int) // androidx.collection/MutableFloatList.ensureCapacity|ensureCapacity(kotlin.Int){}[0]
final fun minusAssign(androidx.collection/FloatList) // androidx.collection/MutableFloatList.minusAssign|minusAssign(androidx.collection.FloatList){}[0]
final fun minusAssign(kotlin/FloatArray) // androidx.collection/MutableFloatList.minusAssign|minusAssign(kotlin.FloatArray){}[0]
- final fun plusAssign(androidx.collection/FloatList) // androidx.collection/MutableFloatList.plusAssign|plusAssign(androidx.collection.FloatList){}[0]
- final fun plusAssign(kotlin/FloatArray) // androidx.collection/MutableFloatList.plusAssign|plusAssign(kotlin.FloatArray){}[0]
final fun remove(kotlin/Float): kotlin/Boolean // androidx.collection/MutableFloatList.remove|remove(kotlin.Float){}[0]
final fun removeAll(androidx.collection/FloatList): kotlin/Boolean // androidx.collection/MutableFloatList.removeAll|removeAll(androidx.collection.FloatList){}[0]
final fun removeAll(kotlin/FloatArray): kotlin/Boolean // androidx.collection/MutableFloatList.removeAll|removeAll(kotlin.FloatArray){}[0]
@@ -564,8 +560,12 @@
final fun sort() // androidx.collection/MutableFloatList.sort|sort(){}[0]
final fun sortDescending() // androidx.collection/MutableFloatList.sortDescending|sortDescending(){}[0]
final fun trim(kotlin/Int = ...) // androidx.collection/MutableFloatList.trim|trim(kotlin.Int){}[0]
+ final inline fun addAll(androidx.collection/FloatList): kotlin/Boolean // androidx.collection/MutableFloatList.addAll|addAll(androidx.collection.FloatList){}[0]
+ final inline fun addAll(kotlin/FloatArray): kotlin/Boolean // androidx.collection/MutableFloatList.addAll|addAll(kotlin.FloatArray){}[0]
final inline fun minusAssign(kotlin/Float) // androidx.collection/MutableFloatList.minusAssign|minusAssign(kotlin.Float){}[0]
+ final inline fun plusAssign(androidx.collection/FloatList) // androidx.collection/MutableFloatList.plusAssign|plusAssign(androidx.collection.FloatList){}[0]
final inline fun plusAssign(kotlin/Float) // androidx.collection/MutableFloatList.plusAssign|plusAssign(kotlin.Float){}[0]
+ final inline fun plusAssign(kotlin/FloatArray) // androidx.collection/MutableFloatList.plusAssign|plusAssign(kotlin.FloatArray){}[0]
}
final class androidx.collection/MutableFloatLongMap : androidx.collection/FloatLongMap { // androidx.collection/MutableFloatLongMap|null[0]
@@ -658,16 +658,12 @@
final fun add(kotlin/Int): kotlin/Boolean // androidx.collection/MutableIntList.add|add(kotlin.Int){}[0]
final fun add(kotlin/Int, kotlin/Int) // androidx.collection/MutableIntList.add|add(kotlin.Int;kotlin.Int){}[0]
- final fun addAll(androidx.collection/IntList): kotlin/Boolean // androidx.collection/MutableIntList.addAll|addAll(androidx.collection.IntList){}[0]
final fun addAll(kotlin/Int, androidx.collection/IntList): kotlin/Boolean // androidx.collection/MutableIntList.addAll|addAll(kotlin.Int;androidx.collection.IntList){}[0]
final fun addAll(kotlin/Int, kotlin/IntArray): kotlin/Boolean // androidx.collection/MutableIntList.addAll|addAll(kotlin.Int;kotlin.IntArray){}[0]
- final fun addAll(kotlin/IntArray): kotlin/Boolean // androidx.collection/MutableIntList.addAll|addAll(kotlin.IntArray){}[0]
final fun clear() // androidx.collection/MutableIntList.clear|clear(){}[0]
final fun ensureCapacity(kotlin/Int) // androidx.collection/MutableIntList.ensureCapacity|ensureCapacity(kotlin.Int){}[0]
final fun minusAssign(androidx.collection/IntList) // androidx.collection/MutableIntList.minusAssign|minusAssign(androidx.collection.IntList){}[0]
final fun minusAssign(kotlin/IntArray) // androidx.collection/MutableIntList.minusAssign|minusAssign(kotlin.IntArray){}[0]
- final fun plusAssign(androidx.collection/IntList) // androidx.collection/MutableIntList.plusAssign|plusAssign(androidx.collection.IntList){}[0]
- final fun plusAssign(kotlin/IntArray) // androidx.collection/MutableIntList.plusAssign|plusAssign(kotlin.IntArray){}[0]
final fun remove(kotlin/Int): kotlin/Boolean // androidx.collection/MutableIntList.remove|remove(kotlin.Int){}[0]
final fun removeAll(androidx.collection/IntList): kotlin/Boolean // androidx.collection/MutableIntList.removeAll|removeAll(androidx.collection.IntList){}[0]
final fun removeAll(kotlin/IntArray): kotlin/Boolean // androidx.collection/MutableIntList.removeAll|removeAll(kotlin.IntArray){}[0]
@@ -679,8 +675,12 @@
final fun sort() // androidx.collection/MutableIntList.sort|sort(){}[0]
final fun sortDescending() // androidx.collection/MutableIntList.sortDescending|sortDescending(){}[0]
final fun trim(kotlin/Int = ...) // androidx.collection/MutableIntList.trim|trim(kotlin.Int){}[0]
+ final inline fun addAll(androidx.collection/IntList): kotlin/Boolean // androidx.collection/MutableIntList.addAll|addAll(androidx.collection.IntList){}[0]
+ final inline fun addAll(kotlin/IntArray): kotlin/Boolean // androidx.collection/MutableIntList.addAll|addAll(kotlin.IntArray){}[0]
final inline fun minusAssign(kotlin/Int) // androidx.collection/MutableIntList.minusAssign|minusAssign(kotlin.Int){}[0]
+ final inline fun plusAssign(androidx.collection/IntList) // androidx.collection/MutableIntList.plusAssign|plusAssign(androidx.collection.IntList){}[0]
final inline fun plusAssign(kotlin/Int) // androidx.collection/MutableIntList.plusAssign|plusAssign(kotlin.Int){}[0]
+ final inline fun plusAssign(kotlin/IntArray) // androidx.collection/MutableIntList.plusAssign|plusAssign(kotlin.IntArray){}[0]
}
final class androidx.collection/MutableIntLongMap : androidx.collection/IntLongMap { // androidx.collection/MutableIntLongMap|null[0]
@@ -773,16 +773,12 @@
final fun add(kotlin/Int, kotlin/Long) // androidx.collection/MutableLongList.add|add(kotlin.Int;kotlin.Long){}[0]
final fun add(kotlin/Long): kotlin/Boolean // androidx.collection/MutableLongList.add|add(kotlin.Long){}[0]
- final fun addAll(androidx.collection/LongList): kotlin/Boolean // androidx.collection/MutableLongList.addAll|addAll(androidx.collection.LongList){}[0]
final fun addAll(kotlin/Int, androidx.collection/LongList): kotlin/Boolean // androidx.collection/MutableLongList.addAll|addAll(kotlin.Int;androidx.collection.LongList){}[0]
final fun addAll(kotlin/Int, kotlin/LongArray): kotlin/Boolean // androidx.collection/MutableLongList.addAll|addAll(kotlin.Int;kotlin.LongArray){}[0]
- final fun addAll(kotlin/LongArray): kotlin/Boolean // androidx.collection/MutableLongList.addAll|addAll(kotlin.LongArray){}[0]
final fun clear() // androidx.collection/MutableLongList.clear|clear(){}[0]
final fun ensureCapacity(kotlin/Int) // androidx.collection/MutableLongList.ensureCapacity|ensureCapacity(kotlin.Int){}[0]
final fun minusAssign(androidx.collection/LongList) // androidx.collection/MutableLongList.minusAssign|minusAssign(androidx.collection.LongList){}[0]
final fun minusAssign(kotlin/LongArray) // androidx.collection/MutableLongList.minusAssign|minusAssign(kotlin.LongArray){}[0]
- final fun plusAssign(androidx.collection/LongList) // androidx.collection/MutableLongList.plusAssign|plusAssign(androidx.collection.LongList){}[0]
- final fun plusAssign(kotlin/LongArray) // androidx.collection/MutableLongList.plusAssign|plusAssign(kotlin.LongArray){}[0]
final fun remove(kotlin/Long): kotlin/Boolean // androidx.collection/MutableLongList.remove|remove(kotlin.Long){}[0]
final fun removeAll(androidx.collection/LongList): kotlin/Boolean // androidx.collection/MutableLongList.removeAll|removeAll(androidx.collection.LongList){}[0]
final fun removeAll(kotlin/LongArray): kotlin/Boolean // androidx.collection/MutableLongList.removeAll|removeAll(kotlin.LongArray){}[0]
@@ -794,8 +790,12 @@
final fun sort() // androidx.collection/MutableLongList.sort|sort(){}[0]
final fun sortDescending() // androidx.collection/MutableLongList.sortDescending|sortDescending(){}[0]
final fun trim(kotlin/Int = ...) // androidx.collection/MutableLongList.trim|trim(kotlin.Int){}[0]
+ final inline fun addAll(androidx.collection/LongList): kotlin/Boolean // androidx.collection/MutableLongList.addAll|addAll(androidx.collection.LongList){}[0]
+ final inline fun addAll(kotlin/LongArray): kotlin/Boolean // androidx.collection/MutableLongList.addAll|addAll(kotlin.LongArray){}[0]
final inline fun minusAssign(kotlin/Long) // androidx.collection/MutableLongList.minusAssign|minusAssign(kotlin.Long){}[0]
+ final inline fun plusAssign(androidx.collection/LongList) // androidx.collection/MutableLongList.plusAssign|plusAssign(androidx.collection.LongList){}[0]
final inline fun plusAssign(kotlin/Long) // androidx.collection/MutableLongList.plusAssign|plusAssign(kotlin.Long){}[0]
+ final inline fun plusAssign(kotlin/LongArray) // androidx.collection/MutableLongList.plusAssign|plusAssign(kotlin.LongArray){}[0]
}
final class androidx.collection/MutableLongLongMap : androidx.collection/LongLongMap { // androidx.collection/MutableLongLongMap|null[0]
@@ -1439,7 +1439,7 @@
final val lastIndex // androidx.collection/DoubleList.lastIndex|{}lastIndex[0]
final inline fun <get-lastIndex>(): kotlin/Int // androidx.collection/DoubleList.lastIndex.<get-lastIndex>|<get-lastIndex>(){}[0]
final val size // androidx.collection/DoubleList.size|{}size[0]
- final fun <get-size>(): kotlin/Int // androidx.collection/DoubleList.size.<get-size>|<get-size>(){}[0]
+ final inline fun <get-size>(): kotlin/Int // androidx.collection/DoubleList.size.<get-size>|<get-size>(){}[0]
final var _size // androidx.collection/DoubleList._size|{}_size[0]
final fun <get-_size>(): kotlin/Int // androidx.collection/DoubleList._size.<get-_size>|<get-_size>(){}[0]
@@ -1448,25 +1448,23 @@
final fun <get-content>(): kotlin/DoubleArray // androidx.collection/DoubleList.content.<get-content>|<get-content>(){}[0]
final fun <set-content>(kotlin/DoubleArray) // androidx.collection/DoubleList.content.<set-content>|<set-content>(kotlin.DoubleArray){}[0]
- final fun any(): kotlin/Boolean // androidx.collection/DoubleList.any|any(){}[0]
+ final fun binarySearch(kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlin/Int // androidx.collection/DoubleList.binarySearch|binarySearch(kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun contains(kotlin/Double): kotlin/Boolean // androidx.collection/DoubleList.contains|contains(kotlin.Double){}[0]
final fun containsAll(androidx.collection/DoubleList): kotlin/Boolean // androidx.collection/DoubleList.containsAll|containsAll(androidx.collection.DoubleList){}[0]
- final fun count(): kotlin/Int // androidx.collection/DoubleList.count|count(){}[0]
final fun elementAt(kotlin/Int): kotlin/Double // androidx.collection/DoubleList.elementAt|elementAt(kotlin.Int){}[0]
final fun first(): kotlin/Double // androidx.collection/DoubleList.first|first(){}[0]
final fun get(kotlin/Int): kotlin/Double // androidx.collection/DoubleList.get|get(kotlin.Int){}[0]
final fun indexOf(kotlin/Double): kotlin/Int // androidx.collection/DoubleList.indexOf|indexOf(kotlin.Double){}[0]
- final fun isEmpty(): kotlin/Boolean // androidx.collection/DoubleList.isEmpty|isEmpty(){}[0]
- final fun isNotEmpty(): kotlin/Boolean // androidx.collection/DoubleList.isNotEmpty|isNotEmpty(){}[0]
final fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ...): kotlin/String // androidx.collection/DoubleList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence){}[0]
final fun last(): kotlin/Double // androidx.collection/DoubleList.last|last(){}[0]
final fun lastIndexOf(kotlin/Double): kotlin/Int // androidx.collection/DoubleList.lastIndexOf|lastIndexOf(kotlin.Double){}[0]
- final fun none(): kotlin/Boolean // androidx.collection/DoubleList.none|none(){}[0]
final inline fun <#A1: kotlin/Any?> fold(#A1, kotlin/Function2<#A1, kotlin/Double, #A1>): #A1 // androidx.collection/DoubleList.fold|fold(0:0;kotlin.Function2<0:0,kotlin.Double,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldIndexed(#A1, kotlin/Function3<kotlin/Int, #A1, kotlin/Double, #A1>): #A1 // androidx.collection/DoubleList.foldIndexed|foldIndexed(0:0;kotlin.Function3<kotlin.Int,0:0,kotlin.Double,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRight(#A1, kotlin/Function2<kotlin/Double, #A1, #A1>): #A1 // androidx.collection/DoubleList.foldRight|foldRight(0:0;kotlin.Function2<kotlin.Double,0:0,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRightIndexed(#A1, kotlin/Function3<kotlin/Int, kotlin/Double, #A1, #A1>): #A1 // androidx.collection/DoubleList.foldRightIndexed|foldRightIndexed(0:0;kotlin.Function3<kotlin.Int,kotlin.Double,0:0,0:0>){0§<kotlin.Any?>}[0]
+ final inline fun any(): kotlin/Boolean // androidx.collection/DoubleList.any|any(){}[0]
final inline fun any(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Boolean // androidx.collection/DoubleList.any|any(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
+ final inline fun count(): kotlin/Int // androidx.collection/DoubleList.count|count(){}[0]
final inline fun count(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Int // androidx.collection/DoubleList.count|count(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
final inline fun elementAtOrElse(kotlin/Int, kotlin/Function1<kotlin/Int, kotlin/Double>): kotlin/Double // androidx.collection/DoubleList.elementAtOrElse|elementAtOrElse(kotlin.Int;kotlin.Function1<kotlin.Int,kotlin.Double>){}[0]
final inline fun first(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Double // androidx.collection/DoubleList.first|first(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
@@ -1476,8 +1474,11 @@
final inline fun forEachReversedIndexed(kotlin/Function2<kotlin/Int, kotlin/Double, kotlin/Unit>) // androidx.collection/DoubleList.forEachReversedIndexed|forEachReversedIndexed(kotlin.Function2<kotlin.Int,kotlin.Double,kotlin.Unit>){}[0]
final inline fun indexOfFirst(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Int // androidx.collection/DoubleList.indexOfFirst|indexOfFirst(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
final inline fun indexOfLast(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Int // androidx.collection/DoubleList.indexOfLast|indexOfLast(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
+ final inline fun isEmpty(): kotlin/Boolean // androidx.collection/DoubleList.isEmpty|isEmpty(){}[0]
+ final inline fun isNotEmpty(): kotlin/Boolean // androidx.collection/DoubleList.isNotEmpty|isNotEmpty(){}[0]
final inline fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ..., crossinline kotlin/Function1<kotlin/Double, kotlin/CharSequence>): kotlin/String // androidx.collection/DoubleList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence;kotlin.Function1<kotlin.Double,kotlin.CharSequence>){}[0]
final inline fun last(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Double // androidx.collection/DoubleList.last|last(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
+ final inline fun none(): kotlin/Boolean // androidx.collection/DoubleList.none|none(){}[0]
final inline fun reversedAny(kotlin/Function1<kotlin/Double, kotlin/Boolean>): kotlin/Boolean // androidx.collection/DoubleList.reversedAny|reversedAny(kotlin.Function1<kotlin.Double,kotlin.Boolean>){}[0]
open fun equals(kotlin/Any?): kotlin/Boolean // androidx.collection/DoubleList.equals|equals(kotlin.Any?){}[0]
open fun hashCode(): kotlin/Int // androidx.collection/DoubleList.hashCode|hashCode(){}[0]
@@ -1580,7 +1581,7 @@
final val lastIndex // androidx.collection/FloatList.lastIndex|{}lastIndex[0]
final inline fun <get-lastIndex>(): kotlin/Int // androidx.collection/FloatList.lastIndex.<get-lastIndex>|<get-lastIndex>(){}[0]
final val size // androidx.collection/FloatList.size|{}size[0]
- final fun <get-size>(): kotlin/Int // androidx.collection/FloatList.size.<get-size>|<get-size>(){}[0]
+ final inline fun <get-size>(): kotlin/Int // androidx.collection/FloatList.size.<get-size>|<get-size>(){}[0]
final var _size // androidx.collection/FloatList._size|{}_size[0]
final fun <get-_size>(): kotlin/Int // androidx.collection/FloatList._size.<get-_size>|<get-_size>(){}[0]
@@ -1589,25 +1590,23 @@
final fun <get-content>(): kotlin/FloatArray // androidx.collection/FloatList.content.<get-content>|<get-content>(){}[0]
final fun <set-content>(kotlin/FloatArray) // androidx.collection/FloatList.content.<set-content>|<set-content>(kotlin.FloatArray){}[0]
- final fun any(): kotlin/Boolean // androidx.collection/FloatList.any|any(){}[0]
+ final fun binarySearch(kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlin/Int // androidx.collection/FloatList.binarySearch|binarySearch(kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun contains(kotlin/Float): kotlin/Boolean // androidx.collection/FloatList.contains|contains(kotlin.Float){}[0]
final fun containsAll(androidx.collection/FloatList): kotlin/Boolean // androidx.collection/FloatList.containsAll|containsAll(androidx.collection.FloatList){}[0]
- final fun count(): kotlin/Int // androidx.collection/FloatList.count|count(){}[0]
final fun elementAt(kotlin/Int): kotlin/Float // androidx.collection/FloatList.elementAt|elementAt(kotlin.Int){}[0]
final fun first(): kotlin/Float // androidx.collection/FloatList.first|first(){}[0]
final fun get(kotlin/Int): kotlin/Float // androidx.collection/FloatList.get|get(kotlin.Int){}[0]
final fun indexOf(kotlin/Float): kotlin/Int // androidx.collection/FloatList.indexOf|indexOf(kotlin.Float){}[0]
- final fun isEmpty(): kotlin/Boolean // androidx.collection/FloatList.isEmpty|isEmpty(){}[0]
- final fun isNotEmpty(): kotlin/Boolean // androidx.collection/FloatList.isNotEmpty|isNotEmpty(){}[0]
final fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ...): kotlin/String // androidx.collection/FloatList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence){}[0]
final fun last(): kotlin/Float // androidx.collection/FloatList.last|last(){}[0]
final fun lastIndexOf(kotlin/Float): kotlin/Int // androidx.collection/FloatList.lastIndexOf|lastIndexOf(kotlin.Float){}[0]
- final fun none(): kotlin/Boolean // androidx.collection/FloatList.none|none(){}[0]
final inline fun <#A1: kotlin/Any?> fold(#A1, kotlin/Function2<#A1, kotlin/Float, #A1>): #A1 // androidx.collection/FloatList.fold|fold(0:0;kotlin.Function2<0:0,kotlin.Float,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldIndexed(#A1, kotlin/Function3<kotlin/Int, #A1, kotlin/Float, #A1>): #A1 // androidx.collection/FloatList.foldIndexed|foldIndexed(0:0;kotlin.Function3<kotlin.Int,0:0,kotlin.Float,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRight(#A1, kotlin/Function2<kotlin/Float, #A1, #A1>): #A1 // androidx.collection/FloatList.foldRight|foldRight(0:0;kotlin.Function2<kotlin.Float,0:0,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRightIndexed(#A1, kotlin/Function3<kotlin/Int, kotlin/Float, #A1, #A1>): #A1 // androidx.collection/FloatList.foldRightIndexed|foldRightIndexed(0:0;kotlin.Function3<kotlin.Int,kotlin.Float,0:0,0:0>){0§<kotlin.Any?>}[0]
+ final inline fun any(): kotlin/Boolean // androidx.collection/FloatList.any|any(){}[0]
final inline fun any(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatList.any|any(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun count(): kotlin/Int // androidx.collection/FloatList.count|count(){}[0]
final inline fun count(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatList.count|count(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
final inline fun elementAtOrElse(kotlin/Int, kotlin/Function1<kotlin/Int, kotlin/Float>): kotlin/Float // androidx.collection/FloatList.elementAtOrElse|elementAtOrElse(kotlin.Int;kotlin.Function1<kotlin.Int,kotlin.Float>){}[0]
final inline fun first(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Float // androidx.collection/FloatList.first|first(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
@@ -1617,8 +1616,11 @@
final inline fun forEachReversedIndexed(kotlin/Function2<kotlin/Int, kotlin/Float, kotlin/Unit>) // androidx.collection/FloatList.forEachReversedIndexed|forEachReversedIndexed(kotlin.Function2<kotlin.Int,kotlin.Float,kotlin.Unit>){}[0]
final inline fun indexOfFirst(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatList.indexOfFirst|indexOfFirst(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
final inline fun indexOfLast(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Int // androidx.collection/FloatList.indexOfLast|indexOfLast(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun isEmpty(): kotlin/Boolean // androidx.collection/FloatList.isEmpty|isEmpty(){}[0]
+ final inline fun isNotEmpty(): kotlin/Boolean // androidx.collection/FloatList.isNotEmpty|isNotEmpty(){}[0]
final inline fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ..., crossinline kotlin/Function1<kotlin/Float, kotlin/CharSequence>): kotlin/String // androidx.collection/FloatList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence;kotlin.Function1<kotlin.Float,kotlin.CharSequence>){}[0]
final inline fun last(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Float // androidx.collection/FloatList.last|last(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
+ final inline fun none(): kotlin/Boolean // androidx.collection/FloatList.none|none(){}[0]
final inline fun reversedAny(kotlin/Function1<kotlin/Float, kotlin/Boolean>): kotlin/Boolean // androidx.collection/FloatList.reversedAny|reversedAny(kotlin.Function1<kotlin.Float,kotlin.Boolean>){}[0]
open fun equals(kotlin/Any?): kotlin/Boolean // androidx.collection/FloatList.equals|equals(kotlin.Any?){}[0]
open fun hashCode(): kotlin/Int // androidx.collection/FloatList.hashCode|hashCode(){}[0]
@@ -1800,7 +1802,7 @@
final val lastIndex // androidx.collection/IntList.lastIndex|{}lastIndex[0]
final inline fun <get-lastIndex>(): kotlin/Int // androidx.collection/IntList.lastIndex.<get-lastIndex>|<get-lastIndex>(){}[0]
final val size // androidx.collection/IntList.size|{}size[0]
- final fun <get-size>(): kotlin/Int // androidx.collection/IntList.size.<get-size>|<get-size>(){}[0]
+ final inline fun <get-size>(): kotlin/Int // androidx.collection/IntList.size.<get-size>|<get-size>(){}[0]
final var _size // androidx.collection/IntList._size|{}_size[0]
final fun <get-_size>(): kotlin/Int // androidx.collection/IntList._size.<get-_size>|<get-_size>(){}[0]
@@ -1809,25 +1811,23 @@
final fun <get-content>(): kotlin/IntArray // androidx.collection/IntList.content.<get-content>|<get-content>(){}[0]
final fun <set-content>(kotlin/IntArray) // androidx.collection/IntList.content.<set-content>|<set-content>(kotlin.IntArray){}[0]
- final fun any(): kotlin/Boolean // androidx.collection/IntList.any|any(){}[0]
+ final fun binarySearch(kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlin/Int // androidx.collection/IntList.binarySearch|binarySearch(kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun contains(kotlin/Int): kotlin/Boolean // androidx.collection/IntList.contains|contains(kotlin.Int){}[0]
final fun containsAll(androidx.collection/IntList): kotlin/Boolean // androidx.collection/IntList.containsAll|containsAll(androidx.collection.IntList){}[0]
- final fun count(): kotlin/Int // androidx.collection/IntList.count|count(){}[0]
final fun elementAt(kotlin/Int): kotlin/Int // androidx.collection/IntList.elementAt|elementAt(kotlin.Int){}[0]
final fun first(): kotlin/Int // androidx.collection/IntList.first|first(){}[0]
final fun get(kotlin/Int): kotlin/Int // androidx.collection/IntList.get|get(kotlin.Int){}[0]
final fun indexOf(kotlin/Int): kotlin/Int // androidx.collection/IntList.indexOf|indexOf(kotlin.Int){}[0]
- final fun isEmpty(): kotlin/Boolean // androidx.collection/IntList.isEmpty|isEmpty(){}[0]
- final fun isNotEmpty(): kotlin/Boolean // androidx.collection/IntList.isNotEmpty|isNotEmpty(){}[0]
final fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ...): kotlin/String // androidx.collection/IntList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence){}[0]
final fun last(): kotlin/Int // androidx.collection/IntList.last|last(){}[0]
final fun lastIndexOf(kotlin/Int): kotlin/Int // androidx.collection/IntList.lastIndexOf|lastIndexOf(kotlin.Int){}[0]
- final fun none(): kotlin/Boolean // androidx.collection/IntList.none|none(){}[0]
final inline fun <#A1: kotlin/Any?> fold(#A1, kotlin/Function2<#A1, kotlin/Int, #A1>): #A1 // androidx.collection/IntList.fold|fold(0:0;kotlin.Function2<0:0,kotlin.Int,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldIndexed(#A1, kotlin/Function3<kotlin/Int, #A1, kotlin/Int, #A1>): #A1 // androidx.collection/IntList.foldIndexed|foldIndexed(0:0;kotlin.Function3<kotlin.Int,0:0,kotlin.Int,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRight(#A1, kotlin/Function2<kotlin/Int, #A1, #A1>): #A1 // androidx.collection/IntList.foldRight|foldRight(0:0;kotlin.Function2<kotlin.Int,0:0,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRightIndexed(#A1, kotlin/Function3<kotlin/Int, kotlin/Int, #A1, #A1>): #A1 // androidx.collection/IntList.foldRightIndexed|foldRightIndexed(0:0;kotlin.Function3<kotlin.Int,kotlin.Int,0:0,0:0>){0§<kotlin.Any?>}[0]
+ final inline fun any(): kotlin/Boolean // androidx.collection/IntList.any|any(){}[0]
final inline fun any(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntList.any|any(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun count(): kotlin/Int // androidx.collection/IntList.count|count(){}[0]
final inline fun count(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/IntList.count|count(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
final inline fun elementAtOrElse(kotlin/Int, kotlin/Function1<kotlin/Int, kotlin/Int>): kotlin/Int // androidx.collection/IntList.elementAtOrElse|elementAtOrElse(kotlin.Int;kotlin.Function1<kotlin.Int,kotlin.Int>){}[0]
final inline fun first(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/IntList.first|first(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
@@ -1837,8 +1837,11 @@
final inline fun forEachReversedIndexed(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // androidx.collection/IntList.forEachReversedIndexed|forEachReversedIndexed(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
final inline fun indexOfFirst(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/IntList.indexOfFirst|indexOfFirst(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
final inline fun indexOfLast(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/IntList.indexOfLast|indexOfLast(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun isEmpty(): kotlin/Boolean // androidx.collection/IntList.isEmpty|isEmpty(){}[0]
+ final inline fun isNotEmpty(): kotlin/Boolean // androidx.collection/IntList.isNotEmpty|isNotEmpty(){}[0]
final inline fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ..., crossinline kotlin/Function1<kotlin/Int, kotlin/CharSequence>): kotlin/String // androidx.collection/IntList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence;kotlin.Function1<kotlin.Int,kotlin.CharSequence>){}[0]
final inline fun last(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Int // androidx.collection/IntList.last|last(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
+ final inline fun none(): kotlin/Boolean // androidx.collection/IntList.none|none(){}[0]
final inline fun reversedAny(kotlin/Function1<kotlin/Int, kotlin/Boolean>): kotlin/Boolean // androidx.collection/IntList.reversedAny|reversedAny(kotlin.Function1<kotlin.Int,kotlin.Boolean>){}[0]
open fun equals(kotlin/Any?): kotlin/Boolean // androidx.collection/IntList.equals|equals(kotlin.Any?){}[0]
open fun hashCode(): kotlin/Int // androidx.collection/IntList.hashCode|hashCode(){}[0]
@@ -2020,7 +2023,7 @@
final val lastIndex // androidx.collection/LongList.lastIndex|{}lastIndex[0]
final inline fun <get-lastIndex>(): kotlin/Int // androidx.collection/LongList.lastIndex.<get-lastIndex>|<get-lastIndex>(){}[0]
final val size // androidx.collection/LongList.size|{}size[0]
- final fun <get-size>(): kotlin/Int // androidx.collection/LongList.size.<get-size>|<get-size>(){}[0]
+ final inline fun <get-size>(): kotlin/Int // androidx.collection/LongList.size.<get-size>|<get-size>(){}[0]
final var _size // androidx.collection/LongList._size|{}_size[0]
final fun <get-_size>(): kotlin/Int // androidx.collection/LongList._size.<get-_size>|<get-_size>(){}[0]
@@ -2029,25 +2032,23 @@
final fun <get-content>(): kotlin/LongArray // androidx.collection/LongList.content.<get-content>|<get-content>(){}[0]
final fun <set-content>(kotlin/LongArray) // androidx.collection/LongList.content.<set-content>|<set-content>(kotlin.LongArray){}[0]
- final fun any(): kotlin/Boolean // androidx.collection/LongList.any|any(){}[0]
+ final fun binarySearch(kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlin/Int // androidx.collection/LongList.binarySearch|binarySearch(kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun contains(kotlin/Long): kotlin/Boolean // androidx.collection/LongList.contains|contains(kotlin.Long){}[0]
final fun containsAll(androidx.collection/LongList): kotlin/Boolean // androidx.collection/LongList.containsAll|containsAll(androidx.collection.LongList){}[0]
- final fun count(): kotlin/Int // androidx.collection/LongList.count|count(){}[0]
final fun elementAt(kotlin/Int): kotlin/Long // androidx.collection/LongList.elementAt|elementAt(kotlin.Int){}[0]
final fun first(): kotlin/Long // androidx.collection/LongList.first|first(){}[0]
final fun get(kotlin/Int): kotlin/Long // androidx.collection/LongList.get|get(kotlin.Int){}[0]
final fun indexOf(kotlin/Long): kotlin/Int // androidx.collection/LongList.indexOf|indexOf(kotlin.Long){}[0]
- final fun isEmpty(): kotlin/Boolean // androidx.collection/LongList.isEmpty|isEmpty(){}[0]
- final fun isNotEmpty(): kotlin/Boolean // androidx.collection/LongList.isNotEmpty|isNotEmpty(){}[0]
final fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ...): kotlin/String // androidx.collection/LongList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence){}[0]
final fun last(): kotlin/Long // androidx.collection/LongList.last|last(){}[0]
final fun lastIndexOf(kotlin/Long): kotlin/Int // androidx.collection/LongList.lastIndexOf|lastIndexOf(kotlin.Long){}[0]
- final fun none(): kotlin/Boolean // androidx.collection/LongList.none|none(){}[0]
final inline fun <#A1: kotlin/Any?> fold(#A1, kotlin/Function2<#A1, kotlin/Long, #A1>): #A1 // androidx.collection/LongList.fold|fold(0:0;kotlin.Function2<0:0,kotlin.Long,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldIndexed(#A1, kotlin/Function3<kotlin/Int, #A1, kotlin/Long, #A1>): #A1 // androidx.collection/LongList.foldIndexed|foldIndexed(0:0;kotlin.Function3<kotlin.Int,0:0,kotlin.Long,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRight(#A1, kotlin/Function2<kotlin/Long, #A1, #A1>): #A1 // androidx.collection/LongList.foldRight|foldRight(0:0;kotlin.Function2<kotlin.Long,0:0,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A1: kotlin/Any?> foldRightIndexed(#A1, kotlin/Function3<kotlin/Int, kotlin/Long, #A1, #A1>): #A1 // androidx.collection/LongList.foldRightIndexed|foldRightIndexed(0:0;kotlin.Function3<kotlin.Int,kotlin.Long,0:0,0:0>){0§<kotlin.Any?>}[0]
+ final inline fun any(): kotlin/Boolean // androidx.collection/LongList.any|any(){}[0]
final inline fun any(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongList.any|any(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun count(): kotlin/Int // androidx.collection/LongList.count|count(){}[0]
final inline fun count(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/LongList.count|count(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
final inline fun elementAtOrElse(kotlin/Int, kotlin/Function1<kotlin/Int, kotlin/Long>): kotlin/Long // androidx.collection/LongList.elementAtOrElse|elementAtOrElse(kotlin.Int;kotlin.Function1<kotlin.Int,kotlin.Long>){}[0]
final inline fun first(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Long // androidx.collection/LongList.first|first(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
@@ -2057,8 +2058,11 @@
final inline fun forEachReversedIndexed(kotlin/Function2<kotlin/Int, kotlin/Long, kotlin/Unit>) // androidx.collection/LongList.forEachReversedIndexed|forEachReversedIndexed(kotlin.Function2<kotlin.Int,kotlin.Long,kotlin.Unit>){}[0]
final inline fun indexOfFirst(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/LongList.indexOfFirst|indexOfFirst(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
final inline fun indexOfLast(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Int // androidx.collection/LongList.indexOfLast|indexOfLast(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun isEmpty(): kotlin/Boolean // androidx.collection/LongList.isEmpty|isEmpty(){}[0]
+ final inline fun isNotEmpty(): kotlin/Boolean // androidx.collection/LongList.isNotEmpty|isNotEmpty(){}[0]
final inline fun joinToString(kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/CharSequence = ..., kotlin/Int = ..., kotlin/CharSequence = ..., crossinline kotlin/Function1<kotlin/Long, kotlin/CharSequence>): kotlin/String // androidx.collection/LongList.joinToString|joinToString(kotlin.CharSequence;kotlin.CharSequence;kotlin.CharSequence;kotlin.Int;kotlin.CharSequence;kotlin.Function1<kotlin.Long,kotlin.CharSequence>){}[0]
final inline fun last(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Long // androidx.collection/LongList.last|last(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
+ final inline fun none(): kotlin/Boolean // androidx.collection/LongList.none|none(){}[0]
final inline fun reversedAny(kotlin/Function1<kotlin/Long, kotlin/Boolean>): kotlin/Boolean // androidx.collection/LongList.reversedAny|reversedAny(kotlin.Function1<kotlin.Long,kotlin.Boolean>){}[0]
open fun equals(kotlin/Any?): kotlin/Boolean // androidx.collection/LongList.equals|equals(kotlin.Any?){}[0]
open fun hashCode(): kotlin/Int // androidx.collection/LongList.hashCode|hashCode(){}[0]
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index 7833819..ff66423 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -53,7 +53,7 @@
commonMain {
dependencies {
api(libs.kotlinStdlib)
- api(project(":annotation:annotation"))
+ api("androidx.annotation:annotation:1.9.0-alpha02")
}
}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
index 2aa9a4a..e1566fb 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
@@ -19,7 +19,6 @@
import androidx.collection.internal.EMPTY_INTS
import androidx.collection.internal.EMPTY_OBJECTS
import androidx.collection.internal.binarySearch
-import kotlin.jvm.JvmOverloads
/** Returns an empty new [ArraySet]. */
@Suppress("NOTHING_TO_INLINE") // Alias to public API.
@@ -58,7 +57,7 @@
* @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and will
* grow once items are added to it.
*/
-public expect class ArraySet<E> @JvmOverloads constructor(capacity: Int = 0) :
+public expect class ArraySet<E> constructor(capacity: Int = 0) :
MutableCollection<E>, MutableSet<E> {
internal var hashes: IntArray
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
index 14dab7f..5da46d1 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/DoubleList.kt
@@ -60,7 +60,7 @@
/** The number of elements in the [DoubleList]. */
@get:IntRange(from = 0)
- public val size: Int
+ public inline val size: Int
get() = _size
/**
@@ -75,12 +75,12 @@
get() = 0 until _size
/** Returns `true` if the collection has no elements in it. */
- public fun none(): Boolean {
+ public inline fun none(): Boolean {
return isEmpty()
}
/** Returns `true` if there's at least one element in the collection. */
- public fun any(): Boolean {
+ public inline fun any(): Boolean {
return isNotEmpty()
}
@@ -131,7 +131,7 @@
}
/** Returns the number of elements in this list. */
- public fun count(): Int = _size
+ public inline fun count(): Int = _size
/**
* Counts the number of elements matching [predicate].
@@ -290,7 +290,7 @@
*/
public operator fun get(@IntRange(from = 0) index: Int): Double {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -301,7 +301,7 @@
*/
public fun elementAt(@IntRange(from = 0) index: Int): Double {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -363,10 +363,10 @@
}
/** Returns `true` if the [DoubleList] has no elements in it or `false` otherwise. */
- public fun isEmpty(): Boolean = _size == 0
+ public inline fun isEmpty(): Boolean = _size == 0
/** Returns `true` if there are elements in the [DoubleList] or `false` if it is empty. */
- public fun isNotEmpty(): Boolean = _size != 0
+ public inline fun isNotEmpty(): Boolean = _size != 0
/**
* Returns the last element in the [DoubleList] or throws a [NoSuchElementException] if it
@@ -409,6 +409,43 @@
}
/**
+ * Searches this list the specified element in the range defined by [fromIndex] and [toIndex].
+ * The list is expected to be sorted into ascending order according to the natural ordering of
+ * its elements, otherwise the result is undefined.
+ *
+ * [fromIndex] must be >= 0 and < [toIndex], and [toIndex] must be <= [size], otherwise an an
+ * [IndexOutOfBoundsException] will be thrown.
+ *
+ * @return the index of the element if it is contained in the list within the specified range.
+ * otherwise, the inverted insertion point `(-insertionPoint - 1)`. The insertion point is
+ * defined as the index at which the element should be inserted, so that the list remains
+ * sorted.
+ */
+ @JvmOverloads
+ public fun binarySearch(element: Int, fromIndex: Int = 0, toIndex: Int = size): Int {
+ if (fromIndex < 0 || fromIndex >= toIndex || toIndex > _size) {
+ throwIndexOutOfBoundsException("")
+ }
+
+ var low = fromIndex
+ var high = toIndex - 1
+
+ while (low <= high) {
+ val mid = low + high ushr 1
+ val midVal = content[mid]
+ if (midVal < element) {
+ low = mid + 1
+ } else if (midVal > element) {
+ high = mid - 1
+ } else {
+ return mid // key found
+ }
+ }
+
+ return -(low + 1) // key not found.
+ }
+
+ /**
* Creates a String from the elements separated by [separator] and using [prefix] before and
* [postfix] after, if supplied.
*
@@ -539,7 +576,7 @@
*/
public fun add(@IntRange(from = 0) index: Int, element: Double) {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
ensureCapacity(_size + 1)
val content = content
@@ -564,7 +601,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: DoubleArray): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements.size)
@@ -591,7 +628,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: DoubleList): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements._size)
@@ -618,7 +655,7 @@
* Adds all [elements] to the end of the [MutableDoubleList] and returns `true` if the
* [MutableDoubleList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: DoubleList): Boolean {
+ public inline fun addAll(elements: DoubleList): Boolean {
return addAll(_size, elements)
}
@@ -626,17 +663,17 @@
* Adds all [elements] to the end of the [MutableDoubleList] and returns `true` if the
* [MutableDoubleList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: DoubleArray): Boolean {
+ public inline fun addAll(elements: DoubleArray): Boolean {
return addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableDoubleList]. */
- public operator fun plusAssign(elements: DoubleList) {
+ public inline operator fun plusAssign(elements: DoubleList) {
addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableDoubleList]. */
- public operator fun plusAssign(elements: DoubleArray) {
+ public inline operator fun plusAssign(elements: DoubleArray) {
addAll(_size, elements)
}
@@ -740,7 +777,7 @@
*/
public fun removeAt(@IntRange(from = 0) index: Int): Double {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val item = content[index]
@@ -764,10 +801,10 @@
*/
public fun removeRange(@IntRange(from = 0) start: Int, @IntRange(from = 0) end: Int) {
if (start !in 0.._size || end !in 0.._size) {
- throwIndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (end < start) {
- throwIllegalArgumentException("Start ($start) is more than end ($end)")
+ throwIllegalArgumentException("")
}
if (end != start) {
if (end < _size) {
@@ -824,7 +861,7 @@
*/
public operator fun set(@IntRange(from = 0) index: Int, element: Double): Double {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val old = content[index]
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
index 98cc53d..072e286 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0f
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
index fcfeca2..1ff0eee 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
index 55812f3..f8fa448 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
@@ -60,7 +60,7 @@
/** The number of elements in the [FloatList]. */
@get:IntRange(from = 0)
- public val size: Int
+ public inline val size: Int
get() = _size
/** Returns the last valid index in the [FloatList]. This can be `-1` when the list is empty. */
@@ -73,12 +73,12 @@
get() = 0 until _size
/** Returns `true` if the collection has no elements in it. */
- public fun none(): Boolean {
+ public inline fun none(): Boolean {
return isEmpty()
}
/** Returns `true` if there's at least one element in the collection. */
- public fun any(): Boolean {
+ public inline fun any(): Boolean {
return isNotEmpty()
}
@@ -129,7 +129,7 @@
}
/** Returns the number of elements in this list. */
- public fun count(): Int = _size
+ public inline fun count(): Int = _size
/**
* Counts the number of elements matching [predicate].
@@ -288,7 +288,7 @@
*/
public operator fun get(@IntRange(from = 0) index: Int): Float {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -299,7 +299,7 @@
*/
public fun elementAt(@IntRange(from = 0) index: Int): Float {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -361,10 +361,10 @@
}
/** Returns `true` if the [FloatList] has no elements in it or `false` otherwise. */
- public fun isEmpty(): Boolean = _size == 0
+ public inline fun isEmpty(): Boolean = _size == 0
/** Returns `true` if there are elements in the [FloatList] or `false` if it is empty. */
- public fun isNotEmpty(): Boolean = _size != 0
+ public inline fun isNotEmpty(): Boolean = _size != 0
/**
* Returns the last element in the [FloatList] or throws a [NoSuchElementException] if it
@@ -407,6 +407,43 @@
}
/**
+ * Searches this list the specified element in the range defined by [fromIndex] and [toIndex].
+ * The list is expected to be sorted into ascending order according to the natural ordering of
+ * its elements, otherwise the result is undefined.
+ *
+ * [fromIndex] must be >= 0 and < [toIndex], and [toIndex] must be <= [size], otherwise an an
+ * [IndexOutOfBoundsException] will be thrown.
+ *
+ * @return the index of the element if it is contained in the list within the specified range.
+ * otherwise, the inverted insertion point `(-insertionPoint - 1)`. The insertion point is
+ * defined as the index at which the element should be inserted, so that the list remains
+ * sorted.
+ */
+ @JvmOverloads
+ public fun binarySearch(element: Int, fromIndex: Int = 0, toIndex: Int = size): Int {
+ if (fromIndex < 0 || fromIndex >= toIndex || toIndex > _size) {
+ throwIndexOutOfBoundsException("")
+ }
+
+ var low = fromIndex
+ var high = toIndex - 1
+
+ while (low <= high) {
+ val mid = low + high ushr 1
+ val midVal = content[mid]
+ if (midVal < element) {
+ low = mid + 1
+ } else if (midVal > element) {
+ high = mid - 1
+ } else {
+ return mid // key found
+ }
+ }
+
+ return -(low + 1) // key not found.
+ }
+
+ /**
* Creates a String from the elements separated by [separator] and using [prefix] before and
* [postfix] after, if supplied.
*
@@ -536,7 +573,7 @@
*/
public fun add(@IntRange(from = 0) index: Int, element: Float) {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
ensureCapacity(_size + 1)
val content = content
@@ -561,7 +598,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: FloatArray): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements.size)
@@ -588,7 +625,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: FloatList): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements._size)
@@ -615,7 +652,7 @@
* Adds all [elements] to the end of the [MutableFloatList] and returns `true` if the
* [MutableFloatList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: FloatList): Boolean {
+ public inline fun addAll(elements: FloatList): Boolean {
return addAll(_size, elements)
}
@@ -623,17 +660,17 @@
* Adds all [elements] to the end of the [MutableFloatList] and returns `true` if the
* [MutableFloatList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: FloatArray): Boolean {
+ public inline fun addAll(elements: FloatArray): Boolean {
return addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableFloatList]. */
- public operator fun plusAssign(elements: FloatList) {
+ public inline operator fun plusAssign(elements: FloatList) {
addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableFloatList]. */
- public operator fun plusAssign(elements: FloatArray) {
+ public inline operator fun plusAssign(elements: FloatArray) {
addAll(_size, elements)
}
@@ -737,7 +774,7 @@
*/
public fun removeAt(@IntRange(from = 0) index: Int): Float {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val item = content[index]
@@ -761,10 +798,10 @@
*/
public fun removeRange(@IntRange(from = 0) start: Int, @IntRange(from = 0) end: Int) {
if (start !in 0.._size || end !in 0.._size) {
- throwIndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (end < start) {
- throwIllegalArgumentException("Start ($start) is more than end ($end)")
+ throwIllegalArgumentException("")
}
if (end != start) {
if (end < _size) {
@@ -821,7 +858,7 @@
*/
public operator fun set(@IntRange(from = 0) index: Int, element: Float): Float {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val old = content[index]
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
index 9560c1d..a60cdc7 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0L
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
index f6b324f..bdb784d 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
@@ -868,7 +868,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -876,7 +875,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -923,25 +921,19 @@
values[targetIndex] = values[index]
values[index] = null
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
index ee0343c..0279ff4 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -739,7 +739,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -747,7 +746,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -791,21 +789,15 @@
elements[targetIndex] = elements[index]
elements[index] = 0f
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- elements[swapIndex] = elements[targetIndex]
+ val oldElement = elements[targetIndex]
elements[targetIndex] = elements[index]
- elements[index] = elements[swapIndex]
+ elements[index] = oldElement
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
index 0b57079..10225ca 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0f
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
index c155d79..9f98b11 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
index 6a05353..37f2510 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
@@ -60,7 +60,7 @@
/** The number of elements in the [IntList]. */
@get:IntRange(from = 0)
- public val size: Int
+ public inline val size: Int
get() = _size
/** Returns the last valid index in the [IntList]. This can be `-1` when the list is empty. */
@@ -73,12 +73,12 @@
get() = 0 until _size
/** Returns `true` if the collection has no elements in it. */
- public fun none(): Boolean {
+ public inline fun none(): Boolean {
return isEmpty()
}
/** Returns `true` if there's at least one element in the collection. */
- public fun any(): Boolean {
+ public inline fun any(): Boolean {
return isNotEmpty()
}
@@ -129,7 +129,7 @@
}
/** Returns the number of elements in this list. */
- public fun count(): Int = _size
+ public inline fun count(): Int = _size
/**
* Counts the number of elements matching [predicate].
@@ -288,7 +288,7 @@
*/
public operator fun get(@IntRange(from = 0) index: Int): Int {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -299,7 +299,7 @@
*/
public fun elementAt(@IntRange(from = 0) index: Int): Int {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -359,10 +359,10 @@
}
/** Returns `true` if the [IntList] has no elements in it or `false` otherwise. */
- public fun isEmpty(): Boolean = _size == 0
+ public inline fun isEmpty(): Boolean = _size == 0
/** Returns `true` if there are elements in the [IntList] or `false` if it is empty. */
- public fun isNotEmpty(): Boolean = _size != 0
+ public inline fun isNotEmpty(): Boolean = _size != 0
/**
* Returns the last element in the [IntList] or throws a [NoSuchElementException] if it
@@ -405,6 +405,43 @@
}
/**
+ * Searches this list the specified element in the range defined by [fromIndex] and [toIndex].
+ * The list is expected to be sorted into ascending order according to the natural ordering of
+ * its elements, otherwise the result is undefined.
+ *
+ * [fromIndex] must be >= 0 and < [toIndex], and [toIndex] must be <= [size], otherwise an an
+ * [IndexOutOfBoundsException] will be thrown.
+ *
+ * @return the index of the element if it is contained in the list within the specified range.
+ * otherwise, the inverted insertion point `(-insertionPoint - 1)`. The insertion point is
+ * defined as the index at which the element should be inserted, so that the list remains
+ * sorted.
+ */
+ @JvmOverloads
+ public fun binarySearch(element: Int, fromIndex: Int = 0, toIndex: Int = size): Int {
+ if (fromIndex < 0 || fromIndex >= toIndex || toIndex > _size) {
+ throwIndexOutOfBoundsException("")
+ }
+
+ var low = fromIndex
+ var high = toIndex - 1
+
+ while (low <= high) {
+ val mid = low + high ushr 1
+ val midVal = content[mid]
+ if (midVal < element) {
+ low = mid + 1
+ } else if (midVal > element) {
+ high = mid - 1
+ } else {
+ return mid // key found
+ }
+ }
+
+ return -(low + 1) // key not found.
+ }
+
+ /**
* Creates a String from the elements separated by [separator] and using [prefix] before and
* [postfix] after, if supplied.
*
@@ -533,7 +570,7 @@
*/
public fun add(@IntRange(from = 0) index: Int, element: Int) {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
ensureCapacity(_size + 1)
val content = content
@@ -558,7 +595,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: IntArray): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements.size)
@@ -585,7 +622,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: IntList): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements._size)
@@ -612,7 +649,7 @@
* Adds all [elements] to the end of the [MutableIntList] and returns `true` if the
* [MutableIntList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: IntList): Boolean {
+ public inline fun addAll(elements: IntList): Boolean {
return addAll(_size, elements)
}
@@ -620,17 +657,17 @@
* Adds all [elements] to the end of the [MutableIntList] and returns `true` if the
* [MutableIntList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: IntArray): Boolean {
+ public inline fun addAll(elements: IntArray): Boolean {
return addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableIntList]. */
- public operator fun plusAssign(elements: IntList) {
+ public inline operator fun plusAssign(elements: IntList) {
addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableIntList]. */
- public operator fun plusAssign(elements: IntArray) {
+ public inline operator fun plusAssign(elements: IntArray) {
addAll(_size, elements)
}
@@ -731,7 +768,7 @@
*/
public fun removeAt(@IntRange(from = 0) index: Int): Int {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val item = content[index]
@@ -755,10 +792,10 @@
*/
public fun removeRange(@IntRange(from = 0) start: Int, @IntRange(from = 0) end: Int) {
if (start !in 0.._size || end !in 0.._size) {
- throwIndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (end < start) {
- throwIllegalArgumentException("Start ($start) is more than end ($end)")
+ throwIllegalArgumentException("")
}
if (end != start) {
if (end < _size) {
@@ -815,7 +852,7 @@
*/
public operator fun set(@IntRange(from = 0) index: Int, element: Int): Int {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val old = content[index]
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
index 60fb86d..dedc3ae 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0L
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
index 30e2aa5..ceab287 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
@@ -868,7 +868,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -876,7 +875,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -923,25 +921,19 @@
values[targetIndex] = values[index]
values[index] = null
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
index a226286..036f203 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -737,7 +737,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -745,7 +744,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -789,21 +787,15 @@
elements[targetIndex] = elements[index]
elements[index] = 0
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- elements[swapIndex] = elements[targetIndex]
+ val oldElement = elements[targetIndex]
elements[targetIndex] = elements[index]
- elements[index] = elements[swapIndex]
+ elements[index] = oldElement
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
index 7cfd1b0..bb34619 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0f
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
index ad8c837..a9dc213 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
index b80e23b..522220b 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
@@ -60,7 +60,7 @@
/** The number of elements in the [LongList]. */
@get:IntRange(from = 0)
- public val size: Int
+ public inline val size: Int
get() = _size
/** Returns the last valid index in the [LongList]. This can be `-1` when the list is empty. */
@@ -73,12 +73,12 @@
get() = 0 until _size
/** Returns `true` if the collection has no elements in it. */
- public fun none(): Boolean {
+ public inline fun none(): Boolean {
return isEmpty()
}
/** Returns `true` if there's at least one element in the collection. */
- public fun any(): Boolean {
+ public inline fun any(): Boolean {
return isNotEmpty()
}
@@ -129,7 +129,7 @@
}
/** Returns the number of elements in this list. */
- public fun count(): Int = _size
+ public inline fun count(): Int = _size
/**
* Counts the number of elements matching [predicate].
@@ -288,7 +288,7 @@
*/
public operator fun get(@IntRange(from = 0) index: Int): Long {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -299,7 +299,7 @@
*/
public fun elementAt(@IntRange(from = 0) index: Int): Long {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -360,10 +360,10 @@
}
/** Returns `true` if the [LongList] has no elements in it or `false` otherwise. */
- public fun isEmpty(): Boolean = _size == 0
+ public inline fun isEmpty(): Boolean = _size == 0
/** Returns `true` if there are elements in the [LongList] or `false` if it is empty. */
- public fun isNotEmpty(): Boolean = _size != 0
+ public inline fun isNotEmpty(): Boolean = _size != 0
/**
* Returns the last element in the [LongList] or throws a [NoSuchElementException] if it
@@ -406,6 +406,43 @@
}
/**
+ * Searches this list the specified element in the range defined by [fromIndex] and [toIndex].
+ * The list is expected to be sorted into ascending order according to the natural ordering of
+ * its elements, otherwise the result is undefined.
+ *
+ * [fromIndex] must be >= 0 and < [toIndex], and [toIndex] must be <= [size], otherwise an an
+ * [IndexOutOfBoundsException] will be thrown.
+ *
+ * @return the index of the element if it is contained in the list within the specified range.
+ * otherwise, the inverted insertion point `(-insertionPoint - 1)`. The insertion point is
+ * defined as the index at which the element should be inserted, so that the list remains
+ * sorted.
+ */
+ @JvmOverloads
+ public fun binarySearch(element: Int, fromIndex: Int = 0, toIndex: Int = size): Int {
+ if (fromIndex < 0 || fromIndex >= toIndex || toIndex > _size) {
+ throwIndexOutOfBoundsException("")
+ }
+
+ var low = fromIndex
+ var high = toIndex - 1
+
+ while (low <= high) {
+ val mid = low + high ushr 1
+ val midVal = content[mid]
+ if (midVal < element) {
+ low = mid + 1
+ } else if (midVal > element) {
+ high = mid - 1
+ } else {
+ return mid // key found
+ }
+ }
+
+ return -(low + 1) // key not found.
+ }
+
+ /**
* Creates a String from the elements separated by [separator] and using [prefix] before and
* [postfix] after, if supplied.
*
@@ -534,7 +571,7 @@
*/
public fun add(@IntRange(from = 0) index: Int, element: Long) {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
ensureCapacity(_size + 1)
val content = content
@@ -559,7 +596,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: LongArray): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements.size)
@@ -586,7 +623,7 @@
*/
public fun addAll(@IntRange(from = 0) index: Int, elements: LongList): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements._size)
@@ -613,7 +650,7 @@
* Adds all [elements] to the end of the [MutableLongList] and returns `true` if the
* [MutableLongList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: LongList): Boolean {
+ public inline fun addAll(elements: LongList): Boolean {
return addAll(_size, elements)
}
@@ -621,17 +658,17 @@
* Adds all [elements] to the end of the [MutableLongList] and returns `true` if the
* [MutableLongList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: LongArray): Boolean {
+ public inline fun addAll(elements: LongArray): Boolean {
return addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableLongList]. */
- public operator fun plusAssign(elements: LongList) {
+ public inline operator fun plusAssign(elements: LongList) {
addAll(_size, elements)
}
/** Adds all [elements] to the end of the [MutableLongList]. */
- public operator fun plusAssign(elements: LongArray) {
+ public inline operator fun plusAssign(elements: LongArray) {
addAll(_size, elements)
}
@@ -733,7 +770,7 @@
*/
public fun removeAt(@IntRange(from = 0) index: Int): Long {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val item = content[index]
@@ -757,10 +794,10 @@
*/
public fun removeRange(@IntRange(from = 0) start: Int, @IntRange(from = 0) end: Int) {
if (start !in 0.._size || end !in 0.._size) {
- throwIndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (end < start) {
- throwIllegalArgumentException("Start ($start) is more than end ($end)")
+ throwIllegalArgumentException("")
}
if (end != start) {
if (end < _size) {
@@ -817,7 +854,7 @@
*/
public operator fun set(@IntRange(from = 0) index: Int, element: Long): Long {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val old = content[index]
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
index 87171f3..dbe6771 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
@@ -886,7 +886,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -894,7 +893,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -941,25 +939,19 @@
values[targetIndex] = values[index]
values[index] = 0L
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
index 93b5b7c..eb674d9 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
@@ -868,7 +868,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -876,7 +875,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -923,25 +921,19 @@
values[targetIndex] = values[index]
values[index] = null
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
index b633354..675d18e 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -738,7 +738,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -746,7 +745,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -790,21 +788,15 @@
elements[targetIndex] = elements[index]
elements[index] = 0L
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- elements[swapIndex] = elements[targetIndex]
+ val oldElement = elements[targetIndex]
elements[targetIndex] = elements[index]
- elements[index] = elements[swapIndex]
+ elements[index] = oldElement
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
index 732c229..d776f9e 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
@@ -20,9 +20,6 @@
import androidx.collection.internal.idealLongArraySize
import androidx.collection.internal.requirePrecondition
import kotlin.DeprecationLevel.HIDDEN
-import kotlin.jvm.JvmField
-import kotlin.jvm.JvmOverloads
-import kotlin.jvm.JvmSynthetic
private val DELETED = Any()
@@ -54,23 +51,10 @@
* initial capacity of 0, the sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
-public expect open class LongSparseArray<E>
-@JvmOverloads
-public constructor(initialCapacity: Int = 10) {
- @JvmSynthetic // Hide from Java callers.
- @JvmField
+public expect open class LongSparseArray<E> public constructor(initialCapacity: Int = 10) {
internal var garbage: Boolean
-
- @JvmSynthetic // Hide from Java callers.
- @JvmField
internal var keys: LongArray
-
- @JvmSynthetic // Hide from Java callers.
- @JvmField
internal var values: Array<Any?>
-
- @JvmSynthetic // Hide from Java callers.
- @JvmField
internal var size: Int
/**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
index 8d32216..6c8b64f 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
@@ -901,7 +901,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -909,7 +908,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -956,25 +954,19 @@
values[targetIndex] = values[index]
values[index] = 0f
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
index f3c0026..ec7ec4b 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
@@ -901,7 +901,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -909,7 +908,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -956,25 +954,19 @@
values[targetIndex] = values[index]
values[index] = 0
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
index be66478..3622689 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
@@ -901,7 +901,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -909,7 +908,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -956,25 +954,19 @@
values[targetIndex] = values[index]
values[index] = 0L
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
index 1eff85a..5145f88 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterMap.kt
@@ -1018,7 +1018,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -1026,7 +1025,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -1072,25 +1070,19 @@
values[targetIndex] = values[index]
values[index] = null
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
index fbc83a2..a6903f8 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
@@ -1041,7 +1041,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -1049,7 +1048,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -1093,21 +1091,15 @@
elements[targetIndex] = elements[index]
elements[index] = null
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- elements[swapIndex] = elements[targetIndex]
+ val oldElement = elements[targetIndex]
elements[targetIndex] = elements[index]
- elements[index] = elements[swapIndex]
+ elements[index] = oldElement
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/SieveCache.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/SieveCache.kt
index 79e0f05..51507f1 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/SieveCache.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/SieveCache.kt
@@ -37,6 +37,9 @@
internal const val EmptyNode = 0x3fffffff_ffffffffL
internal val EmptyNodes = LongArray(0)
+private const val InvalidMappingLink: Int = -1
+private const val InvalidMapping: Long = -1L
+
/**
* [SieveCache] is an in-memory cache that holds strong references to a limited number of values
* determined by the cache's [maxSize] and the size of each value. When a value is added to a full
@@ -787,12 +790,54 @@
val values = values
val nodes = nodes
- val indexMapping = IntArray(capacity)
+ // In this function, we are swapping values in place in the keys/values/nodes arrays.
+ // This requires us to track where the values came from original in the array and
+ // where they moved. You can think of this as an allocation-free double-linked list.
+ //
+ // We need this mapping to fix the links encoded in the nodes array. The nodes array
+ // is itself an allocation-free double-linked list which uses indices to indicate where
+ // to find the next/previous node. Since this method will move the values inside the
+ // data structure, we need to patch the nodes array when we're done. We could skip the
+ // mapping array but that would require scanning the entire nodes array every time we move
+ // a value inside the data structure which would be more expensive. Instead we traverse
+ // the nodes array only once in [fixup].
+ //
+ // Each index mapping is a (src, dst) pair. The source index indicates which
+ // index the current value came, and the destination index indicates where the
+ // value previously held was moved. For instance we want to swap the values
+ // at index 4 and 21:
+ //
+ // indexMapping[4] = (21, 21)
+ // The value at index 4 came from index 21 (src) and the value previously at index 4
+ // is now at index 21.
+ //
+ // indexMapping[21] = (4, 4)
+ // The value at index 21 came from index 4 (src) and the value previously at index 21
+ // is now at index 4.
+ //
+ // Now let's imagine we want to swap the values at index 4 and 22 (following the previous
+ // swap):
+ //
+ // indexMapping[4] = (22, 21)
+ // The value at index 4 came from index 22 (src) and the value previously at index 4
+ // is now at index 21.
+ //
+ // indexMapping[21] = (4, 22)
+ // The value at index 21 came from index 4 (src) and the value previously at index 21
+ // is now at index 22.
+ //
+ // indexMapping[22] = (21, 4)
+ // The value at index 22 came from index 21 (src) and the value previously at index 22
+ // is now at index 4.
+ //
+ // If a src or dst mapping is set to -1 ([InvalidMappingLink]), the mapping does not
+ // exist. We initialize the array to (-1, -1).
+ val indexMapping = LongArray(capacity)
+ indexMapping.fill(InvalidMapping, 0, capacity)
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -800,7 +845,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -816,7 +860,7 @@
val hash1 = h1(hash)
val targetIndex = findFirstAvailableSlot(hash1)
- // Test if the current index (i) and the new index (targetIndex) fall
+ // Test if the current index (index) and the new index (targetIndex) fall
// within the same group based on the hash. If the group doesn't change,
// we don't move the entry
val probeOffset = hash1 and capacity
@@ -827,7 +871,7 @@
val hash2 = h2(hash)
writeRawMetadata(metadata, index, hash2.toLong())
- indexMapping[index] = index
+ indexMapping[index] = createMapping(index, index)
// Copies the metadata into the clone area
metadata[metadata.size - 1] = metadata[0]
@@ -852,33 +896,43 @@
nodes[targetIndex] = nodes[index]
nodes[index] = EmptyNode
- indexMapping[index] = targetIndex
-
- swapIndex = index
+ val mapping = indexMapping[index]
+ val src = mapping.src
+ if (src != -1) {
+ indexMapping[src] = createDstMapping(indexMapping[src], targetIndex)
+ indexMapping[index] = eraseSrcMapping(indexMapping[index])
+ } else {
+ indexMapping[index] = createMapping(InvalidMappingLink, targetIndex)
+ }
+ indexMapping[targetIndex] = createMapping(index, InvalidMappingLink)
} else /* m == Deleted */ {
- // The target isn't empty so we use an empty slot denoted by
- // swapIndex to perform the swap
+ // The target isn't empty
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
+ val oldKey = keys[targetIndex]
+ keys[targetIndex] = keys[index]
+ keys[index] = oldKey
+
+ val oldValue = values[targetIndex]
+ values[targetIndex] = values[index]
+ values[index] = oldValue
+
+ val oldNode = nodes[targetIndex]
+ nodes[targetIndex] = nodes[index]
+ nodes[index] = oldNode
+
+ val mapping = indexMapping[index]
+ var src = mapping.src
+ if (src != -1) {
+ indexMapping[src] = createDstMapping(indexMapping[src], targetIndex)
+ indexMapping[index] = createSrcMapping(indexMapping[index], targetIndex)
+ } else {
+ indexMapping[index] = createMapping(targetIndex, targetIndex)
+ src = index
}
- keys[swapIndex] = keys[targetIndex]
- keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
-
- values[swapIndex] = values[targetIndex]
- values[targetIndex] = values[index]
- values[index] = values[swapIndex]
-
- nodes[swapIndex] = nodes[targetIndex]
- nodes[targetIndex] = nodes[index]
- nodes[index] = nodes[swapIndex]
-
- indexMapping[index] = targetIndex
- indexMapping[targetIndex] = index
+ indexMapping[targetIndex] = createMapping(src, index)
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
@@ -904,6 +958,9 @@
val previousNodes = nodes
val previousCapacity = _capacity
+ // src index -> dst index mapping
+ // We only need the mapping to go one way since we are copying from
+ // the existing array into a new one
val indexMapping = IntArray(previousCapacity)
initializeStorage(newCapacity)
@@ -932,6 +989,19 @@
fixupNodes(indexMapping)
}
+ private fun fixupNodes(mapping: LongArray) {
+ val nodes = nodes
+ for (i in nodes.indices) {
+ val node = nodes[i]
+ val previous = node.previousNode
+ val next = node.nextNode
+ nodes[i] = createLinks(node, previous, next, mapping)
+ }
+ if (head != NodeInvalidLink) head = mapping[head].dst
+ if (tail != NodeInvalidLink) tail = mapping[tail].dst
+ if (hand != NodeInvalidLink) hand = mapping[hand].dst
+ }
+
private fun fixupNodes(mapping: IntArray) {
val nodes = nodes
for (i in nodes.indices) {
@@ -1017,6 +1087,13 @@
}
}
+internal inline fun createLinks(node: Long, previous: Int, next: Int, mapping: LongArray): Long {
+ return (node and NodeMetaMask) or
+ (if (previous == NodeInvalidLink) NodeInvalidLink else mapping[previous].dst).toLong() shl
+ 31 or
+ (if (next == NodeInvalidLink) NodeInvalidLink else mapping[next].dst).toLong()
+}
+
internal inline fun createLinks(node: Long, previous: Int, next: Int, mapping: IntArray): Long {
return (node and NodeMetaMask) or
(if (previous == NodeInvalidLink) NodeInvalidLink else mapping[previous]).toLong() shl
@@ -1046,3 +1123,20 @@
internal inline val Long.visited: Int
get() = ((this shr 62) and 0x1).toInt()
+
+private inline fun createMapping(src: Int, dst: Int) = (src.toLong() shl 32) or dst.toLong()
+
+private inline fun createSrcMapping(mapping: Long, src: Int) =
+ (src.toLong() shl 32) or (mapping and 0xffff_ffffL)
+
+private inline fun createDstMapping(mapping: Long, dst: Int) =
+ (mapping and 0xffff_ffff_0000_0000UL.toLong()) or dst.toLong()
+
+private inline fun eraseSrcMapping(mapping: Long) =
+ 0xffff_ffff_0000_0000UL.toLong() or (mapping and 0xffff_ffffL)
+
+private inline val Long.src: Int
+ get() = ((this shr 32) and 0xffff_ffffL).toInt()
+
+private inline val Long.dst: Int
+ get() = (this and 0xffff_ffffL).toInt()
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/SparseArrayCompat.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/SparseArrayCompat.kt
index fb5a08e..4e0d2793 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/SparseArrayCompat.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/SparseArrayCompat.kt
@@ -18,9 +18,6 @@
import androidx.collection.internal.binarySearch
import androidx.collection.internal.idealIntArraySize
-import kotlin.jvm.JvmField
-import kotlin.jvm.JvmOverloads
-import kotlin.jvm.JvmSynthetic
import kotlin.math.min
private val DELETED = Any()
@@ -57,23 +54,10 @@
* initial capacity of 0, the sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
-public expect open class SparseArrayCompat<E>
-@JvmOverloads
-public constructor(initialCapacity: Int = 10) {
- @JvmSynthetic // Hide from Java callers.
- @JvmField
+public expect open class SparseArrayCompat<E> public constructor(initialCapacity: Int = 10) {
internal var garbage: Boolean
-
- @JvmSynthetic // Hide from Java callers.
- @JvmField
internal var keys: IntArray
-
- @JvmSynthetic // Hide from Java callers.
- @JvmField
internal var values: Array<Any?>
-
- @JvmSynthetic // Hide from Java callers.
- @JvmField
internal var size: Int
/**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt
index 61e384a..16ba576 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt
@@ -21,6 +21,5 @@
internal inline fun <T> Lock.synchronized(block: () -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
-
return synchronizedImpl(block)
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
index 4bc3b68..4c324c7 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/DoubleListTest.kt
@@ -713,4 +713,16 @@
assertEquals(-1.0, l[2])
assertEquals(10.0, l[3])
}
+
+ @Test
+ fun binarySearchDoubleList() {
+ val l = mutableDoubleListOf(-2.0, -1.0, 2.0, 10.0, 10.0)
+ assertEquals(0, l.binarySearch(-2))
+ assertEquals(2, l.binarySearch(2))
+ assertEquals(3, l.binarySearch(10))
+
+ assertEquals(-1, l.binarySearch(-20))
+ assertEquals(-4, l.binarySearch(3))
+ assertEquals(-6, l.binarySearch(20))
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
index cdc37e9..e1b4649c 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
@@ -713,4 +713,16 @@
assertEquals(-1f, l[2])
assertEquals(10f, l[3])
}
+
+ @Test
+ fun binarySearchFloatList() {
+ val l = mutableFloatListOf(-2f, -1f, 2f, 10f, 10f)
+ assertEquals(0, l.binarySearch(-2))
+ assertEquals(2, l.binarySearch(2))
+ assertEquals(3, l.binarySearch(10))
+
+ assertEquals(-1, l.binarySearch(-20))
+ assertEquals(-4, l.binarySearch(3))
+ assertEquals(-6, l.binarySearch(20))
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
index ae3bc8f4..cdc0cf9 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
@@ -713,4 +713,16 @@
assertEquals(-1, l[2])
assertEquals(10, l[3])
}
+
+ @Test
+ fun binarySearchIntList() {
+ val l = mutableIntListOf(-2, -1, 2, 10, 10)
+ assertEquals(0, l.binarySearch(-2))
+ assertEquals(2, l.binarySearch(2))
+ assertEquals(3, l.binarySearch(10))
+
+ assertEquals(-1, l.binarySearch(-20))
+ assertEquals(-4, l.binarySearch(3))
+ assertEquals(-6, l.binarySearch(20))
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
index 9fdd870..af0536e 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
@@ -713,4 +713,16 @@
assertEquals(-1L, l[2])
assertEquals(10L, l[3])
}
+
+ @Test
+ fun binarySearchLongList() {
+ val l = mutableLongListOf(-2L, -1L, 2L, 10L, 10L)
+ assertEquals(0, l.binarySearch(-2))
+ assertEquals(2, l.binarySearch(2))
+ assertEquals(3, l.binarySearch(10))
+
+ assertEquals(-1, l.binarySearch(-20))
+ assertEquals(-4, l.binarySearch(3))
+ assertEquals(-6, l.binarySearch(20))
+ }
}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/SieveCacheTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/SieveCacheTest.kt
index bbe1487..0b2ff31 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/SieveCacheTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/SieveCacheTest.kt
@@ -16,6 +16,8 @@
package androidx.collection
+import kotlin.math.max
+import kotlin.math.min
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
@@ -869,6 +871,39 @@
assertEquals("b", cache["2"])
}
+ @Test
+ fun hashCollisions() {
+ val cache = SieveCache<BadHashKey, Int>(24, 24)
+
+ for (i in 0..128) {
+ val key = BadHashKey(i.toString())
+ cache.put(key, i)
+ assertEquals(min(i + 1, 24), cache.size)
+ for (j in i downTo max(0, i - 24)) {
+ assertTrue(cache.contains(BadHashKey(i.toString())))
+ }
+ }
+ }
+
+ private class BadHashKey(val name: String) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as BadHashKey
+
+ return name == other.name
+ }
+
+ override fun hashCode(): Int {
+ return name.length
+ }
+
+ override fun toString(): String {
+ return "BadHashKey(name='$name')"
+ }
+ }
+
private fun createCreatingCache(): SieveCache<String, String> {
return SieveCache(4, createValueFromKey = { key -> "created-$key" })
}
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/Lock.jvm.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/Lock.jvm.kt
index 1be5d51..82591d2 100644
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/Lock.jvm.kt
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/Lock.jvm.kt
@@ -22,6 +22,7 @@
internal actual class Lock {
- actual inline fun <T> synchronizedImpl(block: () -> T): T =
- synchronized(lock = this, block = block)
+ actual inline fun <T> synchronizedImpl(block: () -> T): T {
+ return synchronized(lock = this, block = block)
+ }
}
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/LruHashMap.jvm.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/LruHashMap.jvm.kt
index c2fe91c..5e526fb 100644
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/LruHashMap.jvm.kt
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/internal/LruHashMap.jvm.kt
@@ -20,7 +20,6 @@
import androidx.annotation.RestrictTo
-@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
internal actual class LruHashMap<K : Any, V : Any>
actual constructor(
initialCapacity: Int,
diff --git a/collection/collection/src/mingwMain/kotlin/androidx/collection/internal/Lock.mingw.kt b/collection/collection/src/mingwMain/kotlin/androidx/collection/internal/LockImpl.mingw.kt
similarity index 100%
rename from collection/collection/src/mingwMain/kotlin/androidx/collection/internal/Lock.mingw.kt
rename to collection/collection/src/mingwMain/kotlin/androidx/collection/internal/LockImpl.mingw.kt
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
index 3adc4c5..607d6ba 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
@@ -43,7 +43,7 @@
* grow once items are added to it.
*/
// JvmOverloads is required on constructor to match expect declaration
-public actual class ArraySet<E> @kotlin.jvm.JvmOverloads actual constructor(capacity: Int) :
+public actual class ArraySet<E> actual constructor(capacity: Int) :
MutableCollection<E>, MutableSet<E> {
internal actual var hashes: IntArray = EMPTY_INTS
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.native.kt
index 2035fec..d991a24 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.native.kt
@@ -18,6 +18,7 @@
/** Native actual of internal utils for handling target differences in collection code. */
internal actual object CollectionPlatformUtils {
+ @Suppress("NOTHING_TO_INLINE")
internal actual inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException {
return IndexOutOfBoundsException()
}
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt
index e66b4fe..d118e16 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/LongSparseArray.native.kt
@@ -48,10 +48,7 @@
* initial capacity of 0, the sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
-public actual open class LongSparseArray<E>
-// JvmOverloads is required on constructor to match expect declaration
[email protected]
-public actual constructor(initialCapacity: Int) {
+public actual open class LongSparseArray<E> public actual constructor(initialCapacity: Int) {
internal actual var garbage = false
internal actual var keys: LongArray
internal actual var values: Array<Any?>
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/SparseArrayCompat.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/SparseArrayCompat.native.kt
index 39b56d6..974ebd4 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/SparseArrayCompat.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/SparseArrayCompat.native.kt
@@ -52,10 +52,7 @@
* initial capacity of 0, the sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
-public actual open class SparseArrayCompat<E>
-// JvmOverloads is required on constructor to match expect declaration
[email protected]
-public actual constructor(initialCapacity: Int) {
+public actual open class SparseArrayCompat<E> public actual constructor(initialCapacity: Int) {
internal actual var garbage = false
internal actual var keys: IntArray
internal actual var values: Array<Any?>
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt
index fcb8eb0..964b216 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt
@@ -16,6 +16,8 @@
package androidx.collection.internal
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.native.internal.createCleaner
/**
@@ -32,16 +34,17 @@
internal fun destroy()
}
-@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
internal actual class Lock actual constructor() {
private val lockImpl = LockImpl()
@Suppress("unused") // The returned Cleaner must be assigned to a property
- @ExperimentalStdlibApi
+ @OptIn(ExperimentalStdlibApi::class)
private val cleaner = createCleaner(lockImpl, LockImpl::destroy)
actual inline fun <T> synchronizedImpl(block: () -> T): T {
+ contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+
lock()
return try {
block()
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/LruHashMap.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/LruHashMap.native.kt
index e7715d6..27f4305 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/LruHashMap.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/LruHashMap.native.kt
@@ -16,7 +16,6 @@
package androidx.collection.internal
-@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
internal actual class LruHashMap<K : Any, V : Any>
actual constructor(
initialCapacity: Int,
diff --git a/collection/collection/src/unixMain/kotlin/androidx/collection/internal/Lock.unix.kt b/collection/collection/src/unixMain/kotlin/androidx/collection/internal/LockImpl.unix.kt
similarity index 100%
rename from collection/collection/src/unixMain/kotlin/androidx/collection/internal/Lock.unix.kt
rename to collection/collection/src/unixMain/kotlin/androidx/collection/internal/LockImpl.unix.kt
diff --git a/collection/collection/template/ObjectPValueMap.kt.template b/collection/collection/template/ObjectPValueMap.kt.template
index e0fef3a..bfc791c 100644
--- a/collection/collection/template/ObjectPValueMap.kt.template
+++ b/collection/collection/template/ObjectPValueMap.kt.template
@@ -1024,7 +1024,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -1032,7 +1031,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -1079,25 +1077,19 @@
values[targetIndex] = values[index]
values[index] = 0ValueSuffix
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/template/PKeyList.kt.template b/collection/collection/template/PKeyList.kt.template
index 3afb59d..929c44c 100644
--- a/collection/collection/template/PKeyList.kt.template
+++ b/collection/collection/template/PKeyList.kt.template
@@ -65,7 +65,7 @@
* The number of elements in the [PKeyList].
*/
@get:IntRange(from = 0)
- public val size: Int
+ public inline val size: Int
get() = _size
/**
@@ -82,14 +82,14 @@
/**
* Returns `true` if the collection has no elements in it.
*/
- public fun none(): Boolean {
+ public inline fun none(): Boolean {
return isEmpty()
}
/**
* Returns `true` if there's at least one element in the collection.
*/
- public fun any(): Boolean {
+ public inline fun any(): Boolean {
return isNotEmpty()
}
@@ -146,7 +146,7 @@
/**
* Returns the number of elements in this list.
*/
- public fun count(): Int = _size
+ public inline fun count(): Int = _size
/**
* Counts the number of elements matching [predicate].
@@ -308,7 +308,7 @@
*/
public operator fun get(@IntRange(from = 0) index: Int): PKey {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -319,7 +319,7 @@
*/
public fun elementAt(@IntRange(from = 0) index: Int): PKey {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
return content[index]
}
@@ -384,12 +384,12 @@
/**
* Returns `true` if the [PKeyList] has no elements in it or `false` otherwise.
*/
- public fun isEmpty(): Boolean = _size == 0
+ public inline fun isEmpty(): Boolean = _size == 0
/**
* Returns `true` if there are elements in the [PKeyList] or `false` if it is empty.
*/
- public fun isNotEmpty(): Boolean = _size != 0
+ public inline fun isNotEmpty(): Boolean = _size != 0
/**
* Returns the last element in the [PKeyList] or throws a [NoSuchElementException] if
@@ -431,6 +431,43 @@
}
/**
+ * Searches this list the specified element in the range defined by [fromIndex] and [toIndex].
+ * The list is expected to be sorted into ascending order according to the natural ordering of
+ * its elements, otherwise the result is undefined.
+ *
+ * [fromIndex] must be >= 0 and < [toIndex], and [toIndex] must be <= [size], otherwise an
+ * an [IndexOutOfBoundsException] will be thrown.
+ *
+ * @return the index of the element if it is contained in the list within the specified range.
+ * otherwise, the inverted insertion point `(-insertionPoint - 1)`. The insertion point is
+ * defined as the index at which the element should be inserted, so that the list remains
+ * sorted.
+ */
+ @JvmOverloads
+ public fun binarySearch(element: Int, fromIndex: Int = 0, toIndex: Int = size): Int {
+ if (fromIndex < 0 || fromIndex >= toIndex || toIndex > _size) {
+ throwIndexOutOfBoundsException("")
+ }
+
+ var low = fromIndex
+ var high = toIndex - 1
+
+ while (low <= high) {
+ val mid = low + high ushr 1
+ val midVal = content[mid]
+ if (midVal < element) {
+ low = mid + 1
+ } else if (midVal > element) {
+ high = mid - 1
+ } else {
+ return mid // key found
+ }
+ }
+
+ return -(low + 1) // key not found.
+ }
+
+ /**
* Creates a String from the elements separated by [separator] and using [prefix] before
* and [postfix] after, if supplied.
*
@@ -570,7 +607,7 @@
*/
public fun add(@IntRange(from = 0) index: Int, element: PKey) {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
ensureCapacity(_size + 1)
val content = content
@@ -597,7 +634,7 @@
elements: PKeyArray
): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements.size)
@@ -626,7 +663,7 @@
elements: PKeyList
): Boolean {
if (index !in 0.._size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (elements.isEmpty()) return false
ensureCapacity(_size + elements._size)
@@ -653,7 +690,7 @@
* Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
* [MutablePKeyList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: PKeyList): Boolean {
+ public inline fun addAll(elements: PKeyList): Boolean {
return addAll(_size, elements)
}
@@ -661,21 +698,21 @@
* Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
* [MutablePKeyList] was changed or `false` if [elements] was empty.
*/
- public fun addAll(elements: PKeyArray): Boolean {
+ public inline fun addAll(elements: PKeyArray): Boolean {
return addAll(_size, elements)
}
/**
* Adds all [elements] to the end of the [MutablePKeyList].
*/
- public operator fun plusAssign(elements: PKeyList) {
+ public inline operator fun plusAssign(elements: PKeyList) {
addAll(_size, elements)
}
/**
* Adds all [elements] to the end of the [MutablePKeyList].
*/
- public operator fun plusAssign(elements: PKeyArray) {
+ public inline operator fun plusAssign(elements: PKeyArray) {
addAll(_size, elements)
}
@@ -785,7 +822,7 @@
*/
public fun removeAt(@IntRange(from = 0) index: Int): PKey {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val item = content[index]
@@ -811,10 +848,10 @@
@IntRange(from = 0) end: Int
) {
if (start !in 0.._size || end !in 0.._size) {
- throwIndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+ throwIndexOutOfBoundsException("")
}
if (end < start) {
- throwIllegalArgumentException("Start ($start) is more than end ($end)")
+ throwIllegalArgumentException("")
}
if (end != start) {
if (end < _size) {
@@ -871,7 +908,7 @@
element: PKey
): PKey {
if (index !in 0 until _size) {
- throwIndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+ throwIndexOutOfBoundsException("")
}
val content = content
val old = content[index]
diff --git a/collection/collection/template/PKeyListTest.kt.template b/collection/collection/template/PKeyListTest.kt.template
index f316360..8f3d8e0 100644
--- a/collection/collection/template/PKeyListTest.kt.template
+++ b/collection/collection/template/PKeyListTest.kt.template
@@ -749,4 +749,17 @@
assertEquals(-1KeySuffix, l[2])
assertEquals(10KeySuffix, l[3])
}
+
+ @Test
+ fun binarySearchPKeyList() {
+ val l = mutablePKeyListOf(-2KeySuffix, -1KeySuffix, 2KeySuffix, 10KeySuffix, 10KeySuffix)
+ assertEquals(0, l.binarySearch(-2))
+ assertEquals(2, l.binarySearch(2))
+ assertEquals(3, l.binarySearch(10))
+
+ assertEquals(-1, l.binarySearch(-20))
+ assertEquals(-4, l.binarySearch(3))
+ assertEquals(-6, l.binarySearch(20))
+
+ }
}
diff --git a/collection/collection/template/PKeyObjectMap.kt.template b/collection/collection/template/PKeyObjectMap.kt.template
index b81707a..39bb237 100644
--- a/collection/collection/template/PKeyObjectMap.kt.template
+++ b/collection/collection/template/PKeyObjectMap.kt.template
@@ -985,7 +985,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -993,7 +992,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -1040,25 +1038,19 @@
values[targetIndex] = values[index]
values[index] = null
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/template/PKeyPValueMap.kt.template b/collection/collection/template/PKeyPValueMap.kt.template
index c292732..524dbe7 100644
--- a/collection/collection/template/PKeyPValueMap.kt.template
+++ b/collection/collection/template/PKeyPValueMap.kt.template
@@ -999,7 +999,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -1007,7 +1006,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -1054,25 +1052,19 @@
values[targetIndex] = values[index]
values[index] = 0ValueSuffix
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- keys[swapIndex] = keys[targetIndex]
+ val oldKey = keys[targetIndex]
keys[targetIndex] = keys[index]
- keys[index] = keys[swapIndex]
+ keys[index] = oldKey
- values[swapIndex] = values[targetIndex]
+ val oldValue = values[targetIndex]
values[targetIndex] = values[index]
- values[index] = values[swapIndex]
+ values[index] = oldValue
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/collection/collection/template/PKeySet.kt.template b/collection/collection/template/PKeySet.kt.template
index cb5a967..38e36a4 100644
--- a/collection/collection/template/PKeySet.kt.template
+++ b/collection/collection/template/PKeySet.kt.template
@@ -797,7 +797,6 @@
// Converts Sentinel and Deleted to Empty, and Full to Deleted
convertMetadataForCleanup(metadata, capacity)
- var swapIndex = -1
var index = 0
// Drop deleted items and re-hashes surviving entries
@@ -805,7 +804,6 @@
var m = readRawMetadata(metadata, index)
// Formerly Deleted entry, we can use it as a swap spot
if (m == Empty) {
- swapIndex = index
index++
continue
}
@@ -849,21 +847,15 @@
elements[targetIndex] = elements[index]
elements[index] = 0KeySuffix
-
- swapIndex = index
} else /* m == Deleted */ {
// The target isn't empty so we use an empty slot denoted by
// swapIndex to perform the swap
val hash2 = h2(hash)
writeRawMetadata(metadata, targetIndex, hash2.toLong())
- if (swapIndex == -1) {
- swapIndex = findEmptySlot(metadata, index + 1, capacity)
- }
-
- elements[swapIndex] = elements[targetIndex]
+ val oldElement = elements[targetIndex]
elements[targetIndex] = elements[index]
- elements[index] = elements[swapIndex]
+ elements[index] = oldElement
// Since we exchanged two slots we must repeat the process with
// element we just moved in the current location
diff --git a/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt
index ecc01b1..565fe581 100644
--- a/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt
+++ b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt
@@ -128,7 +128,7 @@
val start = AnimationVector4D(0f, 0f, 0f, 0f)
val end = AnimationVector4D(120f, -50f, 256f, 0f)
val anim =
- VectorizedKeyframesSpec<AnimationVector4D>(
+ VectorizedKeyframesSpec(
keyframes =
mapOf(
0 to (start to LinearEasing),
@@ -150,7 +150,7 @@
val start = AnimationVector4D(0f, 0f, 0f, 0f)
val end = AnimationVector4D(120f, -50f, 256f, 0f)
val anim =
- VectorizedKeyframesSpec<AnimationVector4D>(
+ VectorizedKeyframesSpec(
keyframes =
mapOf(
0 to (start to LinearEasing),
diff --git a/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/KeyframesSpecWithSplineBenchmark.kt b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/KeyframesSpecWithSplineBenchmark.kt
index 9903e53..fdfec66 100644
--- a/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/KeyframesSpecWithSplineBenchmark.kt
+++ b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/KeyframesSpecWithSplineBenchmark.kt
@@ -107,18 +107,20 @@
val frame0 = playTimeNanosToEvaluate
val frame1 = frame0 + (1000.0f / 60 * 1_000_000).roundToLong()
benchmarkRule.measureRepeated {
- vectorized.getValueFromNanos(
- playTimeNanos = frame0,
- initialValue = initialVector,
- targetValue = targetVector,
- initialVelocity = initialVector
- )
- vectorized.getValueFromNanos(
- playTimeNanos = frame1,
- initialValue = initialVector,
- targetValue = targetVector,
- initialVelocity = initialVector
- )
+ for (i in 0..10) {
+ vectorized.getValueFromNanos(
+ playTimeNanos = frame0,
+ initialValue = initialVector,
+ targetValue = targetVector,
+ initialVelocity = initialVector
+ )
+ vectorized.getValueFromNanos(
+ playTimeNanos = frame1,
+ initialValue = initialVector,
+ targetValue = targetVector,
+ initialVelocity = initialVector
+ )
+ }
}
}
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
index 32d573f..8efa4d2 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/SeekableTransitionStateTest.kt
@@ -71,6 +71,7 @@
import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -2474,6 +2475,46 @@
}
@Test
+ fun isRunningFalseAfterChildAnimatedVisibilityTransition() {
+ val seekableTransitionState = SeekableTransitionState(AnimStates.From)
+ lateinit var coroutineScope: CoroutineScope
+ lateinit var transition: Transition<AnimStates>
+ var animatedVisibilityTransition: Transition<*>? = null
+
+ rule.mainClock.autoAdvance = false
+
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ transition = rememberTransition(seekableTransitionState, label = "Test")
+ transition.AnimatedVisibility(
+ visible = { it == AnimStates.To },
+ ) {
+ animatedVisibilityTransition = this.transition
+ Box(Modifier.size(100.dp))
+ }
+ }
+ rule.runOnIdle {
+ assertFalse(transition.isRunning)
+ assertNull(animatedVisibilityTransition)
+ }
+
+ rule.runOnUiThread {
+ coroutineScope.launch { seekableTransitionState.animateTo(AnimStates.To) }
+ }
+ rule.mainClock.advanceTimeBy(50)
+ rule.runOnIdle {
+ assertTrue(transition.isRunning)
+ assertTrue(animatedVisibilityTransition!!.isRunning)
+ }
+
+ rule.mainClock.advanceTimeBy(5000)
+ rule.runOnIdle {
+ assertFalse(transition.isRunning)
+ assertFalse(animatedVisibilityTransition!!.isRunning)
+ }
+ }
+
+ @Test
fun testCleanupAfterDispose() {
fun isObserving(): Boolean {
var active = false
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IntListExtensionTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IntListExtensionTest.kt
deleted file mode 100644
index a24dda5..0000000
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IntListExtensionTest.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation.core
-
-import androidx.collection.intListOf
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertThrows
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class IntListExtensionTest {
- @Test
- fun binarySearch() {
- val l = intListOf(1, 3, 5)
- assertEquals(0, l.binarySearch(1))
- assertEquals(-2, l.binarySearch(2))
- assertEquals(1, l.binarySearch(3))
- assertEquals(-3, l.binarySearch(4))
- assertEquals(2, l.binarySearch(5))
-
- assertEquals(-2, l.binarySearch(2, fromIndex = 1))
- assertEquals(-3, l.binarySearch(2, fromIndex = 2))
- assertEquals(-3, l.binarySearch(5, toIndex = l.size - 1))
-
- // toIndex is exclusive, fails with size + 1
- assertThrows(IndexOutOfBoundsException::class.java) {
- l.binarySearch(element = 3, toIndex = l.size + 1)
- }
- assertThrows(IndexOutOfBoundsException::class.java) {
- l.binarySearch(element = 3, fromIndex = -1)
- }
- assertThrows(IllegalArgumentException::class.java) {
- l.binarySearch(element = 3, fromIndex = 1, toIndex = 0)
- }
- }
-}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
index 9fe726a..ad48e92 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
@@ -48,7 +48,7 @@
val end = start // the same
val fullTime = 400
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = fullTime
start at 100
0.5f at 200
@@ -66,7 +66,7 @@
fun possibleToOverrideStartAndEndValues() {
val fullTime = 100
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = fullTime
1f at 0
0f at fullTime
@@ -81,7 +81,7 @@
fun withEasingOnFullDuration() {
val easing = FastOutSlowInEasing
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = 100
0f at 0 using easing
1f at durationMillis
@@ -95,7 +95,7 @@
fun easingOnTheSecondPart() {
val easing = FastOutSlowInEasing
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = 200
1f at 100 using easing
2f at durationMillis
@@ -108,7 +108,7 @@
@Test
fun firstPartIsLinearWithEasingOnTheSecondPart() {
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = 100
0.5f at 50 using FastOutSlowInEasing
1f at durationMillis
@@ -122,11 +122,11 @@
fun testMultiDimensKeyframesWithEasing() {
val easing = FastOutLinearInEasing
val animation =
- keyframes<AnimationVector2D> {
+ keyframes {
durationMillis = 400
AnimationVector(200f, 300f) at 200 using easing
}
- .vectorize(TwoWayConverter<AnimationVector2D, AnimationVector2D>({ it }, { it }))
+ .vectorize(TwoWayConverter({ it }, { it }))
val start = AnimationVector(0f, 0f)
val end = AnimationVector(200f, 400f)
@@ -158,18 +158,17 @@
1f at durationMillis
}
- val animation = keyframes<Float>(config)
+ val animation = keyframes(config)
- val animationReuseConfig = keyframes<Float>(config)
+ val animationReuseConfig = keyframes(config)
- val animationRedeclareConfig =
- keyframes<Float> {
- durationMillis = 500
- 0f at 100
- 0.5f at 200 using FastOutLinearInEasing
- 0.8f at 300
- 1f at durationMillis
- }
+ val animationRedeclareConfig = keyframes {
+ durationMillis = 500
+ 0f at 100
+ 0.5f at 200 using FastOutLinearInEasing
+ 0.8f at 300
+ 1f at durationMillis
+ }
assertTrue(animation != animationReuseConfig)
assertTrue(animation != animationRedeclareConfig)
@@ -178,41 +177,37 @@
@Test
fun testNotEquals1() {
- val animation =
- keyframes<Float> {
- durationMillis = 500
- 0f at 100
- 0.5f at 200 using FastOutLinearInEasing
- 0.8f at 300
- 1f at durationMillis
- }
+ val animation = keyframes {
+ durationMillis = 500
+ 0f at 100
+ 0.5f at 200 using FastOutLinearInEasing
+ 0.8f at 300
+ 1f at durationMillis
+ }
- val animationAlteredDuration =
- keyframes<Float> {
- durationMillis = 700
- 0f at 100
- 0.5f at 200 using FastOutLinearInEasing
- 0.8f at 300
- 1f at durationMillis
- }
+ val animationAlteredDuration = keyframes {
+ durationMillis = 700
+ 0f at 100
+ 0.5f at 200 using FastOutLinearInEasing
+ 0.8f at 300
+ 1f at durationMillis
+ }
- val animationAlteredEasing =
- keyframes<Float> {
- durationMillis = 500
- 0f at 100 using FastOutSlowInEasing
- 0.5f at 200
- 0.8f at 300
- 1f at durationMillis
- }
+ val animationAlteredEasing = keyframes {
+ durationMillis = 500
+ 0f at 100 using FastOutSlowInEasing
+ 0.5f at 200
+ 0.8f at 300
+ 1f at durationMillis
+ }
- val animationAlteredKeyframes =
- keyframes<Float> {
- durationMillis = 500
- 0f at 100
- 0.3f at 200 using FastOutLinearInEasing
- 0.8f at 400
- 1f at durationMillis
- }
+ val animationAlteredKeyframes = keyframes {
+ durationMillis = 500
+ 0f at 100
+ 0.3f at 200 using FastOutLinearInEasing
+ 0.8f at 400
+ 1f at durationMillis
+ }
assertTrue(animation != animationAlteredDuration)
assertTrue(animation != animationAlteredEasing)
@@ -225,7 +220,7 @@
val end = start // the same
val fullTime = 400
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = fullTime
start atFraction 0.25f
0.5f atFraction 0.5f
@@ -242,7 +237,7 @@
@Test
fun percentageBasedKeyframesWithEasing() {
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = 100
0.5f atFraction 0.5f using FastOutSlowInEasing
1f atFraction 1f
@@ -260,13 +255,13 @@
// Out of range values should be effectively ignored.
// It should interpolate within the expected time range without issues
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = duration
delayMillis = delay
- -1f at -delay using LinearEasing
- -2f at -duration using LinearEasing
- -3f at (duration + 50) using LinearEasing
+ (-1f) at -delay using LinearEasing
+ (-2f) at -duration using LinearEasing
+ (-3f) at (duration + 50) using LinearEasing
}
.vectorize(Float.VectorConverter)
@@ -294,13 +289,13 @@
// Out of range values should be effectively ignored.
// It should interpolate within the expected time range without issues
val animation =
- keyframes<Float> {
+ keyframes {
durationMillis = duration
delayMillis = delay
- -1f at -delay using LinearEasing
- -2f at -duration using LinearEasing
- -3f at (duration + 50) using LinearEasing
+ (-1f) at -delay using LinearEasing
+ (-2f) at -duration using LinearEasing
+ (-3f) at (duration + 50) using LinearEasing
// Force initial and target
4f at 0 using LinearEasing
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
index 65d9e6f..fa3f502 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
@@ -22,7 +22,6 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-@OptIn(ExperimentalAnimationSpecApi::class)
@RunWith(JUnit4::class)
class MonoSplineTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MotionTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MotionTest.kt
deleted file mode 100644
index ddc271f..0000000
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MotionTest.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 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.
- */
-
-package androidx.compose.animation.core
-
-import org.junit.Assert
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class MotionTest {
- @Test
- fun testMotionCopy() {
- val motion = Motion(100f, 200f)
- Assert.assertEquals(motion, motion.copy())
- }
-
- @Test
- fun testMotionCopyOverwriteValue() {
- val motion = Motion(100f, 200f)
- val copy = motion.copy(value = 50f)
- Assert.assertEquals(50f, copy.value)
- Assert.assertEquals(200f, copy.velocity)
- }
-
- @Test
- fun testMotionCopyOverwriteY() {
- val radius = Motion(100f, 200f)
- val copy = radius.copy(velocity = 300f)
- Assert.assertEquals(100f, copy.value)
- Assert.assertEquals(300f, copy.velocity)
- }
-}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
index 173f3af..0c1f85e 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
@@ -674,7 +674,7 @@
internal constructor(
value: T,
easing: Easing = LinearEasing,
- internal var arcMode: ArcMode = ArcMode.Companion.ArcLinear
+ internal var arcMode: ArcMode = ArcMode.ArcLinear
) : KeyframeBaseEntity<T>(value = value, easing = easing) {
override fun equals(other: Any?): Boolean {
@@ -758,16 +758,17 @@
converter: TwoWayConverter<T, V>
): VectorizedDurationBasedAnimationSpec<V> {
// Allocate so that we don't resize the list even if the initial/last timestamps are missing
- val timestamps = MutableIntList(config.keyframes.size + 2)
- val timeToVectorMap = MutableIntObjectMap<Pair<V, Easing>>(config.keyframes.size)
- config.keyframes.forEach { key, value ->
+ val keyframes = config.keyframes
+ val timestamps = MutableIntList(keyframes.size + 2)
+ val timeToVectorMap = MutableIntObjectMap<Pair<V, Easing>>(keyframes.size)
+ keyframes.forEach { key, value ->
timestamps.add(key)
timeToVectorMap[key] = Pair(converter.convertToVector(value.value), value.easing)
}
- if (!config.keyframes.contains(0)) {
+ if (!keyframes.contains(0)) {
timestamps.add(0, 0)
}
- if (!config.keyframes.contains(config.durationMillis)) {
+ if (!keyframes.contains(config.durationMillis)) {
timestamps.add(config.durationMillis)
}
timestamps.sort()
@@ -832,8 +833,8 @@
* @see KeyframesSpec.KeyframesSpecConfig
*/
@Stable
-public fun <T> keyframes(init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit): KeyframesSpec<T> {
- return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init))
+public fun <T> keyframes(init: KeyframesSpecConfig<T>.() -> Unit): KeyframesSpec<T> {
+ return KeyframesSpec(KeyframesSpecConfig<T>().apply(init))
}
/**
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt
index 740faa1..45dfee1 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt
@@ -14,12 +14,18 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE")
+
package androidx.compose.animation.core
+import androidx.compose.ui.util.fastCoerceIn
+import kotlin.jvm.JvmField
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.hypot
+import kotlin.math.max
+import kotlin.math.min
import kotlin.math.sin
/**
@@ -41,33 +47,39 @@
arcs =
Array(timePoints.size - 1) { i ->
when (arcModes[i]) {
- ArcStartVertical -> {
+ ArcSplineArcStartVertical -> {
mode = StartVertical
last = mode
}
- ArcStartHorizontal -> {
+ ArcSplineArcStartHorizontal -> {
mode = StartHorizontal
last = mode
}
- ArcStartFlip -> {
+ ArcSplineArcStartFlip -> {
mode = if (last == StartVertical) StartHorizontal else StartVertical
last = mode
}
- ArcStartLinear -> mode = StartLinear
- ArcAbove -> mode = UpArc
- ArcBelow -> mode = DownArc
+ ArcSplineArcStartLinear -> mode = StartLinear
+ ArcSplineArcAbove -> mode = UpArc
+ ArcSplineArcBelow -> mode = DownArc
}
- val dim = y[i].size / 2 + y[i].size % 2
+
+ val yArray = y[i]
+ val yArray1 = y[i + 1]
+ val timeArray = timePoints[i]
+ val timeArray1 = timePoints[i + 1]
+
+ val dim = yArray.size / 2 + yArray.size % 2
Array(dim) { j ->
val k = j * 2
Arc(
mode = mode,
- time1 = timePoints[i],
- time2 = timePoints[i + 1],
- x1 = y[i][k],
- y1 = y[i][k + 1],
- x2 = y[i + 1][k],
- y2 = y[i + 1][k + 1]
+ time1 = timeArray,
+ time2 = timeArray1,
+ x1 = yArray[k],
+ y1 = yArray[k + 1],
+ x2 = yArray1[k],
+ y2 = yArray1[k + 1]
)
}
}
@@ -76,29 +88,36 @@
/** get the values of the at t point in time. */
fun getPos(time: Float, v: FloatArray) {
var t = time
+ val arcs = arcs
+ val lastIndex = arcs.size - 1
+ val start = arcs[0][0].time1
+ val end = arcs[lastIndex][0].time2
+ val size = v.size
+
if (isExtrapolate) {
- if (t < arcs[0][0].time1 || t > arcs[arcs.size - 1][0].time2) {
+ if (t < start || t > end) {
val p: Int
val t0: Float
- if (t > arcs[arcs.size - 1][0].time2) {
- p = arcs.size - 1
- t0 = arcs[arcs.size - 1][0].time2
+ if (t > end) {
+ p = lastIndex
+ t0 = end
} else {
p = 0
- t0 = arcs[0][0].time1
+ t0 = start
}
val dt = t - t0
var i = 0
var j = 0
- while (i < v.size) {
- if (arcs[p][j].isLinear) {
- v[i] = arcs[p][j].getLinearX(t0) + dt * arcs[p][j].getLinearDX()
- v[i + 1] = arcs[p][j].getLinearY(t0) + dt * arcs[p][j].getLinearDY()
+ while (i < size - 1) {
+ val arc = arcs[p][j]
+ if (arc.isLinear) {
+ v[i] = arc.getLinearX(t0) + dt * arc.linearDX
+ v[i + 1] = arc.getLinearY(t0) + dt * arc.linearDY
} else {
- arcs[p][j].setPoint(t0)
- v[i] = arcs[p][j].calcX() + dt * arcs[p][j].calcDX()
- v[i + 1] = arcs[p][j].calcY() + dt * arcs[p][j].calcDY()
+ arc.setPoint(t0)
+ v[i] = arc.calcX() + dt * arc.calcDX()
+ v[i + 1] = arc.calcY() + dt * arc.calcDY()
}
i += 2
j++
@@ -106,12 +125,8 @@
return
}
} else {
- if (t < arcs[0][0].time1) {
- t = arcs[0][0].time1
- }
- if (t > arcs[arcs.size - 1][0].time2) {
- t = arcs[arcs.size - 1][0].time2
- }
+ t = max(t, start)
+ t = min(t, end)
}
// TODO: Consider passing the index from the caller to improve performance
@@ -119,18 +134,18 @@
for (i in arcs.indices) {
var k = 0
var j = 0
- while (j < v.size) {
- if (t <= arcs[i][k].time2) {
- if (arcs[i][k].isLinear) {
- v[j] = arcs[i][k].getLinearX(t)
- v[j + 1] = arcs[i][k].getLinearY(t)
- populated = true
+ while (j < size - 1) {
+ val arc = arcs[i][k]
+ if (t <= arc.time2) {
+ if (arc.isLinear) {
+ v[j] = arc.getLinearX(t)
+ v[j + 1] = arc.getLinearY(t)
} else {
- arcs[i][k].setPoint(t)
- v[j] = arcs[i][k].calcX()
- v[j + 1] = arcs[i][k].calcY()
- populated = true
+ arc.setPoint(t)
+ v[j] = arc.calcX()
+ v[j + 1] = arc.calcY()
}
+ populated = true
}
j += 2
k++
@@ -143,29 +158,27 @@
/** Get the differential which of the curves at point t */
fun getSlope(time: Float, v: FloatArray) {
- var t = time
- if (t < arcs[0][0].time1) {
- t = arcs[0][0].time1
- } else if (t > arcs[arcs.size - 1][0].time2) {
- t = arcs[arcs.size - 1][0].time2
- }
+ val arcs = arcs
+ val t = time.fastCoerceIn(arcs[0][0].time1, arcs[arcs.size - 1][0].time2)
+ val size = v.size
+
var populated = false
// TODO: Consider passing the index from the caller to improve performance
for (i in arcs.indices) {
var j = 0
var k = 0
- while (j < v.size) {
- if (t <= arcs[i][k].time2) {
- if (arcs[i][k].isLinear) {
- v[j] = arcs[i][k].getLinearDX()
- v[j + 1] = arcs[i][k].getLinearDY()
- populated = true
+ while (j < size - 1) {
+ val arc = arcs[i][k]
+ if (t <= arc.time2) {
+ if (arc.isLinear) {
+ v[j] = arc.linearDX
+ v[j + 1] = arc.linearDY
} else {
- arcs[i][k].setPoint(t)
- v[j] = arcs[i][k].calcDX()
- v[j + 1] = arcs[i][k].calcDY()
- populated = true
+ arc.setPoint(t)
+ v[j] = arc.calcDX()
+ v[j + 1] = arc.calcDY()
}
+ populated = true
}
j += 2
k++
@@ -192,46 +205,51 @@
private val lut: FloatArray
private val oneOverDeltaTime: Float
- private val ellipseA: Float
- private val ellipseB: Float
- private val ellipseCenterX: Float // also used to cache the slope in the unused center
- private val ellipseCenterY: Float // also used to cache the slope in the unused center
private val arcVelocity: Float
- private val isVertical: Boolean
+ private val vertical: Float
- val isLinear: Boolean
+ @JvmField internal val ellipseA: Float
+ @JvmField internal val ellipseB: Float
+
+ @JvmField internal val isLinear: Boolean
+
+ // also used to cache the slope in the unused center
+ @JvmField internal val ellipseCenterX: Float
+ // also used to cache the slope in the unused center
+ @JvmField internal val ellipseCenterY: Float
+
+ internal inline val linearDX: Float
+ get() = ellipseCenterX
+
+ internal inline val linearDY: Float
+ get() = ellipseCenterY
init {
val dx = x2 - x1
val dy = y2 - y1
- isVertical =
+ val isVertical =
when (mode) {
StartVertical -> true
UpArc -> dy < 0
DownArc -> dy > 0
else -> false
}
+ vertical = if (isVertical) -1.0f else 1.0f
oneOverDeltaTime = 1 / (this.time2 - this.time1)
+ lut = FloatArray(LutSize)
- var isLinear = false
- if (StartLinear == mode) {
- isLinear = true
- }
+ var isLinear = mode == StartLinear
if (isLinear || abs(dx) < Epsilon || abs(dy) < Epsilon) {
isLinear = true
arcDistance = hypot(dy, dx)
arcVelocity = arcDistance * oneOverDeltaTime
- ellipseCenterX =
- dx / (this.time2 - this.time1) // cache the slope in the unused center
- ellipseCenterY =
- dy / (this.time2 - this.time1) // cache the slope in the unused center
- lut = FloatArray(101)
+ ellipseCenterX = dx * oneOverDeltaTime // cache the slope in the unused center
+ ellipseCenterY = dy * oneOverDeltaTime // cache the slope in the unused center
ellipseA = Float.NaN
ellipseB = Float.NaN
} else {
- lut = FloatArray(101)
- ellipseA = dx * if (isVertical) -1 else 1
- ellipseB = dy * if (isVertical) 1 else -1
+ ellipseA = dx * vertical
+ ellipseB = dy * -vertical
ellipseCenterX = if (isVertical) x2 else x1
ellipseCenterY = if (isVertical) y1 else y2
buildTable(x1, y1, x2, y2)
@@ -241,17 +259,21 @@
}
fun setPoint(time: Float) {
- val percent = (if (isVertical) time2 - time else time - time1) * oneOverDeltaTime
- val angle = PI.toFloat() * 0.5f * lookup(percent)
+ val angle = calcAngle(time)
tmpSinAngle = sin(angle)
tmpCosAngle = cos(angle)
}
- fun calcX(): Float {
+ private inline fun calcAngle(time: Float): Float {
+ val percent = (if (vertical == -1.0f) time2 - time else time - time1) * oneOverDeltaTime
+ return HalfPi * lookup(percent)
+ }
+
+ inline fun calcX(): Float {
return ellipseCenterX + ellipseA * tmpSinAngle
}
- fun calcY(): Float {
+ inline fun calcY(): Float {
return ellipseCenterY + ellipseB * tmpCosAngle
}
@@ -259,14 +281,14 @@
val vx = ellipseA * tmpCosAngle
val vy = -ellipseB * tmpSinAngle
val norm = arcVelocity / hypot(vx, vy)
- return if (isVertical) -vx * norm else vx * norm
+ return vx * vertical * norm
}
fun calcDY(): Float {
val vx = ellipseA * tmpCosAngle
val vy = -ellipseB * tmpSinAngle
val norm = arcVelocity / hypot(vx, vy)
- return if (isVertical) -vy * norm else vy * norm
+ return vy * vertical * norm
}
fun getLinearX(time: Float): Float {
@@ -281,14 +303,6 @@
return y1 + t * (y2 - y1)
}
- fun getLinearDX(): Float {
- return ellipseCenterX
- }
-
- fun getLinearDY(): Float {
- return ellipseCenterY
- }
-
private fun lookup(v: Float): Float {
if (v <= 0) {
return 0.0f
@@ -296,40 +310,48 @@
if (v >= 1) {
return 1.0f
}
- val pos = v * (lut.size - 1)
+ val pos = v * (LutSize - 1)
val iv = pos.toInt()
val off = pos - pos.toInt()
return lut[iv] + off * (lut[iv + 1] - lut[iv])
}
- private fun buildTable(x1: Float, y1: Float, x2: Float, y2: Float) {
+ // Internal to prevent inlining from R8
+ @Suppress("MemberVisibilityCanBePrivate")
+ internal fun buildTable(x1: Float, y1: Float, x2: Float, y2: Float) {
val a = x2 - x1
val b = y1 - y2
var lx = 0f
- var ly = 0f
+ var ly = b // == b * cos(0), because we skip index 0 in the loops below
var dist = 0f
- for (i in ourPercent.indices) {
- val angle = toRadians(90.0 * i / (ourPercent.size - 1)).toFloat()
+
+ val ourPercent = OurPercentCache
+ val lastIndex = ourPercent.size - 1
+ val lut = lut
+
+ for (i in 1..lastIndex) {
+ val angle = toRadians(90.0 * i / lastIndex).toFloat()
val s = sin(angle)
val c = cos(angle)
val px = a * s
val py = b * c
- if (i > 0) {
- dist += hypot((px - lx), (py - ly))
- ourPercent[i] = dist
- }
+ dist += hypot((px - lx), (py - ly)) // we don't want to compute and store dist
+ ourPercent[i] = dist // for i == 0
lx = px
ly = py
}
+
arcDistance = dist
- for (i in ourPercent.indices) {
+ for (i in 1..lastIndex) {
ourPercent[i] /= dist
}
+
+ val lutLastIndex = (LutSize - 1).toFloat()
for (i in lut.indices) {
- val pos = i / (lut.size - 1).toFloat()
+ val pos = i / lutLastIndex
val index = binarySearch(ourPercent, pos)
if (index >= 0) {
- lut[i] = index / (ourPercent.size - 1).toFloat()
+ lut[i] = index / lutLastIndex
} else if (index == -1) {
lut[i] = 0f
} else {
@@ -337,42 +359,33 @@
val p2 = -index - 1
val ans =
(p1 + (pos - ourPercent[p1]) / (ourPercent[p2] - ourPercent[p1])) /
- (ourPercent.size - 1)
+ lastIndex
lut[i] = ans
}
}
}
-
- companion object {
- private var _ourPercent: FloatArray? = null
- private val ourPercent: FloatArray
- get() {
- if (_ourPercent != null) {
- return _ourPercent!!
- }
- _ourPercent = FloatArray(91)
- return _ourPercent!!
- }
-
- private const val Epsilon = 0.001f
- }
- }
-
- companion object {
- const val ArcStartVertical = 1
- const val ArcStartHorizontal = 2
- const val ArcStartFlip = 3
- const val ArcBelow = 4
- const val ArcAbove = 5
- const val ArcStartLinear = 0
- private const val StartVertical = 1
- private const val StartHorizontal = 2
- private const val StartLinear = 3
- private const val DownArc = 4
- private const val UpArc = 5
}
}
+internal const val ArcSplineArcStartLinear = 0
+internal const val ArcSplineArcStartVertical = 1
+internal const val ArcSplineArcStartHorizontal = 2
+internal const val ArcSplineArcStartFlip = 3
+internal const val ArcSplineArcBelow = 4
+internal const val ArcSplineArcAbove = 5
+
+private const val StartVertical = 1
+private const val StartHorizontal = 2
+private const val StartLinear = 3
+private const val DownArc = 4
+private const val UpArc = 5
+private const val LutSize = 101
+
+private const val Epsilon = 0.001f
+private const val HalfPi = (PI * 0.5).toFloat()
+
+private val OurPercentCache: FloatArray = FloatArray(91)
+
internal expect inline fun toRadians(value: Double): Double
internal expect inline fun binarySearch(array: FloatArray, position: Float): Int
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ComplexDouble.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ComplexDouble.kt
deleted file mode 100644
index 0a2ceb2..0000000
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ComplexDouble.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 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.
- */
-
-@file:Suppress("NOTHING_TO_INLINE")
-
-package androidx.compose.animation.core
-
-import kotlin.math.abs
-import kotlin.math.sqrt
-
-internal data class ComplexDouble(private var _real: Double, private var _imaginary: Double) {
- val real: Double
- get() {
- return _real
- }
-
- val imaginary: Double
- get() {
- return _imaginary
- }
-
- inline operator fun plus(other: Double): ComplexDouble {
- _real += other
- return this
- }
-
- inline operator fun plus(other: ComplexDouble): ComplexDouble {
- _real += other.real
- _imaginary += other.imaginary
- return this
- }
-
- inline operator fun minus(other: Double): ComplexDouble {
- return this + -other
- }
-
- inline operator fun minus(other: ComplexDouble): ComplexDouble {
- return this + -other
- }
-
- inline operator fun times(other: Double): ComplexDouble {
- _real *= other
- _imaginary *= other
- return this
- }
-
- inline operator fun times(other: ComplexDouble): ComplexDouble {
- _real = real * other.real - imaginary * other.imaginary
- _imaginary = real * other.imaginary + other.real * imaginary
- return this
- }
-
- inline operator fun unaryMinus(): ComplexDouble {
- _real *= -1
- _imaginary *= -1
- return this
- }
-
- inline operator fun div(other: Double): ComplexDouble {
- _real /= other
- _imaginary /= other
- return this
- }
-}
-
-/** Returns the roots of the polynomial [a]x^2+[b]x+[c]=0 which may be complex. */
-internal fun complexQuadraticFormula(
- a: Double,
- b: Double,
- c: Double
-): Pair<ComplexDouble, ComplexDouble> {
- val partialRoot = b * b - 4.0 * a * c
- val divisor = 1.0 / (2.0 * a)
- val firstRoot = (-b + complexSqrt(partialRoot)) * divisor
- val secondRoot = (-b - complexSqrt(partialRoot)) * divisor
- return firstRoot to secondRoot
-}
-
-/** Returns the square root of [num] which may be imaginary. */
-internal fun complexSqrt(num: Double): ComplexDouble =
- if (num < 0.0) {
- ComplexDouble(0.0, sqrt(abs(num)))
- } else {
- ComplexDouble(sqrt(num), 0.0)
- }
-
-internal inline operator fun Double.plus(other: ComplexDouble): ComplexDouble {
- return other + this
-}
-
-internal inline operator fun Double.minus(other: ComplexDouble): ComplexDouble {
- return this + -other
-}
-
-internal inline operator fun Double.times(other: ComplexDouble): ComplexDouble {
- return other * this
-}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/FloatAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/FloatAnimationSpec.kt
index e0c04a3..98c6625 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/FloatAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/FloatAnimationSpec.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.animation.core
import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
@@ -149,8 +151,7 @@
// TODO: Properly support Nanos in the spring impl
val playTimeMillis = playTimeNanos / MillisToNanos
spring.finalPosition = targetValue
- val value = spring.updateValues(initialValue, initialVelocity, playTimeMillis).value
- return value
+ return spring.updateValues(initialValue, initialVelocity, playTimeMillis).value
}
override fun getVelocityFromNanos(
@@ -162,8 +163,7 @@
// TODO: Properly support Nanos in the spring impl
val playTimeMillis = playTimeNanos / MillisToNanos
spring.finalPosition = targetValue
- val velocity = spring.updateValues(initialValue, initialVelocity, playTimeMillis).velocity
- return velocity
+ return spring.updateValues(initialValue, initialVelocity, playTimeMillis).velocity
}
override fun getEndVelocity(
@@ -193,7 +193,7 @@
* specified, the animation will start right away.
*
* @param duration the amount of time (in milliseconds) the animation will take to finish. Defaults
- * to [DefaultDuration]
+ * to [DefaultDurationMillis]
* @param delay the amount of time the animation will wait before it starts running. Defaults to 0.
* @param easing the easing function that will be used to interoplate between the start and end
* value of the animation. Defaults to [FastOutSlowInEasing].
@@ -215,12 +215,12 @@
): Float {
val clampedPlayTimeNanos = clampPlayTimeNanos(playTimeNanos)
val rawFraction = if (duration == 0) 1f else clampedPlayTimeNanos / durationNanos.toFloat()
- val fraction = easing.transform(rawFraction.fastCoerceIn(0f, 1f))
+ val fraction = easing.transform(rawFraction)
return lerp(initialValue, targetValue, fraction)
}
- private fun clampPlayTimeNanos(playTimeNanos: Long): Long {
- return (playTimeNanos - delayNanos).coerceIn(0, durationNanos)
+ private inline fun clampPlayTimeNanos(playTimeNanos: Long): Long {
+ return (playTimeNanos - delayNanos).fastCoerceIn(0, durationNanos)
}
@Suppress("MethodNameUnits")
@@ -229,7 +229,7 @@
targetValue: Float,
initialVelocity: Float
): Long {
- return (delay + duration) * MillisToNanos
+ return delayNanos + durationNanos
}
// Calculate velocity by difference between the current value and the value 1 ms ago. This is a
@@ -242,9 +242,7 @@
initialVelocity: Float
): Float {
val clampedPlayTimeNanos = clampPlayTimeNanos(playTimeNanos)
- if (clampedPlayTimeNanos < 0L) {
- return 0f
- } else if (clampedPlayTimeNanos == 0L) {
+ if (clampedPlayTimeNanos == 0L) {
return initialVelocity
}
val startNum =
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/IntListExtension.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/IntListExtension.kt
deleted file mode 100644
index 5544fcb..0000000
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/IntListExtension.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation.core
-
-import androidx.collection.IntList
-import kotlin.jvm.JvmOverloads
-
-// TODO(b/311454748): Move to :collection as public API once it's back on alpha. Also, add versions
-// for LongList and FloatList.
-
-/**
- * [IntArray.binarySearch] For original documentation.
- *
- * Searches the list or the range of the list for the provided [element] using the binary search
- * algorithm. The list is expected to be sorted, otherwise the result is undefined.
- *
- * If the list contains multiple elements equal to the specified [element], there is no guarantee
- * which one will be found.
- *
- * @param element the to search for.
- * @param fromIndex the start of the range (inclusive) to search in, 0 by default.
- * @param toIndex the end of the range (exclusive) to search in, size of this list by default.
- * @return the index of the element, if it is contained in the list within the specified range;
- * otherwise, the inverted insertion point `(-insertion point - 1)`. The insertion point is
- * defined as the index at which the element should be inserted, so that the list (or the
- * specified subrange of list) still remains sorted.
- * @throws IndexOutOfBoundsException if [fromIndex] is less than zero or [toIndex] is greater than
- * the size of this list.
- * @throws IllegalArgumentException if [fromIndex] is greater than [toIndex].
- */
-@JvmOverloads
-internal fun IntList.binarySearch(element: Int, fromIndex: Int = 0, toIndex: Int = size): Int {
- requirePrecondition(fromIndex <= toIndex) { "fromIndex($fromIndex) > toIndex($toIndex)" }
- if (fromIndex < 0) {
- throw IndexOutOfBoundsException("Index out of range: $fromIndex")
- }
- if (toIndex > size) {
- throw IndexOutOfBoundsException("Index out of range: $toIndex")
- }
-
- var low = fromIndex
- var high = toIndex - 1
-
- while (low <= high) {
- val mid = low + high ushr 1
- val midVal = this[mid]
- if (midVal < element) {
- low = mid + 1
- } else if (midVal > element) {
- high = mid - 1
- } else {
- return mid // key found
- }
- }
- return -(low + 1) // key not found.
-}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt
index 508d41d..1ce3959 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt
@@ -16,8 +16,11 @@
package androidx.compose.animation.core
+import androidx.compose.ui.util.fastCoerceIn
import kotlin.math.hypot
+private const val MonoSplineIsExtrapolate = true
+
/**
* This performs a spline interpolation in multiple dimensions time is an array of all positions and
* y is a list of arrays each with the values at each point
@@ -26,7 +29,6 @@
private val timePoints: FloatArray
private val values: Array<FloatArray>
private val tangents: Array<FloatArray>
- private val isExtrapolate = true
private val slopeTemp: FloatArray
init {
@@ -88,22 +90,20 @@
/** get the value of the j'th spline at time t */
fun getPos(t: Float, j: Int): Float {
+ val values = values
+ val tangents = tangents
val n = timePoints.size
- if (isExtrapolate) {
- if (t <= timePoints[0]) {
- return values[0][j] + (t - timePoints[0]) * getSlope(timePoints[0], j)
- }
- if (t >= timePoints[n - 1]) {
- return values[n - 1][j] + (t - timePoints[n - 1]) * getSlope(timePoints[n - 1], j)
+ val index = if (t <= timePoints[0]) 0 else (if (t >= timePoints[n - 1]) n - 1 else -1)
+ if (MonoSplineIsExtrapolate) {
+ if (index != -1) {
+ return values[index][j] + (t - timePoints[index]) * getSlope(timePoints[index], j)
}
} else {
- if (t <= timePoints[0]) {
- return values[0][j]
- }
- if (t >= timePoints[n - 1]) {
- return values[n - 1][j]
+ if (index != -1) {
+ return values[index][j]
}
}
+
for (i in 0 until n - 1) {
if (t == timePoints[i]) {
return values[i][j]
@@ -115,7 +115,7 @@
val y2 = values[i + 1][j]
val t1 = tangents[i][j]
val t2 = tangents[i + 1][j]
- return interpolate(h, x, y1, y2, t1, t2)
+ return hermiteInterpolate(h, x, y1, y2, t1, t2)
}
}
return 0.0f // should never reach here
@@ -129,40 +129,30 @@
fun getPos(time: Float, v: AnimationVector, index: Int = 0) {
val n = timePoints.size
val dim = values[0].size
- if (isExtrapolate) {
- if (time <= timePoints[0]) {
- getSlope(timePoints[0], slopeTemp)
+ val k = if (time <= timePoints[0]) 0 else (if (time >= timePoints[n - 1]) n - 1 else -1)
+ if (MonoSplineIsExtrapolate) {
+ if (k != -1) {
+ getSlope(timePoints[k], slopeTemp)
for (j in 0 until dim) {
- v[j] = values[0][j] + (time - timePoints[0]) * slopeTemp[j]
- }
- return
- }
- if (time >= timePoints[n - 1]) {
- getSlope(timePoints[n - 1], slopeTemp)
- for (j in 0 until dim) {
- v[j] = values[n - 1][j] + (time - timePoints[n - 1]) * slopeTemp[j]
+ v[j] = values[k][j] + (time - timePoints[k]) * slopeTemp[j]
}
return
}
} else {
- if (time <= timePoints[0]) {
+ if (k != -1) {
for (j in 0 until dim) {
- v[j] = values[0][j]
- }
- return
- }
- if (time >= timePoints[n - 1]) {
- for (j in 0 until dim) {
- v[j] = values[n - 1][j]
+ v[j] = values[k][j]
}
return
}
}
+
for (i in index until n - 1) {
if (time == timePoints[i]) {
for (j in 0 until dim) {
v[j] = values[i][j]
}
+ return
}
if (time < timePoints[i + 1]) {
val h = timePoints[i + 1] - timePoints[i]
@@ -172,7 +162,7 @@
val y2 = values[i + 1][j]
val t1 = tangents[i][j]
val t2 = tangents[i + 1][j]
- v[j] = interpolate(h, x, y1, y2, t1, t2)
+ v[j] = hermiteInterpolate(h, x, y1, y2, t1, t2)
}
return
}
@@ -180,15 +170,12 @@
}
/** Get the differential of the value at time fill an array of slopes for each spline */
- fun getSlope(time: Float, v: FloatArray) {
- var t = time
- val n = timePoints.size
+ private fun getSlope(time: Float, v: FloatArray) {
val dim = values[0].size
- if (t <= timePoints[0]) {
- t = timePoints[0]
- } else if (t >= timePoints[n - 1]) {
- t = timePoints[n - 1]
- }
+ val n = timePoints.size
+ val t = time.fastCoerceIn(timePoints[0], timePoints[n - 1])
+
+ if (v.size < dim) return
for (i in 0 until n - 1) {
if (t <= timePoints[i + 1]) {
val h = timePoints[i + 1] - timePoints[i]
@@ -198,12 +185,11 @@
val y2 = values[i + 1][j]
val t1 = tangents[i][j]
val t2 = tangents[i + 1][j]
- v[j] = diff(h, x, y1, y2, t1, t2) / h
+ v[j] = hermiteDifferential(h, x, y1, y2, t1, t2) / h
}
break
}
}
- return
}
/**
@@ -213,78 +199,104 @@
* You may provide [index] to simplify searching for the correct keyframe for the given [time].
*/
fun getSlope(time: Float, v: AnimationVector, index: Int = 0) {
- val t = time
+ val timePoints = timePoints
+ val values = values
+ val tangents = tangents
+
val n = timePoints.size
val dim = values[0].size
// If time is 0, max or out of range we directly return the corresponding slope value
- if (t <= timePoints[0]) {
+ val tangentIndex =
+ if (time <= timePoints[0]) 0 else (if (time >= timePoints[n - 1]) n - 1 else -1)
+ if (tangentIndex != -1) {
+ val tangent = tangents[tangentIndex]
+ // Help ART eliminate bound checks
+ if (tangent.size < dim) return
for (j in 0 until dim) {
- v[j] = tangents[0][j]
- }
- return
- } else if (t >= timePoints[n - 1]) {
- for (j in 0 until dim) {
- v[j] = tangents[n - 1][j]
+ v[j] = tangent[j]
}
return
}
// Otherwise, calculate interpolated velocity
for (i in index until n - 1) {
- if (t <= timePoints[i + 1]) {
+ if (time <= timePoints[i + 1]) {
val h = timePoints[i + 1] - timePoints[i]
- val x = (t - timePoints[i]) / h
+ val x = (time - timePoints[i]) / h
for (j in 0 until dim) {
val y1 = values[i][j]
val y2 = values[i + 1][j]
val t1 = tangents[i][j]
val t2 = tangents[i + 1][j]
- v[j] = diff(h, x, y1, y2, t1, t2) / h
+ v[j] = hermiteDifferential(h, x, y1, y2, t1, t2) / h
}
break
}
}
- return
}
private fun getSlope(time: Float, j: Int): Float {
- var t = time
+ val timePoints = timePoints
+ val values = values
+ val tangents = tangents
val n = timePoints.size
- if (t < timePoints[0]) {
- t = timePoints[0]
- } else if (t >= timePoints[n - 1]) {
- t = timePoints[n - 1]
- }
+ val t = time.fastCoerceIn(timePoints[0], timePoints[n - 1])
for (i in 0 until n - 1) {
if (t <= timePoints[i + 1]) {
- val h = timePoints[i + 1] - timePoints[i]
- val x = (t - timePoints[i]) / h
val y1 = values[i][j]
val y2 = values[i + 1][j]
val t1 = tangents[i][j]
val t2 = tangents[i + 1][j]
- return diff(h, x, y1, y2, t1, t2) / h
+ val h = timePoints[i + 1] - timePoints[i]
+ val x = (t - timePoints[i]) / h
+ return hermiteDifferential(h, x, y1, y2, t1, t2) / h
}
}
return 0.0f // should never reach here
}
+}
- /** Cubic Hermite spline */
- private fun interpolate(h: Float, x: Float, y1: Float, y2: Float, t1: Float, t2: Float): Float {
- val x2 = x * x
- val x3 = x2 * x
- return (-2 * x3 * y2 + 3 * x2 * y2 + 2 * x3 * y1 - 3 * x2 * y1 +
- y1 +
- h * t2 * x3 +
- h * t1 * x3 - h * t2 * x2 - 2 * h * t1 * x2 + h * t1 * x)
- }
+/** Cubic Hermite spline */
+internal fun hermiteInterpolate(
+ h: Float,
+ x: Float,
+ y1: Float,
+ y2: Float,
+ t1: Float,
+ t2: Float
+): Float {
+ val x2 = x * x
+ val x3 = x2 * x
+ // The exact formula is as follows:
+ //
+ // -2 * x3 * y2 + 3 * x2 * y2 + 2 * x3 * y1 - 3 * x2 * y1 +
+ // y1 +
+ // h * t2 * x3 +
+ // h * t1 * x3 - h * t2 * x2 - 2 * h * t1 * x2 + h * t1 * x)
+ //
+ // The code below is equivalent but factored to go from 30 down to 20 instructions
+ // on aarch64 devices
+ return h * t1 * (x - 2 * x2 + x3) + h * t2 * (x3 - x2) + y1 - (3 * x2 - 2 * x3) * (y1 - y2)
+}
- /** Cubic Hermite spline slope differentiated */
- private fun diff(h: Float, x: Float, y1: Float, y2: Float, t1: Float, t2: Float): Float {
- val x2 = x * x
- return (-6 * x2 * y2 + 6 * x * y2 + 6 * x2 * y1 - 6 * x * y1 +
- 3 * h * t2 * x2 +
- 3 * h * t1 * x2 - 2 * h * t2 * x - 4 * h * t1 * x + h * t1)
- }
+/** Cubic Hermite spline slope differentiated */
+internal fun hermiteDifferential(
+ h: Float,
+ x: Float,
+ y1: Float,
+ y2: Float,
+ t1: Float,
+ t2: Float
+): Float {
+ // The exact formula is as follows:
+ //
+ // -6 * x2 * y2 + 6 * x * y2 + 6 * x2 * y1 - 6 * x * y1 +
+ // 3 * h * t2 * x2 +
+ // 3 * h * t1 * x2 - 2 * h * t2 * x - 4 * h * t1 * x + h * t1
+ //
+ // The code below is equivalent but factored to go from 33 down to 19 instructions
+ // on aarch64 devices
+ val x2 = x * x
+ return h * (t1 - 2 * x * (2 * t1 + t2) + 3 * (t1 + t2) * x2) - 6 * (x - x2) * (y1 - y2)
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt
index 697eca6..12b230a 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringEstimation.kt
@@ -17,6 +17,7 @@
package androidx.compose.animation.core
import androidx.annotation.RestrictTo
+import androidx.compose.ui.util.fastIsFinite
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.ln
@@ -67,12 +68,17 @@
// Compute the roots of the polynomial [a]x^2+[b]x+[c]=0 which may be complex.
// Here a is set to the constant 1.0, and folded into the other computations
val partialRoot = dampingCoefficient * dampingCoefficient - 4.0 * stiffness
- val firstRoot = (-dampingCoefficient + complexSqrt(partialRoot)) * 0.5
- val secondRoot = (-dampingCoefficient - complexSqrt(partialRoot)) * 0.5
+ val partialRootReal = if (partialRoot < 0.0) 0.0 else sqrt(partialRoot)
+ val partialRootImaginary = if (partialRoot < 0.0) sqrt(abs(partialRoot)) else 0.0
+
+ val firstRootReal = (-dampingCoefficient + partialRootReal) * 0.5
+ val firstRootImaginary = partialRootImaginary * 0.5
+ val secondRootReal = (-dampingCoefficient - partialRootReal) * 0.5
return estimateDurationInternal(
- firstRoot,
- secondRoot,
+ firstRootReal,
+ firstRootImaginary,
+ secondRootReal,
dampingRatio,
initialVelocity,
initialDisplacement,
@@ -96,12 +102,17 @@
// Compute the roots of the polynomial [a]x^2+[b]x+[c]=0 which may be complex.
val partialRoot = dampingCoefficient * dampingCoefficient - 4.0 * mass * springConstant
val divisor = 1.0 / (2.0 * mass)
- val firstRoot = (-dampingCoefficient + complexSqrt(partialRoot)) * divisor
- val secondRoot = (-dampingCoefficient - complexSqrt(partialRoot)) * divisor
+ val partialRootReal = if (partialRoot < 0.0) 0.0 else sqrt(partialRoot)
+ val partialRootImaginary = if (partialRoot < 0.0) sqrt(abs(partialRoot)) else 0.0
+
+ val firstRootReal = (-dampingCoefficient + partialRootReal) * divisor
+ val firstRootImaginary = partialRootImaginary * divisor
+ val secondRootReal = (-dampingCoefficient - partialRootReal) * divisor
return estimateDurationInternal(
- firstRoot,
- secondRoot,
+ firstRootReal,
+ firstRootImaginary,
+ secondRootReal,
dampingRatio,
initialVelocity,
initialDisplacement,
@@ -115,14 +126,15 @@
* c*e^(r*t)*cos(...) where c*e^(r*t) is the envelope of x(t)
*/
private fun estimateUnderDamped(
- firstRoot: ComplexDouble,
+ firstRootReal: Double,
+ firstRootImaginary: Double,
p0: Double,
v0: Double,
delta: Double
): Double {
- val r = firstRoot.real
+ val r = firstRootReal
val c1 = p0
- val c2 = (v0 - r * c1) / firstRoot.imaginary
+ val c2 = (v0 - r * c1) / firstRootImaginary
val c = sqrt(c1 * c1 + c2 * c2)
return ln(delta / c) / r
@@ -133,12 +145,12 @@
* equation x(t) = c_1*e^(r*t) + c_2*t*e^(r*t)
*/
private fun estimateCriticallyDamped(
- firstRoot: ComplexDouble,
+ firstRootReal: Double,
p0: Double,
v0: Double,
delta: Double
): Double {
- val r = firstRoot.real
+ val r = firstRootReal
val c1 = p0
val c2 = v0 - r * c1
@@ -214,14 +226,14 @@
* equation x(t) = c_1*e^(r_1*t) + c_2*e^(r_2*t)
*/
private fun estimateOverDamped(
- firstRoot: ComplexDouble,
- secondRoot: ComplexDouble,
+ firstRootReal: Double,
+ secondRootReal: Double,
p0: Double,
v0: Double,
delta: Double
): Double {
- val r1 = firstRoot.real
- val r2 = secondRoot.real
+ val r1 = firstRootReal
+ val r2 = secondRootReal
val c2 = (r1 * p0 - v0) / (r1 - r2)
val c1 = p0 - c2
@@ -293,8 +305,9 @@
// Applies Newton-Raphson's method to solve for the estimated time the spring mass system will
// last be at [delta].
private fun estimateDurationInternal(
- firstRoot: ComplexDouble,
- secondRoot: ComplexDouble,
+ firstRootReal: Double,
+ firstRootImaginary: Double,
+ secondRootReal: Double,
dampingRatio: Double,
initialVelocity: Double,
initialPosition: Double,
@@ -309,16 +322,16 @@
return (when {
dampingRatio > 1.0 ->
- estimateOverDamped(
- firstRoot = firstRoot,
- secondRoot = secondRoot,
+ estimateOverDamped(firstRootReal, secondRootReal, p0 = p0, v0 = v0, delta = delta)
+ dampingRatio < 1.0 ->
+ estimateUnderDamped(
+ firstRootReal,
+ firstRootImaginary,
v0 = v0,
p0 = p0,
delta = delta
)
- dampingRatio < 1.0 ->
- estimateUnderDamped(firstRoot = firstRoot, v0 = v0, p0 = p0, delta = delta)
- else -> estimateCriticallyDamped(firstRoot = firstRoot, v0 = v0, p0 = p0, delta = delta)
+ else -> estimateCriticallyDamped(firstRootReal, p0 = p0, v0 = v0, delta = delta)
} * 1000.0)
.toLong()
}
@@ -331,4 +344,4 @@
return x - fn(x) / fnPrime(x)
}
-@Suppress("NOTHING_TO_INLINE") private inline fun Double.isNotFinite() = !isFinite()
+@Suppress("NOTHING_TO_INLINE") private inline fun Double.isNotFinite() = !fastIsFinite()
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringSimulation.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringSimulation.kt
index f97c75f..e35eff2 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringSimulation.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SpringSimulation.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.animation.core
import androidx.compose.ui.util.packFloats
@@ -24,6 +26,17 @@
import kotlin.math.sin
import kotlin.math.sqrt
[email protected]
+internal value class Motion(val packedValue: Long) {
+ inline val value: Float
+ get() = unpackFloat1(packedValue)
+
+ inline val velocity: Float
+ get() = unpackFloat2(packedValue)
+}
+
+internal inline fun Motion(value: Float, velocity: Float) = Motion(packFloats(value, velocity))
+
/**
* Spring Simulation simulates spring physics, and allows you to query the motion (i.e. value and
* velocity) at certain time in the future based on the starting velocity and value.
@@ -39,52 +52,17 @@
* under-damped), the mass tends to overshoot, and return, and overshoot again. Without any damping
* (i.e. damping ratio = 0), the mass will oscillate forever.
*/
[email protected]
-internal value class Motion(val packedValue: Long) {
- val value: Float
- get() = unpackFloat1(packedValue)
-
- val velocity: Float
- get() = unpackFloat2(packedValue)
-
- /**
- * Returns a copy of this Motion instance optionally overriding the value or velocity parameters
- */
- fun copy(value: Float = this.value, velocity: Float = this.velocity) = Motion(value, velocity)
-}
-
-internal fun Motion(value: Float, velocity: Float) = Motion(packFloats(value, velocity))
-
-// This multiplier is used to calculate the velocity threshold given a certain value threshold.
-// The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
-// is a reasonable threshold.
-private const val VelocityThresholdMultiplier = 1000.0 / 16.0
-
-// Value to indicate an unset state.
-internal val UNSET = Float.MAX_VALUE
-
internal class SpringSimulation(var finalPosition: Float) {
-
// Natural frequency
private var naturalFreq = sqrt(Spring.StiffnessVeryLow.toDouble())
- // Indicates whether the spring has been initialized
- private var initialized = false
-
- // Intermediate values to simplify the spring function calculation per frame.
- private var gammaPlus: Double = 0.0
- private var gammaMinus: Double = 0.0
- private var dampedFreq: Double = 0.0
-
/** Stiffness of the spring. */
var stiffness: Float
set(value) {
if (stiffness <= 0) {
- throw IllegalArgumentException("Spring stiffness constant must be positive.")
+ throwIllegalArgumentException("Spring stiffness constant must be positive.")
}
naturalFreq = sqrt(value.toDouble())
- // All the intermediate values need to be recalculated.
- initialized = false
}
get() {
return (naturalFreq * naturalFreq).toFloat()
@@ -98,11 +76,9 @@
var dampingRatio: Float = Spring.DampingRatioNoBouncy
set(value) {
if (value < 0) {
- throw IllegalArgumentException("Damping ratio must be non-negative")
+ throwIllegalArgumentException("Damping ratio must be non-negative")
}
field = value
- // All the intermediate values need to be recalculated.
- initialized = false
}
/** ********************* Below are private APIs */
@@ -116,37 +92,6 @@
}
/**
- * Initialize the string by doing the necessary pre-calculation as well as some validity check
- * on the setup.
- *
- * @throws IllegalStateException if the final position is not yet set by the time the spring
- * animation has started
- */
- private fun init() {
- if (initialized) {
- return
- }
-
- if (finalPosition == UNSET) {
- throw IllegalStateException(
- "Error: Final position of the spring must be set before the animation starts"
- )
- }
-
- val dampingRatioSquared = dampingRatio * dampingRatio.toDouble()
- if (dampingRatio > 1) {
- // Over damping
- gammaPlus = (-dampingRatio * naturalFreq + naturalFreq * sqrt(dampingRatioSquared - 1))
- gammaMinus = (-dampingRatio * naturalFreq - naturalFreq * sqrt(dampingRatioSquared - 1))
- } else if (dampingRatio >= 0 && dampingRatio < 1) {
- // Under damping
- dampedFreq = naturalFreq * sqrt(1 - dampingRatioSquared)
- }
-
- initialized = true
- }
-
- /**
* Internal only call for Spring to calculate the spring position/velocity using an analytical
* approach.
*/
@@ -155,19 +100,24 @@
lastVelocity: Float,
timeElapsed: Long
): Motion {
- init()
-
val adjustedDisplacement = lastDisplacement - finalPosition
val deltaT = timeElapsed / 1000.0 // unit: seconds
+ val dampingRatioSquared = dampingRatio * dampingRatio.toDouble()
+ val r = -dampingRatio * naturalFreq
+
val displacement: Double
val currentVelocity: Double
+
if (dampingRatio > 1) {
+ // Over damping
+ val s = naturalFreq * sqrt(dampingRatioSquared - 1)
+ val gammaPlus = r + s
+ val gammaMinus = r - s
+
// Overdamped
- val coeffA =
- (adjustedDisplacement -
- ((gammaMinus * adjustedDisplacement - lastVelocity) / (gammaMinus - gammaPlus)))
val coeffB =
- ((gammaMinus * adjustedDisplacement - lastVelocity) / (gammaMinus - gammaPlus))
+ (gammaMinus * adjustedDisplacement - lastVelocity) / (gammaMinus - gammaPlus)
+ val coeffA = adjustedDisplacement - coeffB
displacement = (coeffA * exp(gammaMinus * deltaT) + coeffB * exp(gammaPlus * deltaT))
currentVelocity =
(coeffA * gammaMinus * exp(gammaMinus * deltaT) +
@@ -176,24 +126,21 @@
// Critically damped
val coeffA = adjustedDisplacement
val coeffB = lastVelocity + naturalFreq * adjustedDisplacement
- displacement = (coeffA + coeffB * deltaT) * exp(-naturalFreq * deltaT)
+ val nFdT = -naturalFreq * deltaT
+ displacement = (coeffA + coeffB * deltaT) * exp(nFdT)
currentVelocity =
- (((coeffA + coeffB * deltaT) * exp(-naturalFreq * deltaT) * (-naturalFreq)) +
- coeffB * exp(-naturalFreq * deltaT))
+ (((coeffA + coeffB * deltaT) * exp(nFdT) * (-naturalFreq)) + coeffB * exp(nFdT))
} else {
+ val dampedFreq = naturalFreq * sqrt(1 - dampingRatioSquared)
// Underdamped
val cosCoeff = adjustedDisplacement
- val sinCoeff =
- ((1 / dampedFreq) *
- (((dampingRatio * naturalFreq * adjustedDisplacement) + lastVelocity)))
- displacement =
- (exp(-dampingRatio * naturalFreq * deltaT) *
- ((cosCoeff * cos(dampedFreq * deltaT) + sinCoeff * sin(dampedFreq * deltaT))))
+ val sinCoeff = ((1 / dampedFreq) * (((-r * adjustedDisplacement) + lastVelocity)))
+ val dFdT = dampedFreq * deltaT
+ displacement = (exp(r * deltaT) * ((cosCoeff * cos(dFdT) + sinCoeff * sin(dFdT))))
currentVelocity =
- (displacement * (-naturalFreq) * dampingRatio +
- (exp(-dampingRatio * naturalFreq * deltaT) *
- ((-dampedFreq * cosCoeff * sin(dampedFreq * deltaT) +
- dampedFreq * sinCoeff * cos(dampedFreq * deltaT)))))
+ (displacement * r +
+ (exp(r * deltaT) *
+ ((-dampedFreq * cosCoeff * sin(dFdT) + dampedFreq * sinCoeff * cos(dFdT)))))
}
val newValue = (displacement + finalPosition).toFloat()
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index b61ed67..9ceaf13 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -707,7 +707,7 @@
animation.animationSpecDuration =
((1.0 - animation.start[0]) * totalDurationNanos).roundToLong()
}
- } else {
+ } else if (totalDurationNanos != 0L) {
// seekTo() called with a fraction. If an animation is running, we can just wait
// for the animation to change the value. The fraction may not be the best way
// to advance a regular animation.
@@ -1416,7 +1416,7 @@
init {
val visibilityThreshold: T? =
- visibilityThresholdMap.get(typeConverter)?.let {
+ VisibilityThresholdMap.get(typeConverter)?.let {
val vector = typeConverter.convertToVector(initialValue)
for (id in 0 until vector.size) {
vector[id] = it
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
index 75b4aba..60c04cf 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.animation.core
import androidx.compose.ui.geometry.Offset
@@ -24,6 +26,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastRoundToInt
/**
@@ -64,7 +67,7 @@
override val convertFromVector: (V) -> T
) : TwoWayConverter<T, V>
-internal fun lerp(start: Float, stop: Float, fraction: Float) =
+internal inline fun lerp(start: Float, stop: Float, fraction: Float) =
(start * (1 - fraction) + stop * fraction)
/** A [TwoWayConverter] that converts [Float] from and to [AnimationVector1D] */
@@ -157,8 +160,8 @@
{ AnimationVector2D(it.width.toFloat(), it.height.toFloat()) },
{
IntSize(
- width = it.v1.fastRoundToInt().coerceAtLeast(0),
- height = it.v2.fastRoundToInt().coerceAtLeast(0)
+ width = it.v1.fastRoundToInt().fastCoerceAtLeast(0),
+ height = it.v2.fastRoundToInt().fastCoerceAtLeast(0)
)
}
)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index d2e83ca..54f125b 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -22,6 +22,7 @@
import androidx.collection.MutableIntObjectMap
import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
import androidx.compose.animation.core.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.util.fastCoerceIn
import kotlin.jvm.JvmInline
import kotlin.math.min
@@ -183,7 +184,7 @@
* [VectorizedDurationBasedAnimationSpec].
*/
internal fun VectorizedDurationBasedAnimationSpec<*>.clampPlayTime(playTime: Long): Long {
- return (playTime - delayMillis).coerceIn(0, durationMillis.toLong())
+ return (playTime - delayMillis).fastCoerceIn(0, durationMillis.toLong())
}
/**
@@ -260,7 +261,7 @@
VectorizedKeyframeSpecElementInfo(
vectorValue = valueEasing.first,
easing = valueEasing.second,
- arcMode = ArcMode.Companion.ArcLinear
+ arcMode = ArcMode.ArcLinear
)
}
@@ -269,7 +270,7 @@
durationMillis = durationMillis,
delayMillis = delayMillis,
defaultEasing = LinearEasing,
- initialArcMode = ArcMode.Companion.ArcLinear
+ initialArcMode = ArcMode.ArcLinear
)
/**
@@ -277,23 +278,23 @@
*
* This will be used to do a faster lookup for the corresponding Easing curves.
*/
- private lateinit var modes: IntArray
- private lateinit var times: FloatArray
- private lateinit var valueVector: V
- private lateinit var velocityVector: V
+ private var modes: IntArray = EmptyIntArray
+ private var times: FloatArray = EmptyFloatArray
+ private var valueVector: V? = null
+ private var velocityVector: V? = null
// Objects for ArcSpline
- private lateinit var lastInitialValue: V
- private lateinit var lastTargetValue: V
- private lateinit var posArray: FloatArray
- private lateinit var slopeArray: FloatArray
- private lateinit var arcSpline: ArcSpline
+ private var lastInitialValue: V? = null
+ private var lastTargetValue: V? = null
+ private var posArray: FloatArray = EmptyFloatArray
+ private var slopeArray: FloatArray = EmptyFloatArray
+ private var arcSpline: ArcSpline = EmptyArcSpline
private fun init(initialValue: V, targetValue: V, initialVelocity: V) {
- var requiresArcSpline = ::arcSpline.isInitialized
+ var requiresArcSpline = arcSpline !== EmptyArcSpline
// Only need to initialize once
- if (!::valueVector.isInitialized) {
+ if (valueVector == null) {
valueVector = initialValue.newInstance()
velocityVector = initialVelocity.newInstance()
@@ -302,7 +303,7 @@
modes =
IntArray(timestamps.size) {
val mode = (keyframes[timestamps[it]]?.arcMode ?: initialArcMode)
- if (mode != ArcMode.Companion.ArcLinear) {
+ if (mode != ArcMode.ArcLinear) {
requiresArcSpline = true
}
@@ -316,7 +317,7 @@
// Initialize variables dependent on initial and/or target value
if (
- !::arcSpline.isInitialized ||
+ arcSpline === EmptyArcSpline ||
lastInitialValue != initialValue ||
lastTargetValue != targetValue
) {
@@ -332,26 +333,18 @@
// may change, and only if the keyframes does not overwrite it
val values =
Array(timestamps.size) {
- when (val timestamp = timestamps[it]) {
- // Start (zero) and end (durationMillis) may not have been declared in
- // keyframes
- 0 -> {
- if (!keyframes.contains(timestamp)) {
- FloatArray(dimensionCount, initialValue::get)
- } else {
- FloatArray(dimensionCount, keyframes[timestamp]!!.vectorValue::get)
- }
- }
- durationMillis -> {
- if (!keyframes.contains(timestamp)) {
- FloatArray(dimensionCount, targetValue::get)
- } else {
- FloatArray(dimensionCount, keyframes[timestamp]!!.vectorValue::get)
- }
- }
-
+ val timestamp = timestamps[it]
+ val info = keyframes[timestamp]
+ // Start (zero) and end (durationMillis) may not have been declared in
+ // keyframes
+ if (timestamp == 0 && info == null) {
+ FloatArray(dimensionCount) { i -> initialValue[i] }
+ } else if (timestamp == durationMillis && info == null) {
+ FloatArray(dimensionCount) { i -> targetValue[i] }
+ } else {
// All other values are guaranteed to exist
- else -> FloatArray(dimensionCount, keyframes[timestamp]!!.vectorValue::get)
+ val vectorValue = info!!.vectorValue
+ FloatArray(dimensionCount) { i -> vectorValue[i] }
}
}
arcSpline = ArcSpline(arcModes = modes, timePoints = times, y = values)
@@ -372,21 +365,28 @@
val clampedPlayTime = clampPlayTime(playTimeMillis).toInt()
// If there is a key frame defined with the given time stamp, return that value
- if (keyframes.contains(clampedPlayTime)) {
- return keyframes[clampedPlayTime]!!.vectorValue
+ val keyframe = keyframes[clampedPlayTime]
+ if (keyframe != null) {
+ return keyframe.vectorValue
}
if (clampedPlayTime >= durationMillis) {
return targetValue
- } else if (clampedPlayTime <= 0) return initialValue
+ } else if (clampedPlayTime <= 0) {
+ return initialValue
+ }
init(initialValue, targetValue, initialVelocity)
+ // Cannot be null after calling init()
+ val valueVector = valueVector!!
+
// ArcSpline is only initialized when necessary
- if (::arcSpline.isInitialized) {
+ if (arcSpline !== EmptyArcSpline) {
// ArcSpline requires eased play time in seconds
val easedTime = getEasedTime(clampedPlayTime)
+ val posArray = posArray
arcSpline.getPos(time = easedTime, v = posArray)
for (i in posArray.indices) {
valueVector[i] = posArray[i]
@@ -401,28 +401,18 @@
val easedTime = getEasedTimeFromIndex(index, clampedPlayTime, true)
val timestampStart = timestamps[index]
- val startValue: V =
- if (keyframes.contains(timestampStart)) {
- keyframes[timestampStart]!!.vectorValue
- } else {
- // Use initial value if it wasn't overwritten by the user
- // This is always the correct fallback assuming timestamps and keyframes were
- // populated
- // as expected
- initialValue
- }
+ val startKeyframe = keyframes[timestampStart]
+ // Use initial value if it wasn't overwritten by the user
+ // This is always the correct fallback assuming timestamps and keyframes were populated
+ // as expected
+ val startValue: V = startKeyframe?.vectorValue ?: initialValue
val timestampEnd = timestamps[index + 1]
- val endValue =
- if (keyframes.contains(timestampEnd)) {
- keyframes[timestampEnd]!!.vectorValue
- } else {
- // Use target value if it wasn't overwritten by the user
- // This is always the correct fallback assuming timestamps and keyframes were
- // populated
- // as expected
- targetValue
- }
+ val endKeyframe = keyframes[timestampEnd]
+ // Use target value if it wasn't overwritten by the user
+ // This is always the correct fallback assuming timestamps and keyframes were populated
+ // as expected
+ val endValue: V = endKeyframe?.vectorValue ?: targetValue
for (i in 0 until valueVector.size) {
valueVector[i] = lerp(startValue[i], endValue[i], easedTime)
@@ -444,9 +434,13 @@
init(initialValue, targetValue, initialVelocity)
+ // Cannot be null after calling init()
+ val velocityVector = velocityVector!!
+
// ArcSpline is only initialized when necessary
- if (::arcSpline.isInitialized) {
+ if (arcSpline !== EmptyArcSpline) {
val easedTime = getEasedTime(clampedPlayTime.toInt())
+ val slopeArray = slopeArray
arcSpline.getSlope(time = easedTime, v = slopeArray)
for (i in slopeArray.indices) {
velocityVector[i] = slopeArray[i]
@@ -505,7 +499,6 @@
}
}
-@OptIn(ExperimentalAnimationSpecApi::class)
internal data class VectorizedKeyframeSpecElementInfo<V : AnimationVector>(
val vectorValue: V,
val easing: Easing,
@@ -528,20 +521,20 @@
* Interpolates using a quarter of an Ellipse where the curve is "above" the center of the
* Ellipse.
*/
- public val ArcAbove: ArcMode = ArcMode(ArcSpline.ArcAbove)
+ public val ArcAbove: ArcMode = ArcMode(ArcSplineArcAbove)
/**
* Interpolates using a quarter of an Ellipse where the curve is "below" the center of the
* Ellipse.
*/
- public val ArcBelow: ArcMode = ArcMode(ArcSpline.ArcBelow)
+ public val ArcBelow: ArcMode = ArcMode(ArcSplineArcBelow)
/**
* An [ArcMode] that forces linear interpolation.
*
* You'll likely only use this mode within a keyframe.
*/
- public val ArcLinear: ArcMode = ArcMode(ArcSpline.ArcStartLinear)
+ public val ArcLinear: ArcMode = ArcMode(ArcSplineArcStartLinear)
}
}
@@ -560,10 +553,10 @@
targetValue: V,
initialVelocity: V
): V {
- if (playTimeNanos < delayMillis * MillisToNanos) {
- return initialValue
+ return if (playTimeNanos < delayMillis * MillisToNanos) {
+ initialValue
} else {
- return targetValue
+ targetValue
}
}
@@ -580,8 +573,6 @@
get() = 0
}
-private const val InfiniteIterations: Int = Int.MAX_VALUE
-
/**
* This animation takes another [VectorizedDurationBasedAnimationSpec] and plays it __infinite__
* times.
@@ -742,10 +733,10 @@
} else {
val postOffsetPlayTimeNanos = playTimeNanos + initialOffsetNanos
val repeatsCount = min(postOffsetPlayTimeNanos / durationNanos, iterations - 1L)
- if (repeatMode == RepeatMode.Restart || repeatsCount % 2 == 0L) {
- return postOffsetPlayTimeNanos - repeatsCount * durationNanos
+ return if (repeatMode == RepeatMode.Restart || repeatsCount % 2 == 0L) {
+ postOffsetPlayTimeNanos - repeatsCount * durationNanos
} else {
- return (repeatsCount + 1) * durationNanos - postOffsetPlayTimeNanos
+ (repeatsCount + 1) * durationNanos - postOffsetPlayTimeNanos
}
}
}
@@ -861,7 +852,7 @@
public val dampingRatio: Float,
public val stiffness: Float,
anims: Animations
-) : VectorizedFiniteAnimationSpec<V> by VectorizedFloatAnimationSpec<V>(anims) {
+) : VectorizedFiniteAnimationSpec<V> by VectorizedFloatAnimationSpec(anims) {
/**
* Creates a [VectorizedSpringSpec] that uses the same spring constants (i.e. [dampingRatio] and
@@ -889,17 +880,17 @@
dampingRatio: Float,
stiffness: Float
): Animations {
- if (visibilityThreshold != null) {
- return object : Animations {
+ return if (visibilityThreshold != null) {
+ object : Animations {
private val anims =
- (0 until visibilityThreshold.size).map { index ->
+ Array(visibilityThreshold.size) { index ->
FloatSpringSpec(dampingRatio, stiffness, visibilityThreshold[index])
}
override fun get(index: Int): FloatSpringSpec = anims[index]
}
} else {
- return object : Animations {
+ object : Animations {
private val anim = FloatSpringSpec(dampingRatio, stiffness)
override fun get(index: Int): FloatSpringSpec = anim
@@ -1029,17 +1020,18 @@
@Suppress("MethodNameUnits")
override fun getDurationNanos(initialValue: V, targetValue: V, initialVelocity: V): Long {
var maxDuration = 0L
- (0 until initialValue.size).forEach {
+ for (i in 0 until initialValue.size) {
maxDuration =
maxOf(
maxDuration,
- anims[it].getDurationNanos(
- initialValue[it],
- targetValue[it],
- initialVelocity[it]
- )
+ anims[i].getDurationNanos(initialValue[i], targetValue[i], initialVelocity[i])
)
}
return maxDuration
}
}
+
+private val EmptyIntArray: IntArray = IntArray(0)
+private val EmptyFloatArray: FloatArray = FloatArray(0)
+private val EmptyArcSpline =
+ ArcSpline(IntArray(2), FloatArray(2), arrayOf(FloatArray(2), FloatArray(2)))
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt
index ddc9938..b92b34a 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt
@@ -28,22 +28,22 @@
private val periodicBias: Float,
) : VectorizedDurationBasedAnimationSpec<V> {
// Objects initialized lazily once
- private lateinit var valueVector: V
- private lateinit var velocityVector: V
+ private var valueVector: V? = null
+ private var velocityVector: V? = null
// Time values passed to MonoSpline.
private lateinit var times: FloatArray
// Objects for MonoSpline
- private lateinit var monoSpline: MonoSpline
+ private var monoSpline: MonoSpline? = null
// [values] are not modified by MonoSpline so we can safely re-use it to re-instantiate it
- private lateinit var values: Array<FloatArray>
+ private var values: Array<FloatArray>? = null
private var lastInitialValue: V? = null
private var lastTargetValue: V? = null
private fun init(initialValue: V, targetValue: V, initialVelocity: V) {
// Only need to initialize once
- if (!::valueVector.isInitialized) {
+ if (valueVector == null) {
valueVector = initialValue.newInstance()
velocityVector = initialVelocity.newInstance()
@@ -52,9 +52,7 @@
// Need to re-initialize based on initial/target values
if (
- !::monoSpline.isInitialized ||
- lastInitialValue != initialValue ||
- lastTargetValue != targetValue
+ monoSpline == null || lastInitialValue != initialValue || lastTargetValue != targetValue
) {
val initialChanged = lastInitialValue != initialValue
val targetChanged = lastTargetValue != targetValue
@@ -63,39 +61,31 @@
val dimension = initialValue.size
- if (!::values.isInitialized) {
+ var values = values
+ if (values == null) {
values =
Array(timestamps.size) {
- when (val timestamp = timestamps[it]) {
- // Start (zero) and end (durationMillis) may not have been declared in
- // keyframes
- 0 -> {
- if (!keyframes.contains(timestamp)) {
- FloatArray(dimension, initialValue::get)
- } else {
- FloatArray(dimension, keyframes[timestamp]!!.first::get)
- }
- }
- durationMillis -> {
- if (!keyframes.contains(timestamp)) {
- FloatArray(dimension, targetValue::get)
- } else {
- FloatArray(dimension, keyframes[timestamp]!!.first::get)
- }
- }
-
- // All other values are guaranteed to exist
- else -> FloatArray(dimension, keyframes[timestamp]!!.first::get)
+ val timestamp = timestamps[it]
+ val keyframe = keyframes[timestamp]
+ if (timestamp == 0 && keyframe == null) {
+ FloatArray(dimension) { i -> initialValue[i] }
+ } else if (timestamp == durationMillis && keyframe == null) {
+ FloatArray(dimension) { i -> targetValue[i] }
+ } else {
+ val vectorValue = keyframe!!.first
+ FloatArray(dimension) { i -> vectorValue[i] }
}
}
+ this.values = values
} else {
// We can re-use most of the objects. Only the start and end may need to be replaced
if (initialChanged && !keyframes.contains(0)) {
- values[timestamps.binarySearch(0)] = FloatArray(dimension, initialValue::get)
+ val index = timestamps.binarySearch(0)
+ values[index] = FloatArray(dimension) { i -> initialValue[i] }
}
if (targetChanged && !keyframes.contains(durationMillis)) {
- values[timestamps.binarySearch(durationMillis)] =
- FloatArray(dimension, targetValue::get)
+ val index = timestamps.binarySearch(durationMillis)
+ values[index] = FloatArray(dimension) { i -> targetValue[i] }
}
}
monoSpline = MonoSpline(times, values, periodicBias)
@@ -111,13 +101,16 @@
val playTimeMillis = playTimeNanos / MillisToNanos
val clampedPlayTime = clampPlayTime(playTimeMillis).toInt()
// If there is a key frame defined with the given time stamp, return that value
- if (keyframes.containsKey(clampedPlayTime)) {
- return keyframes[clampedPlayTime]!!.first
+ val keyframe = keyframes[clampedPlayTime]
+ if (keyframe != null) {
+ return keyframe.first
}
if (clampedPlayTime >= durationMillis) {
return targetValue
- } else if (clampedPlayTime <= 0) return initialValue
+ } else if (clampedPlayTime <= 0) {
+ return initialValue
+ }
init(initialValue, targetValue, initialVelocity)
@@ -125,7 +118,8 @@
// time range at every call
val index = findEntryForTimeMillis(clampedPlayTime)
- monoSpline.getPos(
+ val valueVector = valueVector!!
+ monoSpline!!.getPos(
index = index,
time = getEasedTimeFromIndex(index, clampedPlayTime),
v = valueVector
@@ -141,9 +135,6 @@
): V {
val playTimeMillis = playTimeNanos / MillisToNanos
val clampedPlayTime = clampPlayTime(playTimeMillis).toInt()
- if (clampedPlayTime < 0) {
- return initialVelocity
- }
init(initialValue, targetValue, initialVelocity)
@@ -151,7 +142,8 @@
// time range at every call
val index = findEntryForTimeMillis(clampedPlayTime)
- monoSpline.getSlope(
+ val velocityVector = velocityVector!!
+ monoSpline!!.getSlope(
index = index,
time = getEasedTimeFromIndex(index, clampedPlayTime),
v = velocityVector
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
index 8852c65..6920792 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
@@ -28,7 +28,7 @@
private const val DpVisibilityThreshold = 0.1f
private const val PxVisibilityThreshold = 0.5f
-private val rectVisibilityThreshold =
+private val RectVisibilityThreshold =
Rect(PxVisibilityThreshold, PxVisibilityThreshold, PxVisibilityThreshold, PxVisibilityThreshold)
/**
@@ -93,11 +93,14 @@
* to stop when the value is close enough to the target.
*/
public val Rect.Companion.VisibilityThreshold: Rect
- get() = rectVisibilityThreshold
+ get() = RectVisibilityThreshold
// TODO: Add Dp.DefaultAnimation = spring<Dp>(visibilityThreshold = Dp.VisibilityThreshold)
-
-internal val visibilityThresholdMap: Map<TwoWayConverter<*, *>, Float> =
+// The floats coming out of this map are fed to APIs that expect objects (generics), so it's
+// better to store them as boxed floats here instead of causing unboxing/boxing every time
+// the values are read out and forwarded to other APIs
+@Suppress("PrimitiveInCollection")
+internal val VisibilityThresholdMap: Map<TwoWayConverter<*, *>, Float> =
mapOf(
Int.VectorConverter to 1f,
IntSize.VectorConverter to 1f,
diff --git a/compose/animation/animation/api/current.txt b/compose/animation/animation/api/current.txt
index e3b1efc..05c53bd 100644
--- a/compose/animation/animation/api/current.txt
+++ b/compose/animation/animation/api/current.txt
@@ -5,6 +5,10 @@
method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> defaultDecayAnimationSpec();
}
+ public final class AnimateBoundsModifierKt {
+ method @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi public static androidx.compose.ui.Modifier animateBounds(androidx.compose.ui.Modifier, androidx.compose.ui.layout.LookaheadScope lookaheadScope, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.BoundsTransform boundsTransform, optional boolean animateMotionFrameOfReference);
+ }
+
public final class AnimatedContentKt {
method @androidx.compose.runtime.Composable public static <S> void AnimatedContent(androidx.compose.animation.core.Transition<S>, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<S>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, optional kotlin.jvm.functions.Function1<? super S,? extends java.lang.Object?> contentKey, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super S,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static <S> void AnimatedContent(S targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<S>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, optional String label, optional kotlin.jvm.functions.Function1<? super S,? extends java.lang.Object?> contentKey, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super S,kotlin.Unit> content);
diff --git a/compose/animation/animation/api/restricted_current.txt b/compose/animation/animation/api/restricted_current.txt
index e3b1efc..05c53bd 100644
--- a/compose/animation/animation/api/restricted_current.txt
+++ b/compose/animation/animation/api/restricted_current.txt
@@ -5,6 +5,10 @@
method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> defaultDecayAnimationSpec();
}
+ public final class AnimateBoundsModifierKt {
+ method @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi public static androidx.compose.ui.Modifier animateBounds(androidx.compose.ui.Modifier, androidx.compose.ui.layout.LookaheadScope lookaheadScope, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.BoundsTransform boundsTransform, optional boolean animateMotionFrameOfReference);
+ }
+
public final class AnimatedContentKt {
method @androidx.compose.runtime.Composable public static <S> void AnimatedContent(androidx.compose.animation.core.Transition<S>, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<S>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, optional kotlin.jvm.functions.Function1<? super S,? extends java.lang.Object?> contentKey, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super S,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static <S> void AnimatedContent(S targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<S>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, optional String label, optional kotlin.jvm.functions.Function1<? super S,? extends java.lang.Object?> contentKey, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super S,kotlin.Unit> content);
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index c8b339c..4f00d4f 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -38,7 +38,9 @@
import androidx.compose.animation.demos.layoutanimation.ScreenTransitionDemo
import androidx.compose.animation.demos.layoutanimation.ShrineCartDemo
import androidx.compose.animation.demos.lookahead.AnimateBoundsModifierDemo
+import androidx.compose.animation.demos.lookahead.AnimateBoundsOnFloatingToolbarDemo
import androidx.compose.animation.demos.lookahead.CraneDemo
+import androidx.compose.animation.demos.lookahead.LookaheadInScrollingColumn
import androidx.compose.animation.demos.lookahead.LookaheadLayoutWithAlignmentLinesDemo
import androidx.compose.animation.demos.lookahead.LookaheadSamplesDemo
import androidx.compose.animation.demos.lookahead.LookaheadWithAnimatedContentSize
@@ -144,6 +146,10 @@
},
ComposableDemo("Lookahead With Tab Row") { LookaheadWithTabRowDemo() },
ComposableDemo("Lookahead With Scaffold") { LookaheadWithScaffold() },
+ ComposableDemo("Lookahead With Scroll") { LookaheadInScrollingColumn() },
+ ComposableDemo("Floating Toolbar w/ AnimateBounds") {
+ AnimateBoundsOnFloatingToolbarDemo()
+ },
)
),
DemoCategory(
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt
deleted file mode 100644
index 87e9e5b..0000000
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt
+++ /dev/null
@@ -1,179 +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.compose.animation.demos.lookahead
-
-import androidx.compose.animation.core.AnimationVector2D
-import androidx.compose.animation.core.DeferredTargetAnimation
-import androidx.compose.animation.core.ExperimentalAnimatableApi
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.spring
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.approachLayout
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.round
-import kotlinx.coroutines.CoroutineScope
-
-context(LookaheadScope)
-@OptIn(ExperimentalAnimatableApi::class)
-fun Modifier.animateBounds(
- modifier: Modifier = Modifier,
- sizeAnimationSpec: FiniteAnimationSpec<IntSize> =
- spring(Spring.DampingRatioNoBouncy, Spring.StiffnessMediumLow),
- positionAnimationSpec: FiniteAnimationSpec<IntOffset> =
- spring(Spring.DampingRatioNoBouncy, Spring.StiffnessMediumLow),
- debug: Boolean = false,
-) = composed {
- val outerOffsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val outerSizeAnimation = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
-
- val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val sizeAnimation = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
-
- val coroutineScope = rememberCoroutineScope()
-
- // The measure logic in `approachLayout` is skipped in the lookahead pass, as
- // approachLayout is expected to produce intermediate stages of a layout transform.
- // When the measure block is invoked after lookahead pass, the lookahead size of the
- // child will be accessible as a parameter to the measure block.
- this.drawWithContent {
- drawContent()
- if (debug) {
- // val offset = outerOffsetAnimation.pendingTarget!! -
- // outerOffsetAnimation.value!!
- // translate(
- // offset.x.toFloat(), offset.y.toFloat()
- // ) {
- // drawRect(Color.Black.copy(alpha = 0.5f), style = Stroke(10f))
- // }
- }
- }
- .approachLayout(
- isMeasurementApproachInProgress = {
- outerSizeAnimation.updateTarget(it, coroutineScope, sizeAnimationSpec)
- !outerSizeAnimation.isIdle
- },
- isPlacementApproachInProgress = {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- outerOffsetAnimation.updateTarget(
- target.round(),
- coroutineScope,
- positionAnimationSpec
- )
- !outerOffsetAnimation.isIdle
- }
- ) { measurable, constraints ->
- val (w, h) =
- outerSizeAnimation.updateTarget(
- lookaheadSize,
- coroutineScope,
- sizeAnimationSpec,
- )
- measurable.measure(constraints).run {
- layout(w, h) {
- with(coroutineScope) {
- val (x, y) =
- outerOffsetAnimation.updateTargetBasedOnCoordinates(
- positionAnimationSpec
- )
- place(x, y)
- }
- }
- }
- }
- .then(modifier)
- .drawWithContent {
- drawContent()
- if (debug) {
- // val offset = offsetAnimation.pendingTarget!! -
- // offsetAnimation.value!!
- // translate(
- // offset.x.toFloat(), offset.y.toFloat()
- // ) {
- // drawRect(Color.Green.copy(alpha = 0.5f), style = Stroke(10f))
- // }
- }
- }
- .approachLayout(
- isMeasurementApproachInProgress = {
- sizeAnimation.updateTarget(it, coroutineScope, sizeAnimationSpec)
- !sizeAnimation.isIdle
- },
- isPlacementApproachInProgress = {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- offsetAnimation.updateTarget(target.round(), coroutineScope, positionAnimationSpec)
- !offsetAnimation.isIdle
- }
- ) { measurable, _ ->
- // When layout changes, the lookahead pass will calculate a new final size for the
- // child modifier. This lookahead size can be used to animate the size
- // change, such that the animation starts from the current size and gradually
- // change towards `lookaheadSize`.
- val (width, height) =
- sizeAnimation.updateTarget(
- lookaheadSize,
- coroutineScope,
- sizeAnimationSpec,
- )
- // Creates a fixed set of constraints using the animated size
- val animatedConstraints = Constraints.fixed(width, height)
- // Measure child/children with animated constraints.
- val placeable = measurable.measure(animatedConstraints)
- layout(placeable.width, placeable.height) {
- val (x, y) =
- with(coroutineScope) {
- offsetAnimation.updateTargetBasedOnCoordinates(
- positionAnimationSpec,
- )
- }
- placeable.place(x, y)
- }
- }
-}
-
-context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
-@OptIn(ExperimentalAnimatableApi::class)
-internal fun DeferredTargetAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
- animationSpec: FiniteAnimationSpec<IntOffset>,
-): IntOffset {
- coordinates?.let { coordinates ->
- with(this@PlacementScope) {
- val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
- val animOffset =
- updateTarget(
- targetOffset.round(),
- this@CoroutineScope,
- animationSpec,
- )
- val current =
- lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
- return (animOffset - current)
- }
- }
-
- return IntOffset.Zero
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt
index 5ebb842..787045d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -39,6 +41,7 @@
import androidx.compose.ui.unit.dp
import kotlin.random.Random
+@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun AnimateBoundsModifierDemo() {
var height by remember { mutableIntStateOf(200) }
@@ -63,18 +66,27 @@
Box(Modifier.fillMaxHeight(0.5f).fillMaxSize()) {
Box(
Modifier.background(Color.Gray)
- .animateBounds(Modifier.padding(left.dp, top.dp, right.dp, bottom.dp))
+ .animateBounds(
+ this@LookaheadScope,
+ Modifier.padding(left.dp, top.dp, right.dp, bottom.dp)
+ )
.background(Color.Red)
.fillMaxSize()
)
}
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
Box(
- Modifier.animateBounds(Modifier.weight(weight).height(height.dp))
+ Modifier.animateBounds(
+ this@LookaheadScope,
+ Modifier.weight(weight).height(height.dp)
+ )
.background(Color(0xffa2d2ff), RoundedCornerShape(5.dp))
)
Box(
- Modifier.animateBounds(Modifier.weight(1f).height(height.dp))
+ Modifier.animateBounds(
+ this@LookaheadScope,
+ Modifier.weight(1f).height(height.dp)
+ )
.background(Color(0xfffff3b0))
)
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsOnFloatingToolbar.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsOnFloatingToolbar.kt
new file mode 100644
index 0000000..0221499
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsOnFloatingToolbar.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.demos.visualaid.EasingItemDemo
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material.icons.outlined.Share
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentWithReceiverOf
+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.layout.LookaheadScope
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.coerceAtLeast
+import androidx.compose.ui.unit.dp
+
+/**
+ * Example using [animateBounds] with nested movable content.
+ *
+ * Animates an Icon component from a Toolbar to a FAB position, the toolbar is also animated to hide
+ * it under the FAB.
+ */
+@Preview
+@Composable
+fun AnimateBoundsOnFloatingToolbarDemo() {
+ Box(Modifier.fillMaxSize()) {
+ Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+ val sampleText = remember { LoremIpsum().values.first() }
+ Text(
+ text = "Click on the Toolbar to animate",
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.h6
+ )
+ Text(text = sampleText)
+ }
+ FloatingFabToolbar(
+ Modifier.align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .padding(8.dp)
+ .padding(bottom = 24.dp)
+ )
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+private fun FloatingFabToolbar(modifier: Modifier = Modifier) {
+ var mode by remember { mutableStateOf(FabToolbarMode.Toolbar) }
+
+ val animationDuration = 600
+ val animEasing = EasingItemDemo.EmphasizedEasing.function
+
+ val editIconPadding by
+ animateDpAsState(
+ targetValue = if (mode == FabToolbarMode.Fab) 12.dp else 0.dp,
+ animationSpec = tween(animationDuration, easing = animEasing),
+ label = "Edit Icon Padding"
+ )
+
+ val myEditIcon = remember {
+ movableContentWithReceiverOf<LookaheadScope, Modifier> { iconModifier ->
+ Box(
+ modifier =
+ iconModifier
+ .let {
+ if (mode == FabToolbarMode.Toolbar) {
+ it.fillMaxSize()
+ } else {
+ it.aspectRatio(1f, matchHeightConstraintsFirst = true)
+ }
+ }
+ .animateBounds(
+ lookaheadScope = this,
+ modifier = Modifier,
+ boundsTransform = { _, _ ->
+ tween(animationDuration, easing = animEasing)
+ },
+ ),
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Edit,
+ contentDescription = null,
+ tint = MaterialTheme.colors.onPrimary,
+ modifier =
+ Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(16.dp))
+ .fillMaxSize()
+ .padding(editIconPadding.coerceAtLeast(0.dp))
+ )
+ }
+ }
+ }
+
+ // Toolbar container + Toolbar
+ val myToolbar = remember {
+ movableContentWithReceiverOf<LookaheadScope, Modifier> { toolbarMod ->
+ // Toolbar container
+ Box(
+ modifier =
+ toolbarMod
+ .animateBounds(
+ lookaheadScope = this,
+ boundsTransform = { _, _ ->
+ tween(animationDuration, easing = animEasing)
+ },
+ )
+ .background(MaterialTheme.colors.background, RoundedCornerShape(50))
+ .let {
+ if (mode == FabToolbarMode.Toolbar) {
+ // Respect toolbar content size when in Toolbar mode
+ it.wrapContentSize().padding(8.dp)
+ } else {
+ // Resize the container so that it doesn't go beyond the Fab box,
+ // clipping the toolbar as needed
+ it.fillMaxWidth().wrapContentHeight().padding(8.dp)
+ }
+ }
+ ) {
+ // Toolbar - Fixed Size
+ Row(
+ modifier = Modifier.align(Alignment.Center),
+ horizontalArrangement = Arrangement.spacedBy(26.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val iconSize = DpSize(30.dp, 20.dp)
+ Icon(
+ imageVector = Icons.Outlined.Share,
+ contentDescription = "Share",
+ modifier = Modifier.size(iconSize)
+ )
+ Icon(
+ imageVector = Icons.Outlined.FavoriteBorder,
+ contentDescription = "Favorite",
+ modifier = Modifier.size(iconSize)
+ )
+ Box(modifier = Modifier.size(iconSize)) {
+ // Slot for the Edit Icon when position on the toolbar
+ if (mode == FabToolbarMode.Toolbar) {
+ myEditIcon(Modifier.align(Alignment.Center))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ LookaheadScope {
+ Box(
+ modifier.clickable {
+ mode =
+ if (mode == FabToolbarMode.Fab) {
+ FabToolbarMode.Toolbar
+ } else {
+ FabToolbarMode.Fab
+ }
+ }
+ ) {
+ Box(
+ Modifier.align(Alignment.Center),
+ ) {
+ // Slot 0 - Toolbar position
+ if (mode == FabToolbarMode.Toolbar) {
+ // The Toolbar container should also place the Edit Icon at this state
+ myToolbar(Modifier.align(Alignment.Center))
+ }
+ }
+ Box(Modifier.size(80.dp).align(Alignment.CenterEnd)) {
+ // Slot 1 - Fab position
+ if (mode == FabToolbarMode.Fab) {
+ // We pull out the Edit Icon in this state
+ myToolbar(Modifier.align(Alignment.Center))
+ myEditIcon(Modifier.align(Alignment.Center))
+ }
+ }
+ }
+ }
+}
+
+enum class FabToolbarMode {
+ Fab,
+ Toolbar
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadInScrollingColumn.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadInScrollingColumn.kt
new file mode 100644
index 0000000..69173ea
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadInScrollingColumn.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.demos.layoutanimation.turquoiseColors
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentWithReceiverOf
+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.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+
+/**
+ * A simple example showing how [animateBounds] behaves when animating from/to a scrolling layout.
+ *
+ * Note that despite the items position changing due to the scroll, it does not affect or trigger an
+ * animation.
+ */
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+@Preview
+fun LookaheadInScrollingColumn() {
+ var displayInScroller by remember { mutableStateOf(false) }
+ val movableContent = remember {
+ movableContentWithReceiverOf<LookaheadScope> {
+ Box(
+ Modifier.zIndex(1f)
+ .let {
+ if (displayInScroller) {
+ it.height(80.dp).fillMaxWidth()
+ } else {
+ it.size(150.dp)
+ }
+ }
+ .animateBounds(
+ lookaheadScope = this@movableContentWithReceiverOf,
+ boundsTransform = { _, _ ->
+ spring(stiffness = 50f, visibilityThreshold = Rect.VisibilityThreshold)
+ }
+ )
+ .clickable { displayInScroller = !displayInScroller }
+ .background(color, RoundedCornerShape(10.dp))
+ )
+ }
+ }
+
+ Box(Modifier.fillMaxSize()) {
+ LookaheadScope {
+ Column(
+ modifier =
+ Modifier.fillMaxSize().verticalScroll(rememberScrollState(0)).padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text("Click Yellow box to animate to/from scrolling list.")
+ repeat(6) {
+ Box(
+ Modifier.fillMaxWidth()
+ .background(turquoiseColors[it % 6], RoundedCornerShape(10.dp))
+ .height(80.dp)
+ )
+ }
+ if (displayInScroller) {
+ movableContent()
+ }
+ repeat(6) {
+ Box(
+ Modifier.animateBounds(lookaheadScope = this@LookaheadScope)
+ .background(turquoiseColors[it % 6], RoundedCornerShape(10.dp))
+ .height(80.dp)
+ .fillMaxWidth()
+ )
+ }
+ }
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) {
+ if (!displayInScroller) {
+ movableContent()
+ }
+ }
+ }
+ }
+}
+
+private val color = Color(0xffffcc5c)
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt
index f0375d7..8af930f 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt
@@ -16,17 +16,56 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
+import androidx.compose.animation.core.ExperimentalAnimatableApi
+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.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
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.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
-import androidx.compose.ui.samples.approachLayoutSample
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
@Preview
@Composable
fun LookaheadSamplesDemo() {
Column {
- approachLayoutSample()
+ ApproachLayoutSample0()
LookaheadLayoutCoordinatesSample()
}
}
+
+@OptIn(ExperimentalAnimatableApi::class, ExperimentalSharedTransitionApi::class)
+@Composable
+public fun ApproachLayoutSample0() {
+ var fullWidth by remember { mutableStateOf(false) }
+ LookaheadScope {
+ Row(
+ (if (fullWidth) Modifier.fillMaxWidth() else Modifier.width(100.dp))
+ .height(200.dp)
+ // Use the custom modifier created above to animate the constraints passed
+ // to the child, and therefore resize children in an animation.
+ .animateBounds(this@LookaheadScope)
+ .clickable { fullWidth = !fullWidth }
+ ) {
+ Box(
+ Modifier.weight(1f).fillMaxHeight().background(Color(0xffff6f69)),
+ )
+ Box(Modifier.weight(2f).fillMaxHeight().background(Color(0xffffcc5c)))
+ }
+ }
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimatedContentSize.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimatedContentSize.kt
index 36f8fec..34c468d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimatedContentSize.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithAnimatedContentSize.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.demos.gesture.pastelColors
import androidx.compose.foundation.background
@@ -34,6 +36,7 @@
import androidx.compose.ui.zIndex
import kotlinx.coroutines.delay
+@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@Composable
fun LookaheadWithAnimatedContentSize() {
@@ -56,7 +59,12 @@
Box(Modifier.fillMaxWidth().height(200.dp).background(Color.White))
}
}
- Box(Modifier.animateBounds().fillMaxWidth().height(100.dp).background(pastelColors[1]))
+ Box(
+ Modifier.animateBounds(this@LookaheadScope)
+ .fillMaxWidth()
+ .height(100.dp)
+ .background(pastelColors[1])
+ )
}
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt
index 24f929f..48ffde5 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithBoxWithConstraints.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.demos.gesture.pastelColors
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -46,6 +48,7 @@
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalSharedTransitionApi::class)
@Suppress("UnusedBoxWithConstraintsScope")
@Composable
fun LookaheadWithBoxWithConstraints() {
@@ -59,6 +62,7 @@
Column(
Modifier.fillMaxHeight()
.animateBounds(
+ this@LookaheadScope,
if (halfSize) Modifier.fillMaxSize(0.5f) else Modifier.fillMaxWidth()
)
.background(pastelColors[2]),
@@ -96,7 +100,10 @@
BoxWithConstraints {
Column(
if (animate) {
- Modifier.animateBounds(Modifier.fillMaxWidth())
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ Modifier.fillMaxWidth()
+ )
} else {
Modifier.fillMaxWidth()
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt
index 6e8296d..d3dd741 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt
@@ -14,8 +14,12 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
@@ -76,17 +80,26 @@
) {
Box(
Modifier.height(50.dp)
- .animateBounds(Modifier.fillMaxWidth(if (isHorizontal) 0.4f else 1f))
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ Modifier.fillMaxWidth(if (isHorizontal) 0.4f else 1f)
+ )
.background(colors[0], RoundedCornerShape(10))
)
Box(
Modifier.height(50.dp)
- .animateBounds(Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f))
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f)
+ )
.background(colors[1], RoundedCornerShape(10))
)
Box(
Modifier.height(50.dp)
- .animateBounds(Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f))
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f)
+ )
.background(colors[2], RoundedCornerShape(10))
)
}
@@ -136,12 +149,19 @@
var expanded by remember { mutableStateOf(false) }
Box(
modifier =
- Modifier.animateBounds(Modifier.widthIn(max = 600.dp)).background(Color.Red)
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ Modifier.widthIn(max = 600.dp)
+ )
+ .background(Color.Red)
) {
val height = animateDpAsState(targetValue = if (expanded) 500.dp else 300.dp)
Box(
modifier =
- Modifier.animateBounds(Modifier.fillMaxWidth().height(height.value))
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ Modifier.fillMaxWidth().height(height.value)
+ )
.clickable { expanded = !expanded }
)
}
@@ -155,8 +175,8 @@
modifier =
Modifier.size(200.dp)
.animateBounds(
+ lookaheadScope = this@LookaheadScope,
Modifier.wrapContentWidth().heightIn(min = 156.dp),
- debug = true
)
.background(Color.Blue)
) {
@@ -166,8 +186,8 @@
modifier =
Modifier.size(200.dp)
.animateBounds(
+ lookaheadScope = this@LookaheadScope,
Modifier.wrapContentWidth().heightIn(min = 156.dp),
- debug = true
)
.background(Color.Yellow)
) {
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt
index c359741..a38e88b 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -43,6 +45,7 @@
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun LookaheadWithIntrinsicsDemo() {
Column {
@@ -64,6 +67,7 @@
) {
Box(
Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
if (isWide) Modifier.width(300.dp) else Modifier.width(150.dp)
)
.height(50.dp)
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt
index 6f6dc17b..0c0acba 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithLazyColumn.kt
@@ -17,8 +17,11 @@
package androidx.compose.animation.demos.lookahead
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.animation.demos.R
import androidx.compose.animation.demos.gesture.pastelColors
@@ -47,6 +50,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LookaheadScope
@@ -54,6 +58,7 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@Composable
fun LookaheadWithLazyColumn() {
@@ -74,10 +79,7 @@
LookaheadScope {
val title = remember {
movableContentOf {
- Text(
- names[index],
- Modifier.padding(20.dp).animateBounds(Modifier)
- )
+ Text(names[index], Modifier.padding(20.dp).animateBounds(this))
}
}
val image = remember {
@@ -89,9 +91,16 @@
modifier =
Modifier.padding(10.dp)
.animateBounds(
+ this,
if (expanded) Modifier.fillMaxWidth()
else Modifier.size(80.dp),
- spring(stiffness = Spring.StiffnessLow)
+ { _, _ ->
+ spring(
+ Spring.DampingRatioNoBouncy,
+ Spring.StiffnessLow,
+ Rect.VisibilityThreshold
+ )
+ }
)
.clip(RoundedCornerShape(5.dp)),
contentScale =
@@ -108,10 +117,17 @@
modifier =
Modifier.padding(10.dp)
.animateBounds(
+ lookaheadScope = this,
if (expanded)
Modifier.fillMaxWidth().aspectRatio(1f)
else Modifier.size(80.dp),
- spring(stiffness = Spring.StiffnessLow)
+ { _, _ ->
+ spring(
+ Spring.DampingRatioNoBouncy,
+ Spring.StiffnessLow,
+ Rect.VisibilityThreshold
+ )
+ }
)
.background(
Color.LightGray,
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
index 2e82042..a02fc9a 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.core.DeferredTargetAnimation
import androidx.compose.animation.core.ExperimentalAnimatableApi
import androidx.compose.animation.core.VectorConverter
@@ -58,6 +60,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
+@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@Composable
fun LookaheadWithMovableContentDemo() {
@@ -91,7 +94,7 @@
Modifier.padding(15.dp)
.height(80.dp)
.fillMaxWidth(weight)
- .animateBoundsInScope()
+ .animateBounds(lookaheadScope = this@movableContentWithReceiverOf)
.background(color, RoundedCornerShape(20)),
contentAlignment = Alignment.Center
) {
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithPopularBoxWithConstraintsUsage.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithPopularBoxWithConstraintsUsage.kt
index 7181a1b..f38a073 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithPopularBoxWithConstraintsUsage.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithPopularBoxWithConstraintsUsage.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.demos.R
import androidx.compose.animation.demos.gesture.pastelColors
@@ -50,6 +52,7 @@
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
+@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@Composable
fun LookaheadWithPopularBoxWithConstraintsUsage() {
@@ -67,7 +70,7 @@
LookaheadScope {
Box(
Modifier.fillMaxSize()
- .animateBounds(Modifier.padding(padding))
+ .animateBounds(this, Modifier.padding(padding))
.background(pastelColors[3])
) {
DetailsContent()
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt
index c2ee5bd..08f1c8a 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.background
@@ -77,6 +79,7 @@
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84))
+@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@Composable
fun LookaheadWithScaffold() {
@@ -91,7 +94,10 @@
Box(
Modifier.fillMaxHeight()
.background(Color.Gray)
- .animateBounds(if (hasPadding) Modifier.padding(bottom = 300.dp) else Modifier)
+ .animateBounds(
+ this@LookaheadScope,
+ if (hasPadding) Modifier.padding(bottom = 300.dp) else Modifier
+ )
) {
var state by remember { mutableIntStateOf(0) }
val titles =
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
index 098f6a2..2ea3bd2 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -107,10 +109,11 @@
}
context(LookaheadScope)
+@OptIn(ExperimentalSharedTransitionApi::class)
private fun Modifier.conditionallyAnimateBounds(
shouldAnimate: Boolean,
modifier: Modifier = Modifier
-) = if (shouldAnimate) this.animateBounds(modifier) else this.then(modifier)
+) = if (shouldAnimate) this.animateBounds(this@LookaheadScope, modifier) else this.then(modifier)
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff2a9d84), Color(0xff264653))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt
index 07df45b..a83f31c 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -50,6 +52,7 @@
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
+@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@Composable
fun LookaheadWithTabRowDemo() {
@@ -63,7 +66,10 @@
}
Column(
Modifier.fillMaxWidth()
- .animateBounds(if (isWide) Modifier else Modifier.padding(end = 100.dp))
+ .animateBounds(
+ this@LookaheadScope,
+ if (isWide) Modifier else Modifier.padding(end = 100.dp)
+ )
.fillMaxHeight()
.background(Color(0xFFfffbd0))
) {
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
index 1d877f5..1b94fc1 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.DeferredTargetAnimation
import androidx.compose.animation.core.ExperimentalAnimatableApi
+import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
@@ -36,6 +37,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
@@ -43,6 +45,7 @@
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
+import kotlinx.coroutines.CoroutineScope
@Composable
fun SceneHost(modifier: Modifier = Modifier, content: @Composable SceneScope.() -> Unit) {
@@ -149,3 +152,26 @@
}
}
}
+
+context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
+@OptIn(ExperimentalAnimatableApi::class)
+internal fun DeferredTargetAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+ animationSpec: FiniteAnimationSpec<IntOffset>,
+): IntOffset {
+ coordinates?.let { coordinates ->
+ with(this@PlacementScope) {
+ val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+ val animOffset =
+ updateTarget(
+ targetOffset.round(),
+ this@CoroutineScope,
+ animationSpec,
+ )
+ val current =
+ lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
+ return (animOffset - current)
+ }
+ }
+
+ return IntOffset.Zero
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt
index 2717e49..497f9b2 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt
@@ -16,6 +16,8 @@
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -137,11 +139,13 @@
}
}
+@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun Root(state: DisplayState) {
SceneHost {
Row(
Modifier.animateBounds(
+ this,
if (state == DisplayState.Compact) {
Modifier.wrapContentSize(align = Alignment.TopStart, unbounded = true)
.requiredWidth(800.dp)
@@ -322,10 +326,12 @@
}
}
+@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SceneScope.NavRail(state: DisplayState) {
Column(
Modifier.animateBounds(
+ this,
if (state == DisplayState.Tablet) Modifier.width(200.dp)
else Modifier.width(IntrinsicSize.Min)
)
diff --git a/compose/animation/animation/samples/build.gradle b/compose/animation/animation/samples/build.gradle
index a3be858..66c33e58 100644
--- a/compose/animation/animation/samples/build.gradle
+++ b/compose/animation/animation/samples/build.gradle
@@ -36,11 +36,11 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(project(":compose:animation:animation"))
- implementation("androidx.compose.foundation:foundation:1.2.1")
- implementation("androidx.compose.material:material:1.2.1")
- implementation("androidx.compose.material:material-icons-core:1.6.7")
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation("androidx.compose.ui:ui-text:1.2.1")
+ implementation("androidx.compose.foundation:foundation:1.6.8")
+ implementation("androidx.compose.material:material:1.6.8")
+ implementation("androidx.compose.material:material-icons-core:1.6.8")
+ implementation("androidx.compose.runtime:runtime:1.6.8")
+ implementation("androidx.compose.ui:ui-text:1.6.8")
}
androidx {
diff --git a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimateBoundsModifierSample.kt b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimateBoundsModifierSample.kt
new file mode 100644
index 0000000..70dbcfa
--- /dev/null
+++ b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimateBoundsModifierSample.kt
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.BoundsTransform
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.animateBounds
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.keyframesWithSpline
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentWithReceiverOf
+import androidx.compose.runtime.mutableIntStateOf
+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.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastForEach
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+private fun AnimateBounds_animateOnContentChange() {
+ // Example where the change in content triggers the layout change on the item with animateBounds
+ val textShort = remember { "Foo ".repeat(10) }
+ val textLong = remember { "Bar ".repeat(50) }
+
+ var toggle by remember { mutableStateOf(true) }
+
+ LookaheadScope {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable { toggle = !toggle },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = if (toggle) textShort else textLong,
+ modifier =
+ Modifier.fillMaxWidth(0.7f)
+ .background(Color.LightGray)
+ .animateBounds(this@LookaheadScope)
+ .padding(10.dp),
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+private fun AnimateBounds_withLayoutModifier() {
+ // Example showing the difference between providing a Layout Modifier as a parameter of
+ // `animateBounds` and chaining the Layout Modifier.
+
+ // We use `padding` in this example, as it provides an immediate change in layout to its child,
+ // but not the parent, which sees the same resulting layout. The difference can be seen in the
+ // Text (content under padding) and an accompanying Cyan Box (a sibling, under the same Row
+ // parent).
+ LookaheadScope {
+ val boundsTransform = remember {
+ BoundsTransform { _, _ ->
+ spring(stiffness = 50f, visibilityThreshold = Rect.VisibilityThreshold)
+ }
+ }
+
+ var toggleAnimation by remember { mutableStateOf(true) }
+
+ Column(Modifier.clickable { toggleAnimation = !toggleAnimation }) {
+ Text(
+ "See the difference in animation when the Layout Modifier is a parameter of animateBounds. Padding, in this example."
+ )
+ Spacer(Modifier.height(12.dp))
+ Text("Layout Modifier as a parameter.")
+ Row(Modifier.fillMaxWidth()) {
+ Box(
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ modifier =
+ // By providing this Modifier as a parameter of `animateBounds`,
+ // both content and parent see a gradual/animated change in Layout.
+ Modifier.padding(
+ horizontal = if (toggleAnimation) 10.dp else 50.dp
+ ),
+ boundsTransform = boundsTransform
+ )
+ .background(Color.Red, RoundedCornerShape(12.dp))
+ .height(50.dp)
+ ) {
+ Text("Layout Content", Modifier.align(Alignment.Center))
+ }
+ Box(Modifier.size(50.dp).background(Color.Cyan, RoundedCornerShape(12.dp)))
+ }
+ Spacer(Modifier.height(12.dp))
+ Text("Layout Modifier after AnimateBounds.")
+ Row(Modifier.fillMaxWidth()) {
+ Box(
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ boundsTransform = boundsTransform
+ )
+ // The content is able to animate the change in padding, but since the
+ // parent Layout sees no difference, the change in position is immediate.
+ .padding(horizontal = if (toggleAnimation) 10.dp else 50.dp)
+ .background(Color.Red, RoundedCornerShape(12.dp))
+ .height(50.dp)
+ ) {
+ Text("Layout Content", Modifier.align(Alignment.Center))
+ }
+ Box(Modifier.size(50.dp).background(Color.Cyan, RoundedCornerShape(12.dp)))
+ }
+ Spacer(Modifier.height(12.dp))
+ Text("Layout Modifier before AnimateBounds.")
+ Row(Modifier.fillMaxWidth()) {
+ Box(
+ Modifier
+ // The parent is able to see the change in position and the animated size,
+ // so it can smoothly place both its children, but the content of the Box
+ // cannot see the gradual changes so it remains constant.
+ .padding(horizontal = if (toggleAnimation) 10.dp else 50.dp)
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ boundsTransform = boundsTransform
+ )
+ .background(Color.Red, RoundedCornerShape(12.dp))
+ .height(50.dp)
+ ) {
+ Text("Layout Content", Modifier.align(Alignment.Center))
+ }
+ Box(Modifier.size(50.dp).background(Color.Cyan, RoundedCornerShape(12.dp)))
+ }
+ }
+ }
+}
+
+@OptIn(
+ ExperimentalLayoutApi::class,
+ ExperimentalSharedTransitionApi::class,
+)
+@Sampled
+@Composable
+private fun AnimateBounds_inFlowRowSample() {
+ var itemRowCount by remember { mutableIntStateOf(1) }
+ val colors = remember { listOf(Color.Cyan, Color.Magenta, Color.Yellow, Color.Green) }
+
+ // A case showing `animateBounds` being used to animate layout changes driven by a parent Layout
+ LookaheadScope {
+ Column(Modifier.clickable { itemRowCount = if (itemRowCount != 2) 2 else 1 }) {
+ Text("Click to toggle animation.")
+ FlowRow(
+ modifier =
+ Modifier.fillMaxWidth()
+ // Note that the wrap content size changes for FlowRow as the content
+ // adjusts
+ // to one or two lines, we can simply use `animateContentSize()` to make
+ // sure
+ // all items are visible during their animation.
+ .animateContentSize(),
+ // Try changing the arrangement as well!
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ // We use the maxItems parameter to change the layout of the FlowRow at different
+ // states
+ maxItemsInEachRow = itemRowCount
+ ) {
+ colors.fastForEach {
+ Box(
+ Modifier.animateBounds(this@LookaheadScope)
+ // Note the modifier order, we declare the background after
+ // `animateBounds` to make sure it animates with the rest of the content
+ .background(it, RoundedCornerShape(12.dp))
+ .weight(weight = 1f, fill = true)
+ .height(100.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+private fun AnimateBounds_usingKeyframes() {
+ var toggle by remember { mutableStateOf(true) }
+
+ // Example using BoundsTransform to calculate an animation using keyframes with splines.
+ LookaheadScope {
+ Box(Modifier.fillMaxSize().clickable { toggle = !toggle }) {
+ Text(
+ text = "Hello, World!",
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier.align(if (toggle) Alignment.TopStart else Alignment.TopEnd)
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ boundsTransform = { initialBounds, targetBounds ->
+ // We'll use a keyframe to emphasize the animation in position and
+ // size.
+ keyframesWithSpline {
+ durationMillis = 1200
+
+ // Emphasize with an increase in size
+ val size = targetBounds.size.times(2f)
+
+ // Emphasize the path with a slight curve at the halfway point
+ val position =
+ targetBounds.topLeft
+ .plus(initialBounds.topLeft)
+ .times(0.5f)
+ .plus(
+ Offset(
+ // Consider the increase in size (from the
+ // center,
+ // to keep the Layout aligned at the keyframe)
+ x = -(size.width - targetBounds.width) * 0.5f,
+ // Emphasize the path with a vertical offset
+ y = size.height * 0.5f
+ )
+ )
+
+ // Only need to define the intermediate keyframe, initial and
+ // target are implicit.
+ Rect(position, size).atFraction(0.5f).using(LinearEasing)
+ }
+ }
+ )
+ .background(Color.LightGray, RoundedCornerShape(50))
+ .padding(10.dp)
+ // Text is laid out with the animated fixed Constraints, relax constraints
+ // back to wrap content to be able to center Align vertically.
+ .wrapContentSize(Alignment.Center)
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+private fun AnimateBounds_withMovableContent() {
+ // Example showing how to animate a Layout that can be presented on different Layout Composables
+ // as the state changes using `movableContent`.
+ var position by remember { mutableIntStateOf(-1) }
+
+ val movableContent = remember {
+ // To animate a Layout that can be presented in different Composables, we can use
+ // `animateBounds` with `movableContent`.
+ movableContentWithReceiverOf<LookaheadScope> {
+ Box(
+ Modifier.animateBounds(
+ lookaheadScope = this@movableContentWithReceiverOf,
+ boundsTransform = { _, _ ->
+ spring(
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ stiffness = Spring.StiffnessVeryLow,
+ visibilityThreshold = Rect.VisibilityThreshold
+ )
+ }
+ )
+ // Our movableContent can always fill its container in this example.
+ .fillMaxSize()
+ .background(Color.Cyan, RoundedCornerShape(8.dp))
+ )
+ }
+ }
+
+ LookaheadScope {
+ Box(Modifier.fillMaxSize()) {
+ // Initial container of our Layout, at the center of the screen.
+ Box(
+ Modifier.size(200.dp)
+ .border(3.dp, Color.Red, RoundedCornerShape(8.dp))
+ .align(Alignment.Center)
+ .clickable { position = -1 }
+ ) {
+ if (position < 0) {
+ movableContent()
+ }
+ }
+
+ repeat(4) { index ->
+ // Four additional Boxes where our content may be move to.
+ Box(
+ Modifier.size(100.dp)
+ .border(2.dp, Color.Blue, RoundedCornerShape(8.dp))
+ .align { size, space, _ ->
+ val horizontal = if (index % 2 == 0) 0.15f else 0.85f
+ val vertical = if (index < 2) 0.15f else 0.85f
+
+ Offset(
+ x = (space.width - size.width) * horizontal,
+ y = (space.height - size.height) * vertical
+ )
+ .round()
+ }
+ .clickable { position = index }
+ ) {
+ if (position == index) {
+ // The call to movable content will trigger `Modifier.animateBounds()` to
+ // animate the content's position and size from its previous state.
+ movableContent()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
new file mode 100644
index 0000000..c69c971b
--- /dev/null
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
@@ -0,0 +1,556 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentWithReceiverOf
+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.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.util.lerp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.math.roundToInt
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class AnimateBoundsTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun animatePosition() =
+ with(rule.density) {
+ val frames = 14 // Even number to reliably test at half duration
+ val durationMillis = frames * 16
+ val rootSizePx = 100
+ val boxSizePX = 20
+
+ var boxPosition = IntOffset.Zero
+
+ var isAtStart by mutableStateOf(true)
+
+ rule.setContent {
+ Box(modifier = Modifier.size(rootSizePx.toDp())) {
+ LookaheadScope {
+ Box(
+ modifier =
+ Modifier.align(
+ if (isAtStart) Alignment.TopStart else Alignment.BottomEnd
+ )
+ .size(boxSizePX.toDp())
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ boundsTransform = { _, _ ->
+ tween(durationMillis, easing = LinearEasing)
+ }
+ )
+ .drawBehind { drawRect(Color.LightGray) }
+ .onGloballyPositioned {
+ boxPosition = it.positionInParent().round()
+ }
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // At TopStart (0, 0)
+ assertEquals(IntOffset.Zero, boxPosition)
+
+ // AutoAdvance off to test animation at different points
+ rule.mainClock.autoAdvance = false
+ isAtStart = false
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Advance to the middle of the animation
+ rule.mainClock.advanceTimeBy(durationMillis / 2L)
+
+ val expectedPosPx = (rootSizePx - boxSizePX) * 0.5f
+ val expectedIntOffset = Offset(expectedPosPx, expectedPosPx).round()
+ assertEquals(expectedIntOffset, boxPosition)
+
+ // AutoAdvance ON to finish the animation
+ rule.mainClock.autoAdvance = true
+ rule.waitForIdle()
+
+ // At BottomEnd (parentSize - boxSize, parentSize - boxSize)
+ val expectedFinalPos = rootSizePx - boxSizePX
+ assertEquals(IntOffset(expectedFinalPos, expectedFinalPos), boxPosition)
+ }
+
+ @Test
+ fun animateSize() =
+ with(rule.density) {
+ val frameTime = 16 // milliseconds
+ val frames = 14 // Even number to reliable test at half duration
+ val durationMillis = frames * frameTime
+ val rootSizePx = 400
+ val boxSizeSmallPx = rootSizePx * 0.25f
+ val boxSizeLargePx = rootSizePx * 0.5f
+
+ val expectedLargeSize = Size(boxSizeLargePx, boxSizeLargePx)
+ val expectedSmallSize = Size(boxSizeSmallPx, boxSizeSmallPx)
+
+ var boxSize = IntSize.Zero
+
+ var isExpanded by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(rootSizePx.toDp())) {
+ LookaheadScope {
+ Box(
+ Modifier.size(
+ if (isExpanded) boxSizeLargePx.toDp() else boxSizeSmallPx.toDp()
+ )
+ .animateBounds(
+ lookaheadScope = this,
+ boundsTransform = { _, _ ->
+ tween(
+ durationMillis = durationMillis,
+ easing = LinearEasing
+ )
+ }
+ )
+ .drawBehind { drawRect(Color.LightGray) }
+ .onGloballyPositioned { boxSize = it.size }
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertEquals(expectedSmallSize.round(), boxSize)
+
+ // AutoAdvance off to test animation at different points
+ rule.mainClock.autoAdvance = false
+ isExpanded = true
+ rule.waitForIdle()
+
+ // Wait until first animated frame, for test stability
+ do {
+ rule.mainClock.advanceTimeByFrame()
+ } while (expectedSmallSize.round() == boxSize)
+
+ // Advance to approx. the middle of the animation (minus the first animated frame)
+ rule.mainClock.advanceTimeBy(durationMillis / 2L - frameTime)
+
+ val expectedMidIntSize = (expectedLargeSize + expectedSmallSize).times(0.5f).round()
+ assertEquals(expectedMidIntSize, boxSize)
+
+ // AutoAdvance ON to finish the animation
+ rule.mainClock.autoAdvance = true
+ rule.waitForIdle()
+
+ assertEquals(expectedLargeSize.round(), boxSize)
+ }
+
+ @Test
+ fun animateBounds() =
+ with(rule.density) {
+ val frames = 14 // Even number to reliable test at half duration
+ val durationMillis = frames * 16
+ val rootSizePx = 400
+ val boxSizeSmallPx = rootSizePx * 0.25f
+ val boxSizeLargePx = rootSizePx * 0.5f
+
+ val expectedLargeSize = Size(boxSizeLargePx, boxSizeLargePx)
+ val expectedSmallSize = Size(boxSizeSmallPx, boxSizeSmallPx)
+ val expectedFinalPos = rootSizePx - boxSizeLargePx
+
+ var boxBounds = Rect(Offset.Zero, Size.Zero)
+
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(rootSizePx.toDp())) {
+ LookaheadScope {
+ Box(
+ Modifier.then(
+ if (toggle) {
+ Modifier.align(Alignment.BottomEnd)
+ .size(boxSizeLargePx.toDp())
+ } else {
+ Modifier.align(Alignment.TopStart)
+ .size(boxSizeSmallPx.toDp())
+ }
+ )
+ .animateBounds(
+ lookaheadScope = this,
+ boundsTransform = { _, _ ->
+ tween(
+ durationMillis = durationMillis,
+ easing = LinearEasing
+ )
+ }
+ )
+ .drawBehind { drawRect(Color.Yellow) }
+ .onGloballyPositioned { boxBounds = it.boundsInParent() }
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertEquals(Rect(Offset.Zero, expectedSmallSize), boxBounds)
+
+ // AutoAdvance off to test animation at different points
+ rule.mainClock.autoAdvance = false
+ toggle = true
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Advance to the middle of the animation
+ rule.mainClock.advanceTimeBy(durationMillis / 2L)
+ rule.waitForIdle()
+
+ // Calculate expected bounds
+ val expectedMidSize = (expectedLargeSize + expectedSmallSize).times(0.5f)
+ val expectedMidPosition = (rootSizePx - boxSizeLargePx) * 0.5f
+ val expectedMidOffset = Offset(expectedMidPosition, expectedMidPosition)
+ val expectedMidBounds = Rect(expectedMidOffset, expectedMidSize)
+
+ assertEquals(expectedMidBounds, boxBounds)
+
+ // AutoAdvance ON to finish the animation
+ rule.mainClock.autoAdvance = true
+ rule.waitForIdle()
+
+ assertEquals(
+ Rect(Offset(expectedFinalPos, expectedFinalPos), expectedLargeSize),
+ boxBounds
+ )
+ }
+
+ @Test
+ fun animateBounds_withIntermediateModifier() =
+ with(rule.density) {
+ val durationMillis = 10 * 16
+
+ var toggleAnimation by mutableStateOf(true)
+
+ val rootWidthPx = 100
+ val padding1Px = 10
+ val padding2Px = 20
+
+ var positionA = IntOffset(-1, -1)
+ var positionB = IntOffset(-1, 1)
+
+ // Change the padding on state change to trigger the animation
+ fun Modifier.applyPadding(): Modifier =
+ this.padding(
+ horizontal =
+ if (toggleAnimation) {
+ padding1Px.toDp()
+ } else {
+ padding2Px.toDp()
+ }
+ )
+
+ rule.setContent {
+ // Based on sample `AnimateBounds_withLayoutModifier`
+ LookaheadScope {
+ Column(Modifier.width(rootWidthPx.toDp())) {
+ Row(Modifier.fillMaxWidth()) {
+ Box(
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ modifier = Modifier.applyPadding(),
+ boundsTransform = { _, _,
+ ->
+ tween(durationMillis, easing = LinearEasing)
+ }
+ )
+ ) {
+ Box(
+ Modifier.onGloballyPositioned {
+ positionA = it.positionInRoot().round()
+ }
+ )
+ }
+ }
+ Row(Modifier.fillMaxWidth()) {
+ Box(
+ Modifier.animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ boundsTransform = { _, _,
+ ->
+ tween(durationMillis, easing = LinearEasing)
+ }
+ )
+ .applyPadding()
+ ) {
+ Box(
+ Modifier.onGloballyPositioned {
+ positionB = it.positionInRoot().round()
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ assertEquals(positionA, IntOffset(padding1Px, 0))
+ assertEquals(positionB, IntOffset(padding1Px, 0))
+
+ rule.mainClock.autoAdvance = false
+ toggleAnimation = !toggleAnimation
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // We measure at the first animated frame
+ rule.mainClock.advanceTimeByFrame()
+
+ // Box A has a continuous change in value from having the Modifier as a parameter
+ val expectedPosA =
+ lerp(padding1Px.toFloat(), padding2Px.toFloat(), 16f / durationMillis)
+ assertEquals(positionA, IntOffset(expectedPosA.fastRoundToInt(), 0))
+ // Box B has an immediate change in value from chaining the Modifier
+ assertEquals(positionB, IntOffset(padding2Px, 0))
+ }
+
+ @Test
+ fun animateBounds_usingMovableContent() =
+ with(rule.density) {
+ val frames = 14 // Even number to reliable test at half duration
+ val durationMillis = frames * 16
+
+ val itemASizePx = 30
+ val itemAOffset = IntOffset(70, 70)
+
+ val itemBSizePx = 50
+ val itemBOffset = IntOffset(110, 110)
+
+ var isBoxAtSlotA by mutableStateOf(true)
+
+ var boxPosition = IntOffset.Zero
+ var boxSize = IntSize.Zero
+
+ rule.setContent {
+ val movableBox = remember {
+ movableContentWithReceiverOf<LookaheadScope> {
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ .animateBounds(
+ lookaheadScope = this,
+ boundsTransform = { _, _ ->
+ tween(
+ durationMillis = durationMillis,
+ easing = LinearEasing
+ )
+ }
+ )
+ .onGloballyPositioned {
+ boxPosition = it.positionInRoot().round()
+ boxSize = it.size
+ }
+ )
+ }
+ }
+
+ LookaheadScope {
+ Box {
+ Box(Modifier.offset { itemAOffset }.size(itemASizePx.toDp())) {
+ // Slot A
+ if (isBoxAtSlotA) {
+ movableBox()
+ }
+ }
+ Box(Modifier.offset { itemBOffset }.size(itemBSizePx.toDp())) {
+ // Slot B
+ if (!isBoxAtSlotA) {
+ movableBox()
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // Initial conditions
+ assertEquals(itemAOffset, boxPosition)
+ assertEquals(IntSize(itemASizePx, itemASizePx), boxSize)
+
+ // AutoAdvance off to test animation at different points
+ rule.mainClock.autoAdvance = false
+ isBoxAtSlotA = false
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Advance to the middle of the animation
+ rule.mainClock.advanceTimeBy(durationMillis / 2L)
+ rule.waitForIdle()
+
+ // Evaluate with expected values at half the animation
+ val sizeAtHalfDuration = (itemASizePx + itemBSizePx) / 2
+ assertEquals((itemAOffset + itemBOffset).div(2f), boxPosition)
+ assertEquals(IntSize(sizeAtHalfDuration, sizeAtHalfDuration), boxSize)
+
+ // AutoAdvance ON to finish the animation
+ rule.mainClock.autoAdvance = true
+ rule.waitForIdle()
+
+ assertEquals(itemBOffset, boxPosition)
+ assertEquals(IntSize(itemBSizePx, itemBSizePx), boxSize)
+ }
+
+ @Test
+ fun animateBounds_scrollBehavior() =
+ with(rule.density) {
+ val itemSizePx = 30f
+ val keyFrameOffset = itemSizePx * 5
+
+ var isAnimateScroll by mutableStateOf(false)
+ val scrollState = ScrollState(0)
+
+ var item0Position = IntOffset(-1, -1)
+
+ rule.setContent {
+ LookaheadScope {
+ Column(Modifier.size(itemSizePx.toDp()).verticalScroll(scrollState)) {
+ repeat(2) { index ->
+ Box(
+ modifier =
+ Modifier.size(itemSizePx.toDp())
+ .animateBounds(
+ lookaheadScope = this@LookaheadScope,
+ boundsTransform = { initial, _ ->
+ // Drive the start position to a specific value, by
+ // default
+ // the animation should not happen, and so we should
+ // never
+ // be able to read that value.
+ keyframes {
+ Rect(Offset(0f, keyFrameOffset), initial.size)
+ .at(0)
+ .using(LinearEasing)
+ }
+ },
+ animateMotionFrameOfReference = isAnimateScroll
+ )
+ .onGloballyPositioned {
+ if (index == 0) {
+ item0Position = it.positionInRoot().round()
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ // First test without animating scroll, note that we still handle the clock, as to allow
+ // any animation to play after we change the scroll.
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+
+ runBlocking { scrollState.scrollBy(itemSizePx) }
+ rule.waitForIdle()
+
+ // Let animations play for the first frame
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Expected position should immediately reflect scroll changes since we are not
+ // animating it
+ assertEquals(IntOffset(0, -itemSizePx.fastRoundToInt()), item0Position)
+
+ // Finish any pending animations
+ rule.mainClock.autoAdvance = true
+ rule.waitForIdle()
+
+ // Enable scroll animation
+ isAnimateScroll = true
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+
+ runBlocking {
+ // Scroll back into starting position
+ scrollState.scrollBy(-itemSizePx)
+ }
+ rule.waitForIdle()
+
+ rule.mainClock.advanceTimeByFrame()
+
+ // Position should correspond to the exaggerated keyframe offset.
+ // Note that the keyframe is actually defined around the item's center
+ assertEquals(
+ Offset(
+ // Center position at x = 0
+ x = 0f,
+ // keyframeOffset - (previousScrollOffset) + itemCenterY
+ y = keyFrameOffset
+ )
+ .round(),
+ item0Position
+ )
+ }
+
+ private fun Size.round(): IntSize = IntSize(width.roundToInt(), height.roundToInt())
+
+ private operator fun Size.plus(other: Size) = Size(width + other.width, height + other.height)
+
+ private operator fun Size.minus(other: Size) = Size(width - other.width, height - other.height)
+
+ private operator fun IntSize.minus(other: IntSize) =
+ IntSize(width - other.width, height - other.height)
+
+ private operator fun Rect.minus(other: Rect) =
+ Rect(offset = this.topLeft - other.topLeft, size = this.size - other.size)
+}
diff --git a/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/AndroidFlingSplineTest.kt b/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/AndroidFlingSplineTest.kt
new file mode 100644
index 0000000..45c7383
--- /dev/null
+++ b/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/AndroidFlingSplineTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class AndroidFlingSplineTest {
+
+ @Test
+ fun testClampedOutOfRangePosition() {
+ val controlStart = AndroidFlingSpline.flingPosition(0f)
+ val controlEnd = AndroidFlingSpline.flingPosition(1f)
+
+ assertEquals(controlStart, AndroidFlingSpline.flingPosition(-10f))
+ assertEquals(controlStart, AndroidFlingSpline.flingPosition(-1f))
+ assertEquals(controlStart, AndroidFlingSpline.flingPosition(-0.06f))
+
+ assertEquals(controlEnd, AndroidFlingSpline.flingPosition(1.5f))
+ assertEquals(controlEnd, AndroidFlingSpline.flingPosition(10f))
+ }
+}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt
new file mode 100644
index 0000000..388b244
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimateBoundsModifier.kt
@@ -0,0 +1,428 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.layout.ApproachLayoutModifierNode
+import androidx.compose.ui.layout.ApproachMeasureScope
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.unit.roundToIntSize
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.util.fastRoundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.launch
+
+/**
+ * [Modifier] to animate layout changes (position and/or size) that occur within a [LookaheadScope].
+ *
+ * So, the given [lookaheadScope] defines the coordinate space considered to trigger an animation.
+ * For example, if [lookaheadScope] was defined at the root of the app hierarchy, then any layout
+ * changes visible within the screen will trigger an animation, if it, in contrast was defined
+ * within a scrolling parent, then, as long the [LookaheadScope] scrolls with is content, no
+ * animation will be triggered, as there will be no changes within its coordinate space.
+ *
+ * The animation is driven with a [FiniteAnimationSpec] produced by the given [BoundsTransform]
+ * function, which you may use to customize the animations based on the initial and target bounds.
+ *
+ * Do note that certain Layout Modifiers when chained with [animateBounds], may only cause an
+ * immediate observable change to either the child or the parent Layout which can result in
+ * undesired behavior. For those cases you can instead provide it to the [modifier] parameter. This
+ * allows [animateBounds] to envelop the size and constraints change and propagate them gradually to
+ * both its parent and child Layout.
+ *
+ * You may see the difference when supplying a Layout Modifier in [modifier] on the following
+ * example:
+ *
+ * @sample androidx.compose.animation.samples.AnimateBounds_withLayoutModifier
+ *
+ * By default, changes in position under [LayoutCoordinates.introducesMotionFrameOfReference] are
+ * excluded from the animation and are instead immediately applied, as they are expected to be
+ * frequent/continuous (to handle Layouts under Scroll). You may change this behavior by passing
+ * [animateMotionFrameOfReference] as `true`. Keep in mind, doing that under a scroll may result in
+ * the Layout "chasing" the scroll offset, as it will constantly animate to the latest position.
+ *
+ * A basic use-case is animating a layout based on content changes, such as the String changing on a
+ * Text:
+ *
+ * @sample androidx.compose.animation.samples.AnimateBounds_animateOnContentChange
+ *
+ * It also provides an easy way to animate layout changes of a complex Composable Layout:
+ *
+ * @sample androidx.compose.animation.samples.AnimateBounds_inFlowRowSample
+ *
+ * Since [BoundsTransform] is called when initiating an animation, you may also use it to calculate
+ * a keyframe based animation:
+ *
+ * @sample androidx.compose.animation.samples.AnimateBounds_usingKeyframes
+ *
+ * It may also be used together with [movableContent][androidx.compose.runtime.movableContentOf] as
+ * long as the given [LookaheadScope] is in a common place within the Layout hierarchy of the slots
+ * presenting the `movableContent`:
+ *
+ * @sample androidx.compose.animation.samples.AnimateBounds_withMovableContent
+ * @param lookaheadScope The scope from which this [animateBounds] will calculate its animations
+ * from. This implies that as long as you're expecting an animation the reference of the given
+ * [LookaheadScope] shouldn't change, otherwise you may get unexpected behavior.
+ * @param modifier Optional intermediate Modifier, may be used in cases where otherwise immediate
+ * layout changes are perceived as gradual by both the parent and child Layout.
+ * @param boundsTransform Produce a customized [FiniteAnimationSpec] based on the initial and target
+ * bounds, called when an animation is triggered.
+ * @param animateMotionFrameOfReference When `true`, changes under
+ * [LayoutCoordinates.introducesMotionFrameOfReference] (for continuous positional changes, such
+ * as Scroll Offset) are included when calculating an animation. `false` by default, where the
+ * changes are instead applied directly into the layout without triggering an animation.
+ * @see ApproachLayoutModifierNode
+ * @see LookaheadScope
+ */
+@ExperimentalSharedTransitionApi // Depends on BoundsTransform
+public fun Modifier.animateBounds(
+ lookaheadScope: LookaheadScope,
+ modifier: Modifier = Modifier,
+ boundsTransform: BoundsTransform = DefaultBoundsTransform,
+ animateMotionFrameOfReference: Boolean = false,
+): Modifier =
+ this.then(
+ BoundsAnimationElement(
+ lookaheadScope = lookaheadScope,
+ boundsTransform = boundsTransform,
+ // Measure with original constraints.
+ // The layout of this element will still be the animated lookahead size.
+ resolveMeasureConstraints = { _, constraints -> constraints },
+ animateMotionFrameOfReference = animateMotionFrameOfReference,
+ )
+ )
+ .then(modifier)
+ .then(
+ BoundsAnimationElement(
+ lookaheadScope = lookaheadScope,
+ boundsTransform = boundsTransform,
+ resolveMeasureConstraints = { animatedSize, _ ->
+ // For the target Layout, pass the animated size as Constraints.
+ Constraints.fixed(animatedSize.width, animatedSize.height)
+ },
+ animateMotionFrameOfReference = animateMotionFrameOfReference,
+ )
+ )
+
+@ExperimentalSharedTransitionApi
+internal data class BoundsAnimationElement(
+ val lookaheadScope: LookaheadScope,
+ val boundsTransform: BoundsTransform,
+ val resolveMeasureConstraints: (animatedSize: IntSize, constraints: Constraints) -> Constraints,
+ val animateMotionFrameOfReference: Boolean,
+) : ModifierNodeElement<BoundsAnimationModifierNode>() {
+ override fun create(): BoundsAnimationModifierNode {
+ return BoundsAnimationModifierNode(
+ lookaheadScope = lookaheadScope,
+ boundsTransform = boundsTransform,
+ onChooseMeasureConstraints = resolveMeasureConstraints,
+ animateMotionFrameOfReference = animateMotionFrameOfReference,
+ )
+ }
+
+ override fun update(node: BoundsAnimationModifierNode) {
+ node.lookaheadScope = lookaheadScope
+ node.boundsTransform = boundsTransform
+ node.onChooseMeasureConstraints = resolveMeasureConstraints
+ node.animateMotionFrameOfReference = animateMotionFrameOfReference
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "boundsAnimation"
+ properties["lookaheadScope"] = lookaheadScope
+ properties["boundsTransform"] = boundsTransform
+ properties["onChooseMeasureConstraints"] = resolveMeasureConstraints
+ properties["animateMotionFrameOfReference"] = animateMotionFrameOfReference
+ }
+}
+
+/**
+ * [Modifier.Node] implementation that handles the bounds animation with
+ * [ApproachLayoutModifierNode].
+ *
+ * @param lookaheadScope The [LookaheadScope] to animate from.
+ * @param boundsTransform Callback to produce [FiniteAnimationSpec] at every triggered animation
+ * @param onChooseMeasureConstraints Callback to decide whether to measure the Modifier Layout with
+ * the current animated size value or the incoming constraints. This reflects on the
+ * [MeasureResult] of this Modifier Layout as well.
+ * @param animateMotionFrameOfReference Whether to include changes under
+ * [LayoutCoordinates.introducesMotionFrameOfReference] to trigger animations.
+ */
+@ExperimentalSharedTransitionApi
+internal class BoundsAnimationModifierNode(
+ var lookaheadScope: LookaheadScope,
+ var boundsTransform: BoundsTransform,
+ var onChooseMeasureConstraints:
+ (animatedSize: IntSize, constraints: Constraints) -> Constraints,
+ var animateMotionFrameOfReference: Boolean,
+) : ApproachLayoutModifierNode, Modifier.Node() {
+ private val boundsAnimation = BoundsTransformDeferredAnimation()
+
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ // Update target size, it will serve to know if we expect an approach in progress
+ boundsAnimation.updateTargetSize(lookaheadSize.toSize())
+
+ return !boundsAnimation.isIdle
+ }
+
+ override fun Placeable.PlacementScope.isPlacementApproachInProgress(
+ lookaheadCoordinates: LayoutCoordinates
+ ): Boolean {
+ // Once we can capture size and offset we may also start the animation
+ boundsAnimation.updateTargetOffsetAndAnimate(
+ lookaheadScope = lookaheadScope,
+ placementScope = this,
+ coroutineScope = coroutineScope,
+ includeMotionFrameOfReference = animateMotionFrameOfReference,
+ boundsTransform = boundsTransform,
+ )
+ return !boundsAnimation.isIdle
+ }
+
+ override fun ApproachMeasureScope.approachMeasure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ // The animated value is null on the first frame as we don't get the full bounds
+ // information until placement, so we can safely use the current Size.
+ val fallbackSize =
+ if (boundsAnimation.currentSize.isUnspecified) {
+ // When using Intrinsics, we may get measured before getting the approach check
+ lookaheadSize.toSize()
+ } else {
+ boundsAnimation.currentSize
+ }
+ val animatedSize = (boundsAnimation.value?.size ?: fallbackSize).roundToIntSize()
+
+ val chosenConstraints = onChooseMeasureConstraints(animatedSize, constraints)
+
+ val placeable = measurable.measure(chosenConstraints)
+ return layout(animatedSize.width, animatedSize.height) {
+ val animatedBounds = boundsAnimation.value
+ val positionInScope =
+ with(lookaheadScope) {
+ coordinates?.let { coordinates ->
+ lookaheadScopeCoordinates.localPositionOf(
+ sourceCoordinates = coordinates,
+ relativeToSource = Offset.Zero,
+ includeMotionFrameOfReference = animateMotionFrameOfReference
+ )
+ }
+ }
+
+ val topLeft =
+ if (animatedBounds != null) {
+ boundsAnimation.updateCurrentBounds(animatedBounds.topLeft, animatedBounds.size)
+ animatedBounds.topLeft
+ } else {
+ boundsAnimation.currentBounds?.topLeft ?: Offset.Zero
+ }
+ val (x, y) = positionInScope?.let { topLeft - it } ?: Offset.Zero
+ placeable.place(x.fastRoundToInt(), y.fastRoundToInt())
+ }
+ }
+}
+
+/** Helper class to keep track of the BoundsAnimation state for [ApproachLayoutModifierNode]. */
+@OptIn(ExperimentalSharedTransitionApi::class)
+internal class BoundsTransformDeferredAnimation {
+ private var animatable: Animatable<Rect, AnimationVector4D>? = null
+
+ private var targetSize: Size = Size.Unspecified
+ private var targetOffset: Offset = Offset.Unspecified
+
+ private var isPending = false
+
+ /**
+ * Captures lookahead size, updates current size for the first pass and marks the animation as
+ * pending.
+ */
+ fun updateTargetSize(size: Size) {
+ if (targetSize.isSpecified && size.roundToIntSize() != targetSize.roundToIntSize()) {
+ // Change in target, animation is pending
+ isPending = true
+ }
+ targetSize = size
+
+ if (currentSize.isUnspecified) {
+ currentSize = size
+ }
+ }
+
+ /**
+ * Captures lookahead position, updates current position for the first pass and marks the
+ * animation as pending.
+ */
+ private fun updateTargetOffset(offset: Offset) {
+ if (targetOffset.isSpecified && offset.round() != targetOffset.round()) {
+ isPending = true
+ }
+ targetOffset = offset
+
+ if (currentPosition.isUnspecified) {
+ currentPosition = offset
+ }
+ }
+
+ // We capture the current bounds parameters individually to avoid unnecessary Rect allocations
+ private var currentPosition: Offset = Offset.Unspecified
+ var currentSize: Size = Size.Unspecified
+
+ val currentBounds: Rect?
+ get() {
+ val size = currentSize
+ val position = currentPosition
+ return if (position.isSpecified && size.isSpecified) {
+ Rect(position, size)
+ } else {
+ null
+ }
+ }
+
+ fun updateCurrentBounds(position: Offset, size: Size) {
+ currentPosition = position
+ currentSize = size
+ }
+
+ val isIdle: Boolean
+ get() = !isPending && animatable?.isRunning != true
+
+ private var animatedValue: Rect? by mutableStateOf(null)
+
+ val value: Rect?
+ get() = if (isIdle) null else animatedValue
+
+ private var directManipulationParents: MutableList<LayoutCoordinates>? = null
+ private var additionalOffset: Offset = Offset.Zero
+
+ fun updateTargetOffsetAndAnimate(
+ lookaheadScope: LookaheadScope,
+ placementScope: Placeable.PlacementScope,
+ coroutineScope: CoroutineScope,
+ includeMotionFrameOfReference: Boolean,
+ boundsTransform: BoundsTransform,
+ ) {
+ placementScope.coordinates?.let { coordinates ->
+ with(lookaheadScope) {
+ val lookaheadScopeCoordinates = placementScope.lookaheadScopeCoordinates
+
+ var delta = Offset.Zero
+ if (!includeMotionFrameOfReference) {
+ // As the Layout changes, we need to keep track of the accumulated offset up
+ // the hierarchy tree, to get the proper Offset accounting for scrolling.
+ val parents = directManipulationParents ?: mutableListOf()
+ var currentCoords = coordinates
+ var index = 0
+
+ // Find the given lookahead coordinates by traversing up the tree
+ while (currentCoords.toLookaheadCoordinates() != lookaheadScopeCoordinates) {
+ if (currentCoords.introducesMotionFrameOfReference) {
+ if (parents.size == index) {
+ parents.add(currentCoords)
+ delta += currentCoords.positionInParent()
+ } else if (parents[index] != currentCoords) {
+ delta -= parents[index].positionInParent()
+ parents[index] = currentCoords
+ delta += currentCoords.positionInParent()
+ }
+ index++
+ }
+ currentCoords = currentCoords.parentCoordinates ?: break
+ }
+
+ for (i in parents.size - 1 downTo index) {
+ delta -= parents[i].positionInParent()
+ parents.removeAt(parents.size - 1)
+ }
+ directManipulationParents = parents
+ }
+ additionalOffset += delta
+
+ val targetOffset =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(
+ sourceCoordinates = coordinates,
+ includeMotionFrameOfReference = includeMotionFrameOfReference
+ )
+ updateTargetOffset(targetOffset + additionalOffset)
+
+ animatedValue =
+ animate(coroutineScope = coroutineScope, boundsTransform = boundsTransform)
+ .translate(-(additionalOffset))
+ }
+ }
+ }
+
+ private fun animate(
+ coroutineScope: CoroutineScope,
+ boundsTransform: BoundsTransform,
+ ): Rect {
+ if (targetOffset.isSpecified && targetSize.isSpecified) {
+ // Initialize Animatable when possible, we might not use it but we need to have it
+ // instantiated since at the first pass the lookahead information will become the
+ // initial bounds when we actually need an animation.
+ val target = Rect(targetOffset, targetSize)
+ val anim = animatable ?: Animatable(target, Rect.VectorConverter)
+ animatable = anim
+
+ // This check should avoid triggering an animation on the first pass, as there would not
+ // be enough information to have a distinct current and target bounds.
+ if (isPending) {
+ isPending = false
+ coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ // Dispatch right away to make sure approach callbacks are accurate on `isIdle`
+ anim.animateTo(target, boundsTransform.transform(currentBounds!!, target))
+ }
+ }
+ }
+ return animatable?.value ?: Rect.Zero
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+private val DefaultBoundsTransform = BoundsTransform { _, _ ->
+ spring(
+ dampingRatio = Spring.DampingRatioNoBouncy,
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = Rect.VisibilityThreshold
+ )
+}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt
index 04e8097..8c6e47f0 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt
@@ -87,7 +87,10 @@
* @param time progress through the fling animation from 0-1
*/
fun flingPosition(time: Float): FlingResult {
- val index = (NbSamples * time).toInt()
+ // We clamp the time to prevent crashes from a clock providing playTime values lower than
+ // the start time, which leads to an IOO here. See b/313685022.
+ val clampedTime = time.coerceIn(0f, 1f)
+ val index = (NbSamples * clampedTime).toInt()
var distanceCoef = 1f
var velocityCoef = 0f
if (index < NbSamples) {
@@ -96,7 +99,7 @@
val dInf = SplinePositions[index]
val dSup = SplinePositions[index + 1]
velocityCoef = (dSup - dInf) / (tSup - tInf)
- distanceCoef = dInf + (time - tInf) * velocityCoef
+ distanceCoef = dInf + (clampedTime - tInf) * velocityCoef
}
return FlingResult(distanceCoefficient = distanceCoef, velocityCoefficient = velocityCoef)
}
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index a17370f..0d0e4ff 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -15,6 +15,8 @@
Added method androidx.compose.foundation.lazy.grid.LazyGridItemInfo.getSpan()
AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo#getMaxSpan():
Added method androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo.getMaxSpan()
+AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridScope#stickyHeader(Object, Object, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit>):
+ Added method androidx.compose.foundation.lazy.grid.LazyGridScope.stickyHeader(Object,Object,kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit>)
ParameterNameChange: androidx.compose.foundation.gestures.DraggableAnchors#positionOf(T) parameter #0:
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 5e258af..bced9b3 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -410,10 +410,10 @@
method @Deprecated public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method @Deprecated public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method @Deprecated public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method @Deprecated public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
method public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
method public static inline <T> void forEach(androidx.compose.foundation.gestures.DraggableAnchors<T>, kotlin.jvm.functions.Function2<? super T,? super java.lang.Float,kotlin.Unit> block);
@@ -1062,6 +1062,7 @@
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker public sealed interface LazyGridScope {
method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridItemSpanScope,androidx.compose.foundation.lazy.grid.GridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemSpanScope,? super java.lang.Integer,androidx.compose.foundation.lazy.grid.GridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ method public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit> content);
}
@kotlin.DslMarker public @interface LazyGridScopeMarker {
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index a17370f..0d0e4ff 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -15,6 +15,8 @@
Added method androidx.compose.foundation.lazy.grid.LazyGridItemInfo.getSpan()
AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo#getMaxSpan():
Added method androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo.getMaxSpan()
+AddedAbstractMethod: androidx.compose.foundation.lazy.grid.LazyGridScope#stickyHeader(Object, Object, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit>):
+ Added method androidx.compose.foundation.lazy.grid.LazyGridScope.stickyHeader(Object,Object,kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit>)
ParameterNameChange: androidx.compose.foundation.gestures.DraggableAnchors#positionOf(T) parameter #0:
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index da88627..38305f9 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -412,10 +412,10 @@
method @Deprecated public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method @Deprecated public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
method public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
- method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method @Deprecated public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+ method @Deprecated public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
method public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
method public static inline <T> void forEach(androidx.compose.foundation.gestures.DraggableAnchors<T>, kotlin.jvm.functions.Function2<? super T,? super java.lang.Float,kotlin.Unit> block);
@@ -1064,6 +1064,7 @@
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker public sealed interface LazyGridScope {
method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridItemSpanScope,androidx.compose.foundation.lazy.grid.GridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemSpanScope,? super java.lang.Integer,androidx.compose.foundation.lazy.grid.GridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ method public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.grid.LazyGridItemScope,? super java.lang.Integer,kotlin.Unit> content);
}
@kotlin.DslMarker public @interface LazyGridScopeMarker {
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyGridScrollingBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyGridScrollingBenchmark.kt
index 1b99bd8..919e277 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyGridScrollingBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyGridScrollingBenchmark.kt
@@ -64,6 +64,18 @@
}
@Test
+ fun scrollProgrammatically_useStickyHeader() {
+ benchmarkRule.toggleStateBenchmark {
+ GridRemeasureTestCase(
+ addNewItemOnToggle = false,
+ content = testCase.content,
+ isVertical = testCase.isVertical,
+ useStickyHeader = true
+ )
+ }
+ }
+
+ @Test
fun scrollProgrammatically_newItemComposed() {
benchmarkRule.toggleStateBenchmark {
GridRemeasureTestCase(
@@ -87,6 +99,19 @@
}
@Test
+ fun scrollViaPointerInput_useStickyHeader() {
+ benchmarkRule.toggleStateBenchmark {
+ GridRemeasureTestCase(
+ addNewItemOnToggle = false,
+ content = testCase.content,
+ isVertical = testCase.isVertical,
+ usePointerInput = true,
+ useStickyHeader = true
+ )
+ }
+ }
+
+ @Test
fun scrollViaPointerInput_newItemComposed() {
benchmarkRule.toggleStateBenchmark {
GridRemeasureTestCase(
@@ -142,7 +167,7 @@
class LazyGridScrollingTestCase(
private val name: String,
val isVertical: Boolean,
- val content: @Composable GridRemeasureTestCase.(LazyGridState) -> Unit
+ val content: @Composable GridRemeasureTestCase.(LazyGridState, Boolean) -> Unit
) {
override fun toString(): String {
return name
@@ -150,36 +175,45 @@
}
private val Vertical =
- LazyGridScrollingTestCase("Vertical", isVertical = true) { state ->
+ LazyGridScrollingTestCase("Vertical", isVertical = true) { state, useStickyHeader ->
LazyVerticalGrid(
columns = GridCells.Fixed(2),
state = state,
modifier = Modifier.requiredHeight(400.dp).fillMaxWidth(),
flingBehavior = NoFlingBehavior
) {
- items(2) { FirstLargeItem() }
+ if (useStickyHeader) {
+ stickyHeader { FirstLargeItem() }
+ } else {
+ items(2) { FirstLargeItem() }
+ }
items(items) { RegularItem() }
}
}
private val Horizontal =
- LazyGridScrollingTestCase("Horizontal", isVertical = false) { state ->
+ LazyGridScrollingTestCase("Horizontal", isVertical = false) { state, useStickyHeader ->
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
state = state,
modifier = Modifier.requiredWidth(400.dp).fillMaxHeight(),
flingBehavior = NoFlingBehavior
) {
- items(2) { FirstLargeItem() }
+ if (useStickyHeader) {
+ stickyHeader { FirstLargeItem() }
+ } else {
+ items(2) { FirstLargeItem() }
+ }
items(items) { RegularItem() }
}
}
class GridRemeasureTestCase(
val addNewItemOnToggle: Boolean,
- val content: @Composable GridRemeasureTestCase.(LazyGridState) -> Unit,
+ val content: @Composable GridRemeasureTestCase.(LazyGridState, Boolean) -> Unit,
val isVertical: Boolean,
- val usePointerInput: Boolean = false
+ val usePointerInput: Boolean = false,
+ val useStickyHeader: Boolean = false
) : LazyBenchmarkTestCase(isVertical, usePointerInput) {
val items = List(300) { LazyItem(it) }
@@ -201,7 +235,7 @@
}
InitializeScrollHelper(scrollAmount = scrollBy)
state = rememberLazyGridState()
- content(state)
+ content(state, useStickyHeader)
}
@Composable
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt
index ddc8782..d9f1be0 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/AnchoredDraggableDemo.kt
@@ -21,7 +21,6 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.samples.AnchoredDraggableAnchorsFromCompositionSample
-import androidx.compose.foundation.samples.AnchoredDraggableCatchAnimatingWidgetSample
import androidx.compose.foundation.samples.AnchoredDraggableCustomAnchoredSample
import androidx.compose.foundation.samples.AnchoredDraggableLayoutDependentAnchorsSample
import androidx.compose.foundation.samples.AnchoredDraggableProgressSample
@@ -40,7 +39,6 @@
Spacer(Modifier.height(50.dp))
AnchoredDraggableLayoutDependentAnchorsSample()
Spacer(Modifier.height(50.dp))
- AnchoredDraggableCatchAnimatingWidgetSample()
Spacer(Modifier.height(50.dp))
AnchoredDraggableCustomAnchoredSample()
Spacer(Modifier.height(50.dp))
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 9938333..a328cd2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -71,8 +71,9 @@
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.samples.StickyHeaderGridSample
import androidx.compose.foundation.samples.StickyHeaderHeaderIndexSample
-import androidx.compose.foundation.samples.StickyHeaderSample
+import androidx.compose.foundation.samples.StickyHeaderListSample
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.integration.demos.common.ComposableDemo
@@ -135,7 +136,8 @@
ComposableDemo("Rtl list") { RtlListDemo() },
ComposableDemo("LazyColumn DSL") { LazyColumnScope() },
ComposableDemo("LazyRow DSL") { LazyRowScope() },
- ComposableDemo("LazyColumn with sticky headers") { StickyHeaderSample() },
+ ComposableDemo("LazyColumn with sticky headers") { StickyHeaderListSample() },
+ ComposableDemo("LazyVerticalGrid with sticky headers") { StickyHeaderGridSample() },
ComposableDemo("LazyColumn with sticky headers - header index") {
StickyHeaderHeaderIndexSample()
},
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridHeadersTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridHeadersTest.kt
new file mode 100644
index 0000000..eda4457
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridHeadersTest.kt
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.list.scrollBy
+import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridHeadersTest {
+
+ private val LazyGridTag = "LazyGrid"
+
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun lazyVerticalGridShowsHeader() {
+ val items = (1..6).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContent {
+ LazyVerticalGrid(columns = GridCells.Fixed(3), modifier = Modifier.height(300.dp)) {
+ stickyHeader {
+ Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(firstHeaderTag))
+ }
+
+ items(items) { Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(it)) }
+
+ stickyHeader {
+ Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(secondHeaderTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstHeaderTag).assertIsDisplayed()
+
+ rule.onNodeWithTag("1").assertIsDisplayed()
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag).assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyVerticalGridShowsHeadersOnScroll() {
+ val items = (1..3).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ lateinit var state: LazyGridState
+
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier.height(300.dp).testTag(LazyGridTag),
+ state = rememberLazyGridState().also { state = it }
+ ) {
+ stickyHeader {
+ Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(firstHeaderTag))
+ }
+
+ items(items) { Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(it)) }
+
+ stickyHeader {
+ Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(secondHeaderTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag).scrollBy(y = 102.dp, density = rule.density)
+
+ rule
+ .onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.runOnIdle {
+ Assert.assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+ Assert.assertEquals(IntOffset.Zero, state.layoutInfo.visibleItemsInfo.first().offset)
+ }
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyVerticalGridHeaderIsReplaced() {
+ val items = (1..6).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier.height(300.dp).testTag(LazyGridTag)
+ ) {
+ stickyHeader {
+ Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(firstHeaderTag))
+ }
+
+ stickyHeader {
+ Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(secondHeaderTag))
+ }
+
+ items(items) { Spacer(Modifier.height(101.dp).fillMaxWidth().testTag(it)) }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag).scrollBy(y = 105.dp, density = rule.density)
+
+ rule.onNodeWithTag(firstHeaderTag).assertIsNotDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
+
+ rule.onNodeWithTag("1").assertIsDisplayed()
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyHorizontalGridShowsHeader() {
+ val items = (1..6).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContent {
+ LazyHorizontalGrid(rows = GridCells.Fixed(3), modifier = Modifier.width(300.dp)) {
+ stickyHeader {
+ Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(firstHeaderTag))
+ }
+
+ items(items) { Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(it)) }
+
+ stickyHeader {
+ Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(secondHeaderTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstHeaderTag).assertIsDisplayed()
+
+ rule.onNodeWithTag("1").assertIsDisplayed()
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag).assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyHorizontalGridShowsHeadersOnScroll() {
+ val items = (1..3).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ lateinit var state: LazyGridState
+
+ rule.setContentWithTestViewConfiguration {
+ LazyHorizontalGrid(
+ rows = GridCells.Fixed(3),
+ modifier = Modifier.width(300.dp).testTag(LazyGridTag),
+ state = rememberLazyGridState().also { state = it }
+ ) {
+ stickyHeader {
+ Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(firstHeaderTag))
+ }
+
+ items(items) { Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(it)) }
+
+ stickyHeader {
+ Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(secondHeaderTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag).scrollBy(x = 102.dp, density = rule.density)
+
+ rule
+ .onNodeWithTag(firstHeaderTag)
+ .assertIsDisplayed()
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.runOnIdle {
+ Assert.assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+ Assert.assertEquals(IntOffset.Zero, state.layoutInfo.visibleItemsInfo.first().offset)
+ }
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyHorizontalGridHeaderIsReplaced() {
+ val items = (1..6).map { it.toString() }
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+
+ rule.setContentWithTestViewConfiguration {
+ LazyHorizontalGrid(
+ rows = GridCells.Fixed(3),
+ modifier = Modifier.width(300.dp).testTag(LazyGridTag)
+ ) {
+ stickyHeader {
+ Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(firstHeaderTag))
+ }
+
+ stickyHeader {
+ Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(secondHeaderTag))
+ }
+
+ items(items) { Spacer(Modifier.width(101.dp).fillMaxHeight().testTag(it)) }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag).scrollBy(x = 105.dp, density = rule.density)
+
+ rule.onNodeWithTag(firstHeaderTag).assertIsNotDisplayed()
+
+ rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
+
+ rule.onNodeWithTag("1").assertIsDisplayed()
+
+ rule.onNodeWithTag("2").assertIsDisplayed()
+ }
+
+ @Test
+ fun headerIsDisplayedWhenItIsFullyInContentPadding() {
+ val headerTag = "header"
+ val itemIndexPx = 100
+ val itemIndexDp = with(rule.density) { itemIndexPx.toDp() }
+ lateinit var state: LazyGridState
+
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier.requiredSize(itemIndexDp * 4),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(top = itemIndexDp * 2)
+ ) {
+ stickyHeader { Spacer(Modifier.requiredSize(itemIndexDp).testTag(headerTag)) }
+
+ items((0..11).toList()) {
+ Spacer(Modifier.requiredSize(itemIndexDp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle { runBlocking { state.scrollToItem(1, itemIndexPx / 2) } }
+
+ rule.onNodeWithTag(headerTag).assertTopPositionInRootIsEqualTo(itemIndexDp / 2)
+
+ rule.runOnIdle {
+ Assert.assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
+ Assert.assertEquals(
+ itemIndexPx / 2 - /* content padding size */ itemIndexPx * 2,
+ state.layoutInfo.visibleItemsInfo.first().offset.y
+ )
+ }
+
+ rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemIndexDp * 3 / 2)
+ }
+
+ @Test
+ fun lazyVerticalGridShowsHeader2() {
+ val firstHeaderTag = "firstHeaderTag"
+ val secondHeaderTag = "secondHeaderTag"
+ val itemSizeDp = with(rule.density) { 100.toDp() }
+ val scrollDistance = 20
+ val scrollDistanceDp = with(rule.density) { scrollDistance.toDp() }
+ val state = LazyGridState()
+
+ rule.setContent {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier.height(itemSizeDp * 3.5f),
+ state = state
+ ) {
+ stickyHeader {
+ Spacer(Modifier.height(itemSizeDp).fillMaxWidth().testTag(firstHeaderTag))
+ }
+ stickyHeader {
+ Spacer(Modifier.height(itemSizeDp).fillMaxWidth().testTag(secondHeaderTag))
+ }
+
+ items(100) {
+ Spacer(Modifier.height(itemSizeDp).fillMaxWidth().testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.runOnIdle { runBlocking { state.scrollBy(scrollDistance.toFloat()) } }
+
+ rule.onNodeWithTag(firstHeaderTag).assertTopPositionInRootIsEqualTo(-scrollDistanceDp)
+ rule
+ .onNodeWithTag(secondHeaderTag)
+ .assertTopPositionInRootIsEqualTo(itemSizeDp - scrollDistanceDp)
+ rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSizeDp * 2 - scrollDistanceDp)
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
index 50dcbe8..84a7799 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
@@ -314,4 +314,33 @@
rule.onNodeWithTag(LazyStaggeredGrid).assertMainAxisSizeIsEqualTo(itemSizeDp * 6)
}
+
+ @Test
+ fun afterContentPaddingWithSmallScrolls() {
+ state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+ rule.setContent {
+ Box(Modifier.axisSize(itemSizeDp * 2, itemSizeDp * 4)) {
+ LazyStaggeredGrid(
+ lanes = 2,
+ modifier = Modifier.testTag(LazyStaggeredGrid),
+ contentPadding = PaddingValues(afterContent = itemSizeDp / 2),
+ state = state
+ ) {
+ items(20, key = { it }) {
+ val size = if (it == 0 || it == 19) itemSizeDp / 2 else itemSizeDp * 2
+ Spacer(Modifier.mainAxisSize(size).testTag("$it").debugBorder())
+ }
+ }
+ }
+ }
+
+ // scroll to the end
+ state.scrollBy(itemSizeDp * 30)
+
+ state.scrollBy(-5.dp)
+
+ state.scrollBy(itemSizeDp / 2)
+
+ rule.onNodeWithTag("19").assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 3f)
+ }
}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt
index 499c99d..b84ee4c 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt
@@ -18,7 +18,6 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animate
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
@@ -179,52 +178,6 @@
@Preview
@Composable
-fun AnchoredDraggableCatchAnimatingWidgetSample() {
- // Attempting to press the box while it is settling to one anchor won't stop the box from
- // animating to that anchor. If you want to catch it while it is animating, you need to press
- // the box and drag it past the touchSlop. This is because startDragImmediately is set to false.
- val state =
- rememberSaveable(saver = AnchoredDraggableState.Saver()) {
- AnchoredDraggableState(initialValue = Start)
- }
- val density = LocalDensity.current
- val draggableSize = 100.dp
- val draggableSizePx = with(density) { draggableSize.toPx() }
- Box(
- Modifier.fillMaxWidth().onSizeChanged { layoutSize ->
- val dragEndPoint = layoutSize.width - draggableSizePx
- state.updateAnchors(
- DraggableAnchors {
- Start at 0f
- End at dragEndPoint
- }
- )
- }
- ) {
- Box(
- Modifier.size(draggableSize)
- .offset { IntOffset(x = state.requireOffset().roundToInt(), y = 0) }
- .anchoredDraggable(
- state = state,
- orientation = Orientation.Horizontal,
- startDragImmediately = false,
- flingBehavior =
- AnchoredDraggableDefaults.flingBehavior(
- state,
- positionalThreshold = { with(density) { 56.dp.toPx() } },
- // Setting the duration of the snapAnimationSpec to 3000ms gives more
- // time
- // to attempt to press or drag the settling box.
- animationSpec = tween(durationMillis = 3000)
- )
- )
- .background(Color.Red)
- )
- }
-}
-
-@Preview
-@Composable
fun AnchoredDraggableWithOverscrollSample() {
val state =
rememberSaveable(saver = AnchoredDraggableState.Saver()) {
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
index 133bf65..b5f57f0 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
@@ -23,12 +23,15 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyLayoutAnimateScrollScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -48,6 +51,7 @@
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -83,7 +87,7 @@
@Sampled
@Composable
-fun StickyHeaderSample() {
+fun StickyHeaderListSample() {
val sections = listOf("A", "B", "C", "D", "E", "F", "G")
LazyColumn(reverseLayout = true, contentPadding = PaddingValues(6.dp)) {
@@ -101,6 +105,25 @@
@Sampled
@Composable
+@Preview
+fun StickyHeaderGridSample() {
+ val sections = listOf("A", "B", "C", "D", "E", "F", "G")
+
+ LazyVerticalGrid(columns = GridCells.Fixed(3), contentPadding = PaddingValues(6.dp)) {
+ sections.forEach { section ->
+ stickyHeader {
+ Text(
+ "Section $section",
+ Modifier.fillMaxWidth().background(Color.LightGray).padding(8.dp)
+ )
+ }
+ items(10) { Text("Item= $it S=$section", modifier = Modifier.height(64.dp)) }
+ }
+ }
+}
+
+@Sampled
+@Composable
fun StickyHeaderHeaderIndexSample() {
/**
* Checks if [index] is in the sticking position, that is, it's the first visible item and its
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableBackwardsCompatibleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableBackwardsCompatibleTest.kt
index b11ce3a..e8dd00d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableBackwardsCompatibleTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableBackwardsCompatibleTest.kt
@@ -71,8 +71,6 @@
snapAnimationSpec = snapAnimationSpec,
decayAnimationSpec = decayAnimationSpec
)
- // This won't work for snapshot observation but should be ok for tests
- val resolvedStartDragImmediately = startDragImmediately ?: state.isAnimationRunning
val modifier =
if (testNewBehavior) {
val flingBehavior =
@@ -89,7 +87,7 @@
enabled = enabled,
interactionSource = interactionSource,
overscrollEffect = overscrollEffect,
- startDragImmediately = resolvedStartDragImmediately,
+ startDragImmediately = startDragImmediately,
flingBehavior = flingBehavior
)
} else {
@@ -100,7 +98,7 @@
enabled = enabled,
interactionSource = interactionSource,
overscrollEffect = overscrollEffect,
- startDragImmediately = resolvedStartDragImmediately,
+ startDragImmediately = startDragImmediately,
flingBehavior = null
)
}
@@ -168,31 +166,58 @@
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
overscrollEffect: OverscrollEffect? = null,
- startDragImmediately: Boolean = state.isAnimationRunning,
+ startDragImmediately: Boolean? = null,
flingBehavior: FlingBehavior? = null
- ) =
+ ): Modifier =
when (reverseDirection) {
null ->
- Modifier.anchoredDraggable(
- state = state,
- orientation = orientation,
- enabled = enabled,
- interactionSource = interactionSource,
- overscrollEffect = overscrollEffect,
- startDragImmediately = startDragImmediately,
- flingBehavior = flingBehavior
- )
+ when (startDragImmediately) {
+ null ->
+ Modifier.anchoredDraggable(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ overscrollEffect = overscrollEffect,
+ flingBehavior = flingBehavior
+ )
+ else ->
+ @Suppress("DEPRECATION")
+ Modifier.anchoredDraggable(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ overscrollEffect = overscrollEffect,
+ startDragImmediately = startDragImmediately,
+ flingBehavior = flingBehavior
+ )
+ }
else ->
- Modifier.anchoredDraggable(
- state = state,
- reverseDirection = reverseDirection,
- orientation = orientation,
- enabled = enabled,
- interactionSource = interactionSource,
- overscrollEffect = overscrollEffect,
- startDragImmediately = startDragImmediately,
- flingBehavior = flingBehavior
- )
+ when (startDragImmediately) {
+ null ->
+ Modifier.anchoredDraggable(
+ state = state,
+ reverseDirection = reverseDirection,
+ orientation = orientation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ overscrollEffect = overscrollEffect,
+ flingBehavior = flingBehavior
+ )
+ else ->
+ @Suppress("DEPRECATION")
+ Modifier.anchoredDraggable(
+ state = state,
+ reverseDirection = reverseDirection,
+ orientation = orientation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ overscrollEffect = overscrollEffect,
+ startDragImmediately = startDragImmediately,
+ flingBehavior = flingBehavior
+ )
+ }
}
/**
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
index 97adac1..6e7154a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -493,8 +493,9 @@
assertThat(state.targetValue).isEqualTo(B)
}
+ // TODO(b/360835763): Remove when removing the old overload
@Test
- fun anchoredDraggable_animationNotCancelledByDrag_startDragImmediatelyIsFalse() {
+ fun anchoredDraggable_startDragImmediately_false_animationNotCancelledByDrag() {
rule.mainClock.autoAdvance = false
val anchors = DraggableAnchors {
A at 0f
@@ -540,6 +541,52 @@
}
@Test
+ fun anchoredDraggable_startDragImmediately_default_processesWithoutSlopWhileAnimating() {
+ rule.mainClock.autoAdvance = false
+ val anchors = DraggableAnchors {
+ A at 0f
+ B at 250f
+ C at 500f
+ }
+ val (state, modifier) =
+ createStateAndModifier(
+ initialValue = A,
+ anchors = anchors,
+ orientation = Orientation.Horizontal,
+ )
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ WithTouchSlop(touchSlop = 5000f) {
+ scope = rememberCoroutineScope()
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier.requiredSize(AnchoredDraggableBoxSize)
+ .testTag(AnchoredDraggableTestTag)
+ .then(modifier)
+ .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
+ .background(Color.Red)
+ )
+ }
+ }
+ }
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ scope.launch { state.animateTo(C) }
+
+ rule.mainClock.advanceTimeUntil { state.requireOffset() > 10 }
+ val offsetBeforeTouch = state.requireOffset()
+
+ rule.onNodeWithTag(AnchoredDraggableTestTag).performTouchInput {
+ down(Offset.Zero)
+ moveBy(Offset(x = 15f, y = 0f))
+ }
+ // rule.mainClock.advanceTimeByFrame()
+ assertThat(state.requireOffset()).isEqualTo(offsetBeforeTouch + 15f)
+ rule.waitForIdle()
+ }
+
+ @Test
fun anchoredDraggable_updatesState() {
val state1 =
createAnchoredDraggableState(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BackspaceCommandTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BackspaceCommandTest.kt
index c54694e..a9cf3a6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BackspaceCommandTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BackspaceCommandTest.kt
@@ -16,6 +16,8 @@
package androidx.compose.foundation.text.input.internal
+import androidx.compose.foundation.text.input.TextFieldBuffer
+import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.ui.text.TextRange
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
@@ -40,92 +42,105 @@
@Test
fun test_delete() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("BCDE")
- Truth.assertThat(eb.cursor).isEqualTo(0)
+ Truth.assertThat(eb.selection.start).isEqualTo(0)
+ Truth.assertThat(eb.selection.end).isEqualTo(0)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_from_offset0() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(0)
+ Truth.assertThat(eb.selection.start).isEqualTo(0)
+ Truth.assertThat(eb.selection.end).isEqualTo(0)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_with_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(2, 3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(2, 3))
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("ABDE")
- Truth.assertThat(eb.cursor).isEqualTo(2)
+ Truth.assertThat(eb.selection.start).isEqualTo(2)
+ Truth.assertThat(eb.selection.end).isEqualTo(2)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_with_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.setComposition(2, 3)
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("ABDE")
- Truth.assertThat(eb.cursor).isEqualTo(1)
+ Truth.assertThat(eb.selection.start).isEqualTo(1)
+ Truth.assertThat(eb.selection.end).isEqualTo(1)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_surrogate_pair() {
- val eb = EditingBuffer("$SP1$SP2$SP3$SP4$SP5", TextRange(2))
+ val eb = TextFieldBuffer("$SP1$SP2$SP3$SP4$SP5", TextRange(2))
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("$SP2$SP3$SP4$SP5")
- Truth.assertThat(eb.cursor).isEqualTo(0)
+ Truth.assertThat(eb.selection.start).isEqualTo(0)
+ Truth.assertThat(eb.selection.end).isEqualTo(0)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_with_selection_surrogate_pair() {
- val eb = EditingBuffer("$SP1$SP2$SP3$SP4$SP5", TextRange(4, 6))
+ val eb = TextFieldBuffer("$SP1$SP2$SP3$SP4$SP5", TextRange(4, 6))
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("$SP1$SP2$SP4$SP5")
- Truth.assertThat(eb.cursor).isEqualTo(4)
+ Truth.assertThat(eb.selection.start).isEqualTo(4)
+ Truth.assertThat(eb.selection.end).isEqualTo(4)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_with_composition_surrogate_pair() {
- val eb = EditingBuffer("$SP1$SP2$SP3$SP4$SP5", TextRange(2))
+ val eb = TextFieldBuffer("$SP1$SP2$SP3$SP4$SP5", TextRange(2))
eb.setComposition(4, 6)
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo("$SP1$SP2$SP4$SP5")
- Truth.assertThat(eb.cursor).isEqualTo(2)
+ Truth.assertThat(eb.selection.start).isEqualTo(2)
+ Truth.assertThat(eb.selection.end).isEqualTo(2)
Truth.assertThat(eb.hasComposition()).isFalse()
}
@Test
@SdkSuppress(minSdkVersion = 26)
fun test_delete_with_composition_zwj_emoji() {
- val eb = EditingBuffer("$ZWJ_EMOJI$ZWJ_EMOJI", TextRange(ZWJ_EMOJI.length))
+ val eb = TextFieldBuffer("$ZWJ_EMOJI$ZWJ_EMOJI", TextRange(ZWJ_EMOJI.length))
eb.backspace()
Truth.assertThat(eb.toString()).isEqualTo(ZWJ_EMOJI)
- Truth.assertThat(eb.cursor).isEqualTo(0)
+ Truth.assertThat(eb.selection.start).isEqualTo(0)
+ Truth.assertThat(eb.selection.end).isEqualTo(0)
Truth.assertThat(eb.hasComposition()).isFalse()
}
}
+
+internal fun TextFieldBuffer(
+ initialValue: String = "",
+ initialSelection: TextRange = TextRange.Zero
+) = TextFieldBuffer(TextFieldCharSequence(initialValue, initialSelection))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/MoveCursorCommandTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/MoveCursorCommandTest.kt
deleted file mode 100644
index 4d5a9c3..0000000
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/MoveCursorCommandTest.kt
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text.input.internal
-
-import androidx.compose.ui.text.TextRange
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class MoveCursorCommandTest {
- private val CH1 = "\uD83D\uDE00" // U+1F600
- private val CH2 = "\uD83D\uDE01" // U+1F601
- private val CH3 = "\uD83D\uDE02" // U+1F602
- private val CH4 = "\uD83D\uDE03" // U+1F603
- private val CH5 = "\uD83D\uDE04" // U+1F604
-
- // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
- private val FAMILY = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"
-
- @Test
- fun test_left() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
-
- eb.moveCursor(-1)
-
- Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(2)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_left_multiple() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
-
- eb.moveCursor(-2)
-
- Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(1)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_left_from_offset0() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.moveCursor(-1)
-
- Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(0)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_right() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
-
- eb.moveCursor(1)
-
- Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(4)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_right_multiple() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
-
- eb.moveCursor(2)
-
- Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(5)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_right_from_offset_length() {
- val eb = EditingBuffer("ABCDE", TextRange(5))
-
- eb.moveCursor(1)
-
- Truth.assertThat(eb.toString()).isEqualTo("ABCDE")
- Truth.assertThat(eb.cursor).isEqualTo(5)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_left_surrogate_pair() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
-
- eb.moveCursor(-1)
-
- Truth.assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH3$CH4$CH5")
- Truth.assertThat(eb.cursor).isEqualTo(4)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_left_multiple_surrogate_pair() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
-
- eb.moveCursor(-2)
-
- Truth.assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH3$CH4$CH5")
- Truth.assertThat(eb.cursor).isEqualTo(2)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_right_surrogate_pair() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
-
- eb.moveCursor(1)
-
- Truth.assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH3$CH4$CH5")
- Truth.assertThat(eb.cursor).isEqualTo(8)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun test_right_multiple_surrogate_pair() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
-
- eb.moveCursor(2)
-
- Truth.assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH3$CH4$CH5")
- Truth.assertThat(eb.cursor).isEqualTo(10)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 26)
- fun test_left_emoji() {
- val eb = EditingBuffer("$FAMILY$FAMILY", TextRange(FAMILY.length))
-
- eb.moveCursor(-1)
-
- Truth.assertThat(eb.toString()).isEqualTo("$FAMILY$FAMILY")
- Truth.assertThat(eb.cursor).isEqualTo(0)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 26)
- fun test_right_emoji() {
- val eb = EditingBuffer("$FAMILY$FAMILY", TextRange(FAMILY.length))
-
- eb.moveCursor(1)
-
- Truth.assertThat(eb.toString()).isEqualTo("$FAMILY$FAMILY")
- Truth.assertThat(eb.cursor).isEqualTo(2 * FAMILY.length)
- Truth.assertThat(eb.hasComposition()).isFalse()
- }
-}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnectionTest.kt
index 1375112..9c99e9a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnectionTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnectionTest.kt
@@ -38,6 +38,7 @@
import android.view.inputmethod.PreviewableHandwritingGesture
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.TransferableContent
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -83,7 +84,7 @@
[email protected]?.invoke(imeAction)
}
- override fun requestEdit(block: EditingBuffer.() -> Unit) {
+ override fun requestEdit(block: TextFieldBuffer.() -> Unit) {
onRequestEdit?.invoke(block)
}
@@ -118,7 +119,7 @@
state = TextFieldState(value.toString(), value.selection)
}
- private var onRequestEdit: ((EditingBuffer.() -> Unit) -> Unit)? = null
+ private var onRequestEdit: ((TextFieldBuffer.() -> Unit) -> Unit)? = null
private var onSendKeyEvent: ((KeyEvent) -> Unit)? = null
private var onImeAction: ((ImeAction) -> Unit)? = null
private var onCommitContent: ((TransferableContent) -> Boolean)? = null
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.jpg b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.jpg
new file mode 100644
index 0000000..628b50a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.jpg
Binary files differ
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.png b/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.png
deleted file mode 100644
index 4eb583c..0000000
--- a/compose/foundation/foundation/src/androidInstrumentedTest/res/drawable-hdpi/ic_image_test.png
+++ /dev/null
Binary files differ
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
index 620e0af..9fade05 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
@@ -29,6 +29,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.TransferableContent
import androidx.compose.foundation.content.internal.ReceiveContentConfiguration
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.internal.HandwritingGestureApi34.performHandwritingGesture
import androidx.compose.foundation.text.input.internal.HandwritingGestureApi34.previewHandwritingGesture
@@ -133,7 +134,7 @@
override val text: TextFieldCharSequence
get() = state.visualText
- override fun requestEdit(block: EditingBuffer.() -> Unit) {
+ override fun requestEdit(block: TextFieldBuffer.() -> Unit) {
state.editUntransformedTextAsUser(
restartImeIfContentChanges = false,
block = block
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnection.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnection.android.kt
index 94694b3..67d8df3 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnection.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/StatelessInputConnection.android.kt
@@ -49,6 +49,7 @@
import androidx.compose.foundation.content.PlatformTransferableContent
import androidx.compose.foundation.content.TransferableContent
import androidx.compose.foundation.text.input.PlacedAnnotation
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.getSelectedText
import androidx.compose.foundation.text.input.getTextAfterSelection
@@ -109,7 +110,7 @@
get() = session.text
/** Recording of editing operations for batch editing */
- private val editCommands = mutableVectorOf<EditingBuffer.() -> Unit>()
+ private val editCommands = mutableVectorOf<TextFieldBuffer.() -> Unit>()
/**
* Wraps this StatelessInputConnection to halt a possible infinite loop in [commitContent]
@@ -211,7 +212,7 @@
* mini batches for every edit op. These batches are only applied when batch depth reaches 0,
* meaning that artificial batches won't be applied until the real batches are completed.
*/
- private fun addEditCommandWithBatch(editCommand: EditingBuffer.() -> Unit) {
+ private fun addEditCommandWithBatch(editCommand: TextFieldBuffer.() -> Unit) {
beginBatchEditInternal()
try {
editCommands.add(editCommand)
@@ -296,7 +297,7 @@
override fun setSelection(start: Int, end: Int): Boolean {
logDebug("setSelection($start, $end)")
- addEditCommandWithBatch { setSelection(start, end) }
+ addEditCommandWithBatch { [email protected](start, end) }
return true
}
@@ -407,7 +408,9 @@
logDebug("performContextMenuAction($id)")
when (id) {
android.R.id.selectAll -> {
- addEditCommandWithBatch { setSelection(0, text.length) }
+ addEditCommandWithBatch {
+ [email protected](0, text.length)
+ }
}
// TODO(siyamed): Need proper connection to cut/copy/paste
android.R.id.cut -> sendSynthesizedKeyEvent(KeyEvent.KEYCODE_CUT)
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.android.kt
index e06d498..446eaa8 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.android.kt
@@ -23,6 +23,7 @@
import android.view.inputmethod.PreviewableHandwritingGesture
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.TransferableContent
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.text.input.ImeAction
@@ -46,7 +47,7 @@
*
* @param block Lambda scoped to an EditingBuffer to apply changes direct onto a buffer.
*/
- fun requestEdit(block: EditingBuffer.() -> Unit)
+ fun requestEdit(block: TextFieldBuffer.() -> Unit)
/** Delegates IME requested KeyEvents. */
fun sendKeyEvent(keyEvent: KeyEvent)
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/CommitTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/CommitTextCommandTest.kt
index 9d35316..e2d8ca2 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/CommitTextCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/CommitTextCommandTest.kt
@@ -27,129 +27,140 @@
@Test
fun test_insert_empty() {
- val eb = EditingBuffer("", TextRange.Zero)
+ val eb = TextFieldBuffer("", TextRange.Zero)
eb.commitText("X", 1)
assertThat(eb.toString()).isEqualTo("X")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_cursor_tail() {
- val eb = EditingBuffer("A", TextRange(1))
+ val eb = TextFieldBuffer("A", TextRange(1))
eb.commitText("X", 1)
assertThat(eb.toString()).isEqualTo("AX")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_cursor_head() {
- val eb = EditingBuffer("A", TextRange(1))
+ val eb = TextFieldBuffer("A", TextRange(1))
eb.commitText("X", 0)
assertThat(eb.toString()).isEqualTo("AX")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_cursor_far_tail() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.commitText("X", 2)
assertThat(eb.toString()).isEqualTo("AXBCDE")
- assertThat(eb.cursor).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_cursor_far_head() {
- val eb = EditingBuffer("ABCDE", TextRange(4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(4))
eb.commitText("X", -2)
assertThat(eb.toString()).isEqualTo("ABCDXE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_head() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.commitText("", 0)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_tail() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.commitText("", 1)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_far_tail() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.commitText("", 2)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_far_head() {
- val eb = EditingBuffer("ABCDE", TextRange(4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(4))
eb.commitText("", -2)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_cancel_composition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposition(1, 4) // Mark "BCD" as composition
eb.commitText("X", 1)
assertThat(eb.toString()).isEqualTo("AXE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_replace_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 4)) // select "BCD"
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 4)) // select "BCD"
eb.commitText("X", 1)
assertThat(eb.toString()).isEqualTo("AXE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_composition_and_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 3)) // select "BC"
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 3)) // select "BC"
eb.setComposition(2, 4) // Mark "CD" as composition
eb.commitText("X", 1)
@@ -157,29 +168,32 @@
// If composition and selection exists at the same time, replace composition and cancel
// selection and place cursor.
assertThat(eb.toString()).isEqualTo("ABXE")
- assertThat(eb.cursor).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_cursor_position_too_small() {
- val eb = EditingBuffer("ABCDE", TextRange(5))
+ val eb = TextFieldBuffer("ABCDE", TextRange(5))
eb.commitText("X", -1000)
assertThat(eb.toString()).isEqualTo("ABCDEX")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_cursor_position_too_large() {
- val eb = EditingBuffer("ABCDE", TextRange(5))
+ val eb = TextFieldBuffer("ABCDE", TextRange(5))
eb.commitText("X", 1000)
assertThat(eb.toString()).isEqualTo("ABCDEX")
- assertThat(eb.cursor).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(6)
+ assertThat(eb.selection.end).isEqualTo(6)
assertThat(eb.hasComposition()).isFalse()
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextCommandTest.kt
index ef45222..193dcc2 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextCommandTest.kt
@@ -28,201 +28,216 @@
@Test
fun test_delete_after() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.deleteSurroundingText(0, 1)
assertThat(eb.toString()).isEqualTo("ACDE")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_before() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.deleteSurroundingText(1, 0)
assertThat(eb.toString()).isEqualTo("BCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_both() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_after_multiple() {
- val eb = EditingBuffer("ABCDE", TextRange(2))
+ val eb = TextFieldBuffer("ABCDE", TextRange(2))
eb.deleteSurroundingText(0, 2)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_before_multiple() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.deleteSurroundingText(2, 0)
assertThat(eb.toString()).isEqualTo("ADE")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_both_multiple() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.deleteSurroundingText(2, 2)
assertThat(eb.toString()).isEqualTo("A")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_selection_preserve() {
- val eb = EditingBuffer("ABCDE", TextRange(2, 4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(2, 4))
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ACD")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(3)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_before_too_many() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.deleteSurroundingText(1000, 0)
assertThat(eb.toString()).isEqualTo("DE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_after_too_many() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.deleteSurroundingText(0, 1000)
assertThat(eb.toString()).isEqualTo("ABC")
- assertThat(eb.cursor).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_both_too_many() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.deleteSurroundingText(1000, 1000)
assertThat(eb.toString()).isEqualTo("")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_composition_no_intersection_preceding_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.setComposition(0, 1)
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(1)
}
@Test
fun test_delete_composition_no_intersection_trailing_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.setComposition(4, 5)
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(3)
}
@Test
fun test_delete_composition_intersection_preceding_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.setComposition(0, 3)
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_delete_composition_intersection_trailing_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.setComposition(3, 5)
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(3)
}
@Test
fun test_delete_covered_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.setComposition(2, 3)
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_composition_covered() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
eb.setComposition(0, 5)
eb.deleteSurroundingText(1, 1)
assertThat(eb.toString()).isEqualTo("ABE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(3)
}
@Test
fun throws_whenLengthBeforeInvalid() {
- val eb = EditingBuffer("", TextRange(0))
+ val eb = TextFieldBuffer("", TextRange(0))
val error =
assertFailsWith<IllegalArgumentException> {
eb.deleteSurroundingText(lengthBeforeCursor = -42, lengthAfterCursor = 0)
@@ -232,7 +247,7 @@
@Test
fun throws_whenLengthAfterInvalid() {
- val eb = EditingBuffer("", TextRange(0))
+ val eb = TextFieldBuffer("", TextRange(0))
val error =
assertFailsWith<IllegalArgumentException> {
eb.deleteSurroundingText(lengthBeforeCursor = 0, lengthAfterCursor = -42)
@@ -245,24 +260,26 @@
val text = "abcde"
val textAfterDelete = "abcd"
val selection = TextRange(textAfterDelete.length)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingText(lengthBeforeCursor = 0, lengthAfterCursor = Int.MAX_VALUE)
assertThat(eb.toString()).isEqualTo(textAfterDelete)
- assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.start).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.end).isEqualTo(textAfterDelete.length)
}
@Test
fun deletes_whenLengthBeforeCursorOverflows_withMaxValue() {
val text = "abcde"
val selection = TextRange(1)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingText(lengthBeforeCursor = Int.MAX_VALUE, lengthAfterCursor = 0)
assertThat(eb.toString()).isEqualTo("bcde")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
}
@Test
@@ -270,23 +287,25 @@
val text = "abcde"
val textAfterDelete = "abcd"
val selection = TextRange(textAfterDelete.length)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingText(lengthBeforeCursor = 0, lengthAfterCursor = Int.MAX_VALUE - 1)
assertThat(eb.toString()).isEqualTo(textAfterDelete)
- assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.start).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.end).isEqualTo(textAfterDelete.length)
}
@Test
fun deletes_whenLengthBeforeCursorOverflows() {
val text = "abcde"
val selection = TextRange(1)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingText(lengthBeforeCursor = Int.MAX_VALUE - 1, lengthAfterCursor = 0)
assertThat(eb.toString()).isEqualTo("bcde")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt
index 3f4e8f8..de8cefa 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/DeleteSurroundingTextInCodePointsCommandTest.kt
@@ -33,201 +33,216 @@
@Test
fun test_delete_after() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(2))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(2))
eb.deleteSurroundingTextInCodePoints(0, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH3$CH4$CH5")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_before() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(2))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(2))
eb.deleteSurroundingTextInCodePoints(1, 0)
assertThat(eb.toString()).isEqualTo("$CH2$CH3$CH4$CH5")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_both() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_after_multiple() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(4))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(4))
eb.deleteSurroundingTextInCodePoints(0, 2)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_before_multiple() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.deleteSurroundingTextInCodePoints(2, 0)
assertThat(eb.toString()).isEqualTo("$CH1$CH4$CH5")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_both_multiple() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.deleteSurroundingTextInCodePoints(2, 2)
assertThat(eb.toString()).isEqualTo(CH1)
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_selection_preserve() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(4, 8))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(4, 8))
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH3$CH4")
- assertThat(eb.selectionStart).isEqualTo(2)
- assertThat(eb.selectionEnd).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(6)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_before_too_many() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.deleteSurroundingTextInCodePoints(1000, 0)
assertThat(eb.toString()).isEqualTo("$CH4$CH5")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_after_too_many() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.deleteSurroundingTextInCodePoints(0, 1000)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH3")
- assertThat(eb.cursor).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(6)
+ assertThat(eb.selection.end).isEqualTo(6)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_both_too_many() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.deleteSurroundingTextInCodePoints(1000, 1000)
assertThat(eb.toString()).isEqualTo("")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_composition_no_intersection_preceding_composition() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.setComposition(0, 2)
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_delete_composition_no_intersection_trailing_composition() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.setComposition(8, 10)
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
- assertThat(eb.compositionStart).isEqualTo(4)
- assertThat(eb.compositionEnd).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(4)
+ assertThat(eb.composition?.end).isEqualTo(6)
}
@Test
fun test_delete_composition_intersection_preceding_composition() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.setComposition(0, 6)
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(4)
}
@Test
fun test_delete_composition_intersection_trailing_composition() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.setComposition(6, 10)
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
- assertThat(eb.compositionStart).isEqualTo(4)
- assertThat(eb.compositionEnd).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(4)
+ assertThat(eb.composition?.end).isEqualTo(6)
}
@Test
fun test_delete_covered_composition() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.setComposition(4, 6)
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_delete_composition_covered() {
- val eb = EditingBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
+ val eb = TextFieldBuffer("$CH1$CH2$CH3$CH4$CH5", TextRange(6))
eb.setComposition(0, 10)
eb.deleteSurroundingTextInCodePoints(1, 1)
assertThat(eb.toString()).isEqualTo("$CH1$CH2$CH5")
- assertThat(eb.cursor).isEqualTo(4)
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(6)
}
@Test
fun throws_whenLengthBeforeInvalid() {
- val eb = EditingBuffer("", TextRange(0))
+ val eb = TextFieldBuffer("", TextRange(0))
val error =
assertFailsWith<IllegalArgumentException> {
eb.deleteSurroundingTextInCodePoints(
@@ -240,7 +255,7 @@
@Test
fun throws_whenLengthAfterInvalid() {
- val eb = EditingBuffer("", TextRange(0))
+ val eb = TextFieldBuffer("", TextRange(0))
val error =
assertFailsWith<IllegalArgumentException> {
eb.deleteSurroundingTextInCodePoints(
@@ -256,7 +271,7 @@
val text = "abcde"
val textAfterDelete = "abcd"
val selection = TextRange(textAfterDelete.length)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingTextInCodePoints(
lengthBeforeCursor = 0,
@@ -264,14 +279,15 @@
)
assertThat(eb.toString()).isEqualTo(textAfterDelete)
- assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.start).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.end).isEqualTo(textAfterDelete.length)
}
@Test
fun deletes_whenLengthBeforeCursorOverflows_withMaxValue() {
val text = "abcde"
val selection = TextRange(1)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingTextInCodePoints(
lengthBeforeCursor = Int.MAX_VALUE,
@@ -279,14 +295,15 @@
)
assertThat(eb.toString()).isEqualTo("bcde")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
}
@Test
fun deletes_whenBothOverflow_withMaxValue_cursorAtStart() {
val text = "abcde"
val selection = TextRange(0)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingTextInCodePoints(
lengthBeforeCursor = Int.MAX_VALUE,
@@ -294,14 +311,15 @@
)
assertThat(eb.toString()).isEqualTo("")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
}
@Test
fun deletes_whenBothOverflow_withMaxValue_cursorAtEnd() {
val text = "abcde"
val selection = TextRange(5)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingTextInCodePoints(
lengthBeforeCursor = Int.MAX_VALUE,
@@ -309,7 +327,8 @@
)
assertThat(eb.toString()).isEqualTo("")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
}
@Test
@@ -317,7 +336,7 @@
val text = "abcde"
val textAfterDelete = "abcd"
val selection = TextRange(textAfterDelete.length)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingTextInCodePoints(
lengthBeforeCursor = 0,
@@ -325,14 +344,15 @@
)
assertThat(eb.toString()).isEqualTo(textAfterDelete)
- assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.start).isEqualTo(textAfterDelete.length)
+ assertThat(eb.selection.end).isEqualTo(textAfterDelete.length)
}
@Test
fun deletes_whenLengthBeforeCursorOverflows() {
val text = "abcde"
val selection = TextRange(1)
- val eb = EditingBuffer(text, selection)
+ val eb = TextFieldBuffer(text, selection)
eb.deleteSurroundingTextInCodePoints(
lengthBeforeCursor = Int.MAX_VALUE - 1,
@@ -340,6 +360,7 @@
)
assertThat(eb.toString()).isEqualTo("bcde")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferChangeTrackingTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferChangeTrackingTest.kt
deleted file mode 100644
index 110d2d7..0000000
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferChangeTrackingTest.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text.input.internal
-
-import androidx.compose.ui.text.TextRange
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class EditingBufferChangeTrackingTest {
-
- @Test
- fun normalReplaceOperation_reportedAsReplace() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(2, 4, "bfghi")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(2, 4))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(2, 7))
- }
-
- @Test
- fun tailInsertionReportedAsReplace_coercesToInsertion() {
- val eb = EditingBuffer("abcd", TextRange.Zero)
-
- eb.replace(2, 4, "cde")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(4))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(4, 5))
- }
-
- @Test
- fun headInsertionReportedAsReplace_coercesToInsertion() {
- val eb = EditingBuffer("abcd", TextRange.Zero)
-
- eb.replace(0, 4, "eabcd")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(0))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(0, 1))
- }
-
- @Test
- fun tailInsertionInTheMiddle_reportedAsReplace_coercesToInsertion() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(1, 3, "bcef")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(3))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(3, 5))
- }
-
- @Test
- fun headInsertionInTheMiddle_reportedAsReplace_coercesToInsertion() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(2, 4, "fgcd")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(2))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(2, 4))
- }
-
- @Test
- fun tailInsertionAtTheEnd_reportedAsFullReplace_coercesToInsertion() {
- val eb = EditingBuffer("abc", TextRange.Zero)
-
- eb.replace(0, 3, "abcd")
-
- assertThat(eb.toString()).isEqualTo("abcd")
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(3, 4))
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(3))
- }
-
- @Test
- fun tailInsertionAtTheEnd_reportedAsFullReplace_sameLastCharacter_coercesToInsertion() {
- val eb = EditingBuffer("abc", TextRange.Zero)
-
- eb.replace(0, 3, "abcc")
-
- assertThat(eb.toString()).isEqualTo("abcc")
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(3, 4))
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(3))
- }
-
- @Test
- fun tailDeletionReportedAsReplace_coercesToDeletion() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(0, 5, "abcd")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(4, 5))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(4))
- }
-
- @Test
- fun headDeletionReportedAsReplace_coercesToDeletion() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(0, 5, "bcde")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(0, 1))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(0))
- }
-
- @Test
- fun tailDeletionInTheMiddle_reportedAsReplace_coercesToDeletion() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(1, 4, "b")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(2, 4))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(2))
- }
-
- @Test
- fun headDeletionInTheMiddle_reportedAsReplace_coercesToDeletion() {
- val eb = EditingBuffer("abcde", TextRange.Zero)
-
- eb.replace(1, 4, "d")
-
- assertThat(eb.changeTracker.changeCount).isEqualTo(1)
- assertThat(eb.changeTracker.getOriginalRange(0)).isEqualTo(TextRange(1, 3))
- assertThat(eb.changeTracker.getRange(0)).isEqualTo(TextRange(1))
- }
-}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferTest.kt
deleted file mode 100644
index 4d0c59f..0000000
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferTest.kt
+++ /dev/null
@@ -1,596 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text.input.internal
-
-import androidx.compose.foundation.text.input.PlacedAnnotation
-import androidx.compose.foundation.text.input.TextHighlightType
-import androidx.compose.foundation.text.input.internal.matchers.assertThat
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.style.TextDecoration
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class EditingBufferTest {
-
- @Test
- fun insert() {
- val eb = EditingBuffer("", TextRange.Zero)
-
- eb.replace(0, 0, "A")
-
- assertThat(eb).hasChars("A")
- assertThat(eb.cursor).isEqualTo(1)
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(1)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- // Keep inserting text to the end of string. Cursor should follow.
- eb.replace(1, 1, "BC")
- assertThat(eb).hasChars("ABC")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- // Insert into middle position. Cursor should be end of inserted text.
- eb.replace(1, 1, "D")
- assertThat(eb).hasChars("ADBC")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.selectionStart).isEqualTo(2)
- assertThat(eb.selectionEnd).isEqualTo(2)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- }
-
- @Test
- fun delete() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.replace(0, 1, "")
-
- // Delete the left character at the cursor.
- assertThat(eb).hasChars("BCDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- // Delete the text before the cursor
- eb.replace(0, 2, "")
- assertThat(eb).hasChars("DE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- // Delete end of the text.
- eb.replace(1, 2, "")
- assertThat(eb).hasChars("D")
- assertThat(eb.cursor).isEqualTo(1)
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(1)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- }
-
- @Test
- fun setSelection() {
- val eb = EditingBuffer("ABCDE", TextRange(0, 3))
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(-1)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.setSelection(0, 5) // Change the selection
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(-1)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(5)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.replace(0, 3, "X") // replace function cancel the selection and place cursor.
- assertThat(eb).hasChars("XDE")
- assertThat(eb.cursor).isEqualTo(1)
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(1)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.setSelection(0, 2) // Set the selection again
- assertThat(eb).hasChars("XDE")
- assertThat(eb.cursor).isEqualTo(-1)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(2)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- }
-
- @Test
- fun setSelection_coerces_whenNegativeStart() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setSelection(-1, 1)
-
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(1)
- }
-
- @Test
- fun setSelection_coerces_whenNegativeEnd() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setSelection(1, -1)
-
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(0)
- }
-
- @Test
- fun setSelection_allowReversedSelection() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
- eb.setSelection(4, 2)
-
- assertThat(eb.selection).isEqualTo(TextRange(4, 2))
- }
-
- @Test
- fun replace_reversedRegion() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
- eb.replace(3, 1, "FGHI")
-
- assertThat(eb).hasChars("AFGHIDE")
- assertThat(eb.cursor).isEqualTo(5)
- assertThat(eb.selectionStart).isEqualTo(5)
- assertThat(eb.selectionEnd).isEqualTo(5)
- }
-
- @Test
- fun setComposition_and_cancelComposition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(0, 5) // Make all text as composition
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(5)
-
- eb.replace(2, 3, "X") // replace function cancel the composition text.
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.setComposition(2, 4) // set composition again
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(4)
- }
-
- @Test
- fun setComposition_and_commitComposition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(0, 5) // Make all text as composition
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(5)
-
- eb.replace(2, 3, "X") // replace function cancel the composition text.
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.setComposition(2, 4) // set composition again
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(4)
-
- eb.commitComposition() // commit the composition
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- }
-
- @Test
- fun setComposition_and_annotationList() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- val annotations: List<PlacedAnnotation> =
- listOf(
- AnnotatedString.Range(SpanStyle(textDecoration = TextDecoration.Underline), 0, 5)
- )
- eb.setComposition(0, 5, annotations)
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(5)
- assertThat(eb.composingAnnotations?.size).isEqualTo(1)
- assertThat(eb.composingAnnotations?.first()).isEqualTo(annotations.first())
-
- eb.replace(2, 3, "X") // replace function cancel the composition text.
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- assertThat(eb.composingAnnotations?.isEmpty()).isTrue()
-
- eb.setComposition(2, 4) // set composition again
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(4)
- assertThat(eb.composingAnnotations?.isEmpty()).isTrue()
-
- eb.setComposition(0, 5, annotations)
- assertThat(eb.composingAnnotations?.size).isEqualTo(1)
- assertThat(eb.composingAnnotations?.first()).isEqualTo(annotations.first())
-
- eb.commitComposition() // commit the composition
- assertThat(eb).hasChars("ABXDE")
- assertThat(eb.cursor).isEqualTo(3)
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(3)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- assertThat(eb.composingAnnotations?.isEmpty()).isTrue()
- }
-
- @Test
- fun setCursor_and_get_cursor() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.cursor = 1
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(1)
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(1)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.cursor = 2
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.selectionStart).isEqualTo(2)
- assertThat(eb.selectionEnd).isEqualTo(2)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
-
- eb.cursor = 5
- assertThat(eb).hasChars("ABCDE")
- assertThat(eb.cursor).isEqualTo(5)
- assertThat(eb.selectionStart).isEqualTo(5)
- assertThat(eb.selectionEnd).isEqualTo(5)
- assertThat(eb.hasComposition()).isFalse()
- assertThat(eb.compositionStart).isEqualTo(-1)
- assertThat(eb.compositionEnd).isEqualTo(-1)
- }
-
- @Test
- fun delete_preceding_cursor_no_composition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.delete(1, 2)
- assertThat(eb).hasChars("ACDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun delete_trailing_cursor_no_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(3))
-
- eb.delete(1, 2)
- assertThat(eb).hasChars("ACDE")
- assertThat(eb.cursor).isEqualTo(2)
- assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun delete_preceding_selection_no_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(0, 1))
-
- eb.delete(1, 2)
- assertThat(eb).hasChars("ACDE")
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(1)
- assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun delete_trailing_selection_no_composition() {
- val eb = EditingBuffer("ABCDE", TextRange(4, 5))
-
- eb.delete(1, 2)
- assertThat(eb).hasChars("ACDE")
- assertThat(eb.selectionStart).isEqualTo(3)
- assertThat(eb.selectionEnd).isEqualTo(4)
- assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun delete_covered_cursor() {
- // AB[]CDE
- val eb = EditingBuffer("ABCDE", TextRange(2, 2))
-
- eb.delete(1, 3)
- // A[]DE
- assertThat(eb).hasChars("ADE")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(1)
- }
-
- @Test
- fun delete_covered_selection() {
- // A[BC]DE
- val eb = EditingBuffer("ABCDE", TextRange(1, 3))
-
- eb.delete(0, 4)
- // []E
- assertThat(eb).hasChars("E")
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- }
-
- @Test
- fun delete_covered_reversedSelection() {
- // A[BC]DE
- val eb = EditingBuffer("ABCDE", TextRange(3, 1))
-
- eb.delete(0, 4)
- // []E
- assertThat(eb).hasChars("E")
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(0)
- }
-
- @Test
- fun delete_intersects_first_half_of_selection() {
- // AB[CD]E
- val eb = EditingBuffer("ABCDE", TextRange(2, 4))
-
- eb.delete(1, 3)
- // A[D]E
- assertThat(eb).hasChars("ADE")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(2)
- }
-
- @Test
- fun delete_intersects_first_half_of_reversedSelection() {
- // AB[CD]E
- val eb = EditingBuffer("ABCDE", TextRange(4, 2))
-
- eb.delete(3, 1)
- // A[D]E
- assertThat(eb).hasChars("ADE")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(2)
- }
-
- @Test
- fun delete_intersects_second_half_of_selection() {
- // A[BCD]EFG
- val eb = EditingBuffer("ABCDEFG", TextRange(1, 4))
-
- eb.delete(3, 5)
- // A[BC]FG
- assertThat(eb).hasChars("ABCFG")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(3)
- }
-
- @Test
- fun delete_intersects_second_half_of_reversedSelection() {
- // A[BCD]EFG
- val eb = EditingBuffer("ABCDEFG", TextRange(4, 1))
-
- eb.delete(5, 3)
- // A[BC]FG
- assertThat(eb).hasChars("ABCFG")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(3)
- }
-
- @Test
- fun delete_preceding_composition_no_intersection() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(1, 2)
- eb.delete(2, 3)
-
- assertThat(eb).hasChars("ABDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
- }
-
- @Test
- fun delete_trailing_composition_no_intersection() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(3, 4)
- eb.delete(2, 3)
-
- assertThat(eb).hasChars("ABDE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(3)
- }
-
- @Test
- fun delete_preceding_composition_intersection() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(1, 3)
- eb.delete(2, 4)
-
- assertThat(eb).hasChars("ABE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
- }
-
- @Test
- fun delete_trailing_composition_intersection() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(3, 5)
- eb.delete(2, 4)
-
- assertThat(eb).hasChars("ABE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(3)
- }
-
- @Test
- fun delete_composition_contains_delrange() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(2, 5)
- eb.delete(3, 4)
-
- assertThat(eb).hasChars("ABCE")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(4)
- }
-
- @Test
- fun delete_delrange_contains_composition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setComposition(3, 4)
- eb.delete(2, 5)
-
- assertThat(eb).hasChars("AB")
- assertThat(eb.cursor).isEqualTo(0)
- assertThat(eb.hasComposition()).isFalse()
- }
-
- @Test
- fun setHighlight_clearHighlight() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
- assertThat(eb.highlight)
- .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
-
- eb.setHighlight(TextHighlightType.HandwritingDeletePreview, 2, 4)
- assertThat(eb.highlight)
- .isEqualTo(Pair(TextHighlightType.HandwritingDeletePreview, TextRange(2, 4)))
-
- eb.clearHighlight()
- assertThat(eb.highlight).isNull()
- }
-
- @Test
- fun setHighlight_setSelection_clearsHighlight() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
- assertThat(eb.highlight)
- .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
-
- eb.setSelection(0, 1)
- assertThat(eb.highlight).isNull()
- }
-
- @Test
- fun setHighlight_replace_clearsHighlight() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
- assertThat(eb.highlight)
- .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
-
- eb.replace(3, 5, "F")
- assertThat(eb.highlight).isNull()
- }
-
- @Test
- fun setHighlight_delete_clearsHighlight() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
-
- eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
- assertThat(eb.highlight)
- .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
-
- eb.delete(0, 1)
- assertThat(eb.highlight).isNull()
- }
-}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/FinishComposingTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/FinishComposingTextCommandTest.kt
index 6c5b836..91f0579 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/FinishComposingTextCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/FinishComposingTextCommandTest.kt
@@ -27,26 +27,27 @@
@Test
fun test_set() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposition(1, 4)
eb.finishComposingText()
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_preserve_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 4))
eb.setComposition(2, 5)
eb.finishComposingText()
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isFalse()
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingRegionCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingRegionCommandTest.kt
index 418f2c3..8ed2671 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingRegionCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingRegionCommandTest.kt
@@ -27,104 +27,111 @@
@Test
fun test_set() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposingRegion(1, 4)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(4)
}
@Test
fun test_preserve_ongoing_composition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposition(1, 3)
eb.setComposingRegion(2, 4)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(4)
}
@Test
fun test_preserve_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 4))
eb.setComposingRegion(2, 4)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(4)
}
@Test
fun test_set_reversed() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposingRegion(4, 1)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(4)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(4)
}
@Test
fun test_set_too_small() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposingRegion(-1000, -1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_set_too_large() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposingRegion(1000, 1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_set_too_small_and_too_large() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposingRegion(-1000, 1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(5)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(5)
}
@Test
fun test_set_too_small_and_too_large_reversed() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposingRegion(1000, -1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(5)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(5)
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingTextCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingTextCommandTest.kt
index 4d4a03c..1ab9fe6 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingTextCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetComposingTextCommandTest.kt
@@ -27,143 +27,154 @@
@Test
fun test_insert_empty() {
- val eb = EditingBuffer("", TextRange.Zero)
+ val eb = TextFieldBuffer("", TextRange.Zero)
eb.setComposingText("X", 1)
assertThat(eb.toString()).isEqualTo("X")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(0)
- assertThat(eb.compositionEnd).isEqualTo(1)
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(1)
}
@Test
fun test_insert_cursor_tail() {
- val eb = EditingBuffer("A", TextRange(1))
+ val eb = TextFieldBuffer("A", TextRange(1))
eb.setComposingText("X", 1)
assertThat(eb.toString()).isEqualTo("AX")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_insert_cursor_head() {
- val eb = EditingBuffer("A", TextRange(1))
+ val eb = TextFieldBuffer("A", TextRange(1))
eb.setComposingText("X", 0)
assertThat(eb.toString()).isEqualTo("AX")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_insert_cursor_far_tail() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.setComposingText("X", 2)
assertThat(eb.toString()).isEqualTo("AXBCDE")
- assertThat(eb.cursor).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_insert_cursor_far_head() {
- val eb = EditingBuffer("ABCDE", TextRange(4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(4))
eb.setComposingText("X", -2)
assertThat(eb.toString()).isEqualTo("ABCDXE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(4)
- assertThat(eb.compositionEnd).isEqualTo(5)
+ assertThat(eb.composition?.start).isEqualTo(4)
+ assertThat(eb.composition?.end).isEqualTo(5)
}
@Test
fun test_insert_empty_text_cursor_head() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.setComposingText("", 0)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_tail() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.setComposingText("", 1)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_far_tail() {
- val eb = EditingBuffer("ABCDE", TextRange(1))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1))
eb.setComposingText("", 2)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_insert_empty_text_cursor_far_head() {
- val eb = EditingBuffer("ABCDE", TextRange(4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(4))
eb.setComposingText("", -2)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_cancel_composition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposition(1, 4) // Mark "BCD" as composition
eb.setComposingText("X", 1)
assertThat(eb.toString()).isEqualTo("AXE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_replace_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 4)) // select "BCD"
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 4)) // select "BCD"
eb.setComposingText("X", 1)
assertThat(eb.toString()).isEqualTo("AXE")
- assertThat(eb.cursor).isEqualTo(2)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(2)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
}
@Test
fun test_composition_and_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 3)) // select "BC"
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 3)) // select "BC"
eb.setComposition(2, 4) // Mark "CD" as composition
eb.setComposingText("X", 1)
@@ -171,35 +182,38 @@
// If composition and selection exists at the same time, replace composition and cancel
// selection and place cursor.
assertThat(eb.toString()).isEqualTo("ABXE")
- assertThat(eb.cursor).isEqualTo(3)
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(2)
- assertThat(eb.compositionEnd).isEqualTo(3)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(3)
}
@Test
fun test_cursor_position_too_small() {
- val eb = EditingBuffer("ABCDE", TextRange(5))
+ val eb = TextFieldBuffer("ABCDE", TextRange(5))
eb.setComposingText("X", -1000)
assertThat(eb.toString()).isEqualTo("ABCDEX")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(5)
- assertThat(eb.compositionEnd).isEqualTo(6)
+ assertThat(eb.composition?.start).isEqualTo(5)
+ assertThat(eb.composition?.end).isEqualTo(6)
}
@Test
fun test_cursor_position_too_large() {
- val eb = EditingBuffer("ABCDE", TextRange(5))
+ val eb = TextFieldBuffer("ABCDE", TextRange(5))
eb.setComposingText("X", 1000)
assertThat(eb.toString()).isEqualTo("ABCDEX")
- assertThat(eb.cursor).isEqualTo(6)
+ assertThat(eb.selection.start).isEqualTo(6)
+ assertThat(eb.selection.end).isEqualTo(6)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(5)
- assertThat(eb.compositionEnd).isEqualTo(6)
+ assertThat(eb.composition?.start).isEqualTo(5)
+ assertThat(eb.composition?.end).isEqualTo(6)
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetSelectionCommandTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetSelectionCommandTest.kt
index ca8cae9..60a15e8 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetSelectionCommandTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/SetSelectionCommandTest.kt
@@ -27,99 +27,101 @@
@Test
fun test_set() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setSelection(1, 4)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(1)
- assertThat(eb.selectionEnd).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_preserve_ongoing_composition() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setComposition(1, 3)
eb.setSelection(2, 4)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(2)
- assertThat(eb.selectionEnd).isEqualTo(4)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(4)
assertThat(eb.hasComposition()).isTrue()
- assertThat(eb.compositionStart).isEqualTo(1)
- assertThat(eb.compositionEnd).isEqualTo(3)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(3)
}
@Test
fun test_cancel_ongoing_selection() {
- val eb = EditingBuffer("ABCDE", TextRange(1, 4))
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 4))
eb.setSelection(2, 5)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(2)
- assertThat(eb.selectionEnd).isEqualTo(5)
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(5)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_set_reversed() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setSelection(4, 1)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(4)
- assertThat(eb.selectionEnd).isEqualTo(1)
+ assertThat(eb.selection.start).isEqualTo(4)
+ assertThat(eb.selection.end).isEqualTo(1)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_set_too_small() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setSelection(-1000, -1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
@Test
fun test_set_too_large() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
eb.setSelection(1000, 1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.cursor).isEqualTo(5)
+ assertThat(eb.selection.start).isEqualTo(5)
+ assertThat(eb.selection.end).isEqualTo(5)
assertThat(eb.hasComposition()).isFalse()
}
@Test
- fun test_set_too_small_too_large() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ fun test_set_too_too_large() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
- eb.setSelection(-1000, 1000)
+ eb.setSelection(0, 1000)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(0)
- assertThat(eb.selectionEnd).isEqualTo(5)
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(5)
assertThat(eb.hasComposition()).isFalse()
}
@Test
- fun test_set_too_small_too_large_reversed() {
- val eb = EditingBuffer("ABCDE", TextRange.Zero)
+ fun test_set_too_large_reversed() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
- eb.setSelection(1000, -1000)
+ eb.setSelection(1000, 0)
assertThat(eb.toString()).isEqualTo("ABCDE")
- assertThat(eb.selectionStart).isEqualTo(5)
- assertThat(eb.selectionEnd).isEqualTo(0)
+ assertThat(eb.selection.start).isEqualTo(5)
+ assertThat(eb.selection.end).isEqualTo(0)
assertThat(eb.hasComposition()).isFalse()
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferDeleteRangeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldBufferDeleteFromImeRangeTest.kt
similarity index 67%
rename from compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferDeleteRangeTest.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldBufferDeleteFromImeRangeTest.kt
index 4529e94..723d68c 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/EditingBufferDeleteRangeTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldBufferDeleteFromImeRangeTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.input.internal
+import androidx.compose.foundation.text.input.adjustTextRange
import androidx.compose.ui.text.TextRange
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -23,13 +24,13 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-class EditingBufferDeleteRangeTest {
+class TextFieldBufferDeleteFromImeRangeTest {
@Test
fun test_does_not_intersect_deleted_is_after_the_target() {
val target = TextRange(0, 1)
val deleted = TextRange(2, 3)
- assertThat(updateRangeAfterDelete(target, deleted))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
.isEqualTo(TextRange(target.start, target.end))
}
@@ -37,48 +38,55 @@
fun test_does_not_intersect_deleted_is_before_the_target() {
val target = TextRange(4, 5)
val deleted = TextRange(0, 2)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(2, 3))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(2, 3))
}
@Test
fun test_deleted_covers_target() {
val target = TextRange(1, 2)
val deleted = TextRange(0, 3)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(0, 0))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(0, 0))
}
@Test
fun test_target_covers_deleted() {
val target = TextRange(0, 3)
val deleted = TextRange(1, 2)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(0, 2))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(0, 2))
}
@Test
fun test_deleted_same_as_target() {
val target = TextRange(1, 2)
val deleted = TextRange(1, 2)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(1, 1))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(1, 1))
}
@Test
fun test_deleted_covers_first_half_of_target() {
val target = TextRange(1, 4)
val deleted = TextRange(0, 2)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(0, 2))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(0, 2))
}
@Test
fun test_deleted_covers_second_half_of_target() {
val target = TextRange(1, 4)
val deleted = TextRange(3, 5)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(1, 3))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(1, 3))
}
@Test
fun test_delete_trailing_cursor() {
val target = TextRange(3, 3)
val deleted = TextRange(1, 2)
- assertThat(updateRangeAfterDelete(target, deleted)).isEqualTo(TextRange(2, 2))
+ assertThat(adjustTextRange(target, deleted.start, deleted.end, 0))
+ .isEqualTo(TextRange(2, 2))
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldBufferUseFromImeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldBufferUseFromImeTest.kt
new file mode 100644
index 0000000..aa5b02b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldBufferUseFromImeTest.kt
@@ -0,0 +1,588 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.input.internal
+
+import androidx.compose.foundation.text.input.PlacedAnnotation
+import androidx.compose.foundation.text.input.TextFieldBuffer
+import androidx.compose.foundation.text.input.TextFieldCharSequence
+import androidx.compose.foundation.text.input.TextHighlightType
+import androidx.compose.foundation.text.input.internal.matchers.assertThat
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.TextDecoration
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class TextFieldBufferUseFromImeTest {
+
+ @Test
+ fun insert() {
+ val eb = TextFieldBuffer("", TextRange.Zero)
+
+ eb.imeReplace(0, 0, "A")
+
+ assertThat(eb).hasChars("A")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ // Keep inserting text to the end of string. Cursor should follow.
+ eb.imeReplace(1, 1, "BC")
+ assertThat(eb).hasChars("ABC")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ // Insert into middle position. Cursor should be end of inserted text.
+ eb.imeReplace(1, 1, "D")
+ assertThat(eb).hasChars("ADBC")
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ }
+
+ @Test
+ fun delete() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.imeReplace(0, 1, "")
+
+ // Delete the left character at the cursor.
+ assertThat(eb).hasChars("BCDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ // Delete the text before the cursor
+ eb.imeReplace(0, 2, "")
+ assertThat(eb).hasChars("DE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ // Delete end of the text.
+ eb.imeReplace(1, 2, "")
+ assertThat(eb).hasChars("D")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ }
+
+ @Test
+ fun setSelection() {
+ val eb = TextFieldBuffer("ABCDE", TextRange(0, 3))
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.selection = TextRange(0, 5) // Change the selection
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(5)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.imeReplace(0, 3, "X") // replace function cancel the selection and place cursor.
+ assertThat(eb).hasChars("XDE")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.setSelection(0, 2) // Set the selection again
+ assertThat(eb).hasChars("XDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ }
+
+ @Test
+ fun imeReplace_cancels_composition() {
+ val tfb =
+ TextFieldBuffer(
+ TextFieldCharSequence(
+ text = "ABCDE",
+ selection = TextRange(1, 4),
+ composition = TextRange(0, 5)
+ )
+ )
+
+ tfb.imeReplace(0, 3, "FGH")
+
+ assertThat(tfb).hasChars("FGHDE")
+ assertThat(tfb.selection.start).isEqualTo(3)
+ assertThat(tfb.selection.end).isEqualTo(3)
+ assertThat(tfb.hasComposition()).isFalse()
+ assertThat(tfb.composition).isNull()
+ }
+
+ @Test
+ fun setSelection_coerces_whenNegativeStart() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setSelection(-1, 1)
+
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(1)
+ }
+
+ @Test
+ fun setSelection_coerces_whenNegativeEnd() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setSelection(1, -1)
+
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(0)
+ }
+
+ @Test
+ fun setSelection_allowReversedSelection() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+ eb.setSelection(4, 2)
+
+ assertThat(eb.selection).isEqualTo(TextRange(4, 2))
+ }
+
+ @Test
+ fun replace_reversedRegion() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+ eb.imeReplace(3, 1, "FGHI")
+
+ assertThat(eb).hasChars("AFGHIDE")
+ assertThat(eb.selection.start).isEqualTo(5)
+ assertThat(eb.selection.end).isEqualTo(5)
+ }
+
+ @Test
+ fun setComposition_and_cancelComposition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(0, 5) // Make all text as composition
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isTrue()
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(5)
+
+ eb.imeReplace(2, 3, "X") // replace function cancel the composition text.
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.setComposition(2, 4) // set composition again
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isTrue()
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(4)
+ }
+
+ @Test
+ fun setComposition_and_commitComposition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(0, 5) // Make all text as composition
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isTrue()
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(5)
+
+ eb.imeReplace(2, 3, "X") // replace function cancel the composition text.
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.setComposition(2, 4) // set composition again
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isTrue()
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(4)
+
+ eb.commitComposition() // commit the composition
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ }
+
+ @Test
+ fun setComposition_and_annotationList() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ val annotations: List<PlacedAnnotation> =
+ listOf(
+ AnnotatedString.Range(SpanStyle(textDecoration = TextDecoration.Underline), 0, 5)
+ )
+ eb.setComposition(0, 5, annotations)
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isTrue()
+ assertThat(eb.composition?.start).isEqualTo(0)
+ assertThat(eb.composition?.end).isEqualTo(5)
+ assertThat(eb.composingAnnotations?.size).isEqualTo(1)
+ assertThat(eb.composingAnnotations?.first()).isEqualTo(annotations.first())
+
+ eb.imeReplace(2, 3, "X") // replace function cancel the composition text.
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ assertThat(eb.composingAnnotations?.isEmpty()).isTrue()
+
+ eb.setComposition(2, 4) // set composition again
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isTrue()
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(4)
+ assertThat(eb.composingAnnotations?.isEmpty()).isTrue()
+
+ eb.setComposition(0, 5, annotations)
+ assertThat(eb.composingAnnotations?.size).isEqualTo(1)
+ assertThat(eb.composingAnnotations?.first()).isEqualTo(annotations.first())
+
+ eb.commitComposition() // commit the composition
+ assertThat(eb).hasChars("ABXDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(3)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ assertThat(eb.composingAnnotations?.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun setCursor_and_get_cursor() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.selection = TextRange(1)
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.selection = TextRange(2)
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+
+ eb.selection = TextRange(5)
+ assertThat(eb).hasChars("ABCDE")
+ assertThat(eb.selection.start).isEqualTo(5)
+ assertThat(eb.selection.end).isEqualTo(5)
+ assertThat(eb.hasComposition()).isFalse()
+ assertThat(eb.composition).isNull()
+ }
+
+ @Test
+ fun delete_preceding_cursor_no_composition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.imeDelete(1, 2)
+ assertThat(eb).hasChars("ACDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isFalse()
+ }
+
+ @Test
+ fun delete_trailing_cursor_no_composition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange(3))
+
+ eb.imeDelete(1, 2)
+ assertThat(eb).hasChars("ACDE")
+ assertThat(eb.selection.start).isEqualTo(2)
+ assertThat(eb.selection.end).isEqualTo(2)
+ assertThat(eb.hasComposition()).isFalse()
+ }
+
+ @Test
+ fun delete_preceding_selection_no_composition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange(0, 1))
+
+ eb.imeDelete(1, 2)
+ assertThat(eb).hasChars("ACDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(1)
+ assertThat(eb.hasComposition()).isFalse()
+ }
+
+ @Test
+ fun delete_trailing_selection_no_composition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange(4, 5))
+
+ eb.imeDelete(1, 2)
+ assertThat(eb).hasChars("ACDE")
+ assertThat(eb.selection.start).isEqualTo(3)
+ assertThat(eb.selection.end).isEqualTo(4)
+ assertThat(eb.hasComposition()).isFalse()
+ }
+
+ @Test
+ fun delete_covered_cursor() {
+ // AB[]CDE
+ val eb = TextFieldBuffer("ABCDE", TextRange(2, 2))
+
+ eb.imeDelete(1, 3)
+ // A[]DE
+ assertThat(eb).hasChars("ADE")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(1)
+ }
+
+ @Test
+ fun delete_covered_selection() {
+ // A[BC]DE
+ val eb = TextFieldBuffer("ABCDE", TextRange(1, 3))
+
+ eb.imeDelete(0, 4)
+ // []E
+ assertThat(eb).hasChars("E")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ }
+
+ @Test
+ fun delete_covered_reversedSelection() {
+ // A[BC]DE
+ val eb = TextFieldBuffer("ABCDE", TextRange(3, 1))
+
+ eb.imeDelete(0, 4)
+ // []E
+ assertThat(eb).hasChars("E")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ }
+
+ @Test
+ fun delete_intersects_first_half_of_selection() {
+ // AB[CD]E
+ val eb = TextFieldBuffer("ABCDE", TextRange(2, 4))
+
+ eb.imeDelete(1, 3)
+ // A[D]E
+ assertThat(eb).hasChars("ADE")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(2)
+ }
+
+ @Test
+ fun delete_intersects_first_half_of_reversedSelection() {
+ // AB[CD]E
+ val eb = TextFieldBuffer("ABCDE", TextRange(4, 2))
+
+ eb.imeDelete(3, 1)
+ // A[D]E
+ assertThat(eb).hasChars("ADE")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(2)
+ }
+
+ @Test
+ fun delete_intersects_second_half_of_selection() {
+ // A[BCD]EFG
+ val eb = TextFieldBuffer("ABCDEFG", TextRange(1, 4))
+
+ eb.imeDelete(3, 5)
+ // A[BC]FG
+ assertThat(eb).hasChars("ABCFG")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(3)
+ }
+
+ @Test
+ fun delete_intersects_second_half_of_reversedSelection() {
+ // A[BCD]EFG
+ val eb = TextFieldBuffer("ABCDEFG", TextRange(4, 1))
+
+ eb.imeDelete(5, 3)
+ // A[BC]FG
+ assertThat(eb).hasChars("ABCFG")
+ assertThat(eb.selection.start).isEqualTo(1)
+ assertThat(eb.selection.end).isEqualTo(3)
+ }
+
+ @Test
+ fun delete_preceding_composition_no_intersection() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(1, 2)
+ eb.imeDelete(2, 3)
+
+ assertThat(eb).hasChars("ABDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
+ }
+
+ @Test
+ fun delete_trailing_composition_no_intersection() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(3, 4)
+ eb.imeDelete(2, 3)
+
+ assertThat(eb).hasChars("ABDE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(3)
+ }
+
+ @Test
+ fun delete_preceding_composition_intersection() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(1, 3)
+ eb.imeDelete(2, 4)
+
+ assertThat(eb).hasChars("ABE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.composition?.start).isEqualTo(1)
+ assertThat(eb.composition?.end).isEqualTo(2)
+ }
+
+ @Test
+ fun delete_trailing_composition_intersection() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(3, 5)
+ eb.imeDelete(2, 4)
+
+ assertThat(eb).hasChars("ABE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(3)
+ }
+
+ @Test
+ fun delete_composition_contains_delrange() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(2, 5)
+ eb.imeDelete(3, 4)
+
+ assertThat(eb).hasChars("ABCE")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.composition?.start).isEqualTo(2)
+ assertThat(eb.composition?.end).isEqualTo(4)
+ }
+
+ @Test
+ fun delete_delrange_contains_composition() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setComposition(3, 4)
+ eb.imeDelete(2, 5)
+
+ assertThat(eb).hasChars("AB")
+ assertThat(eb.selection.start).isEqualTo(0)
+ assertThat(eb.selection.end).isEqualTo(0)
+ assertThat(eb.hasComposition()).isFalse()
+ }
+
+ @Test
+ fun setHighlight_clearHighlight() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
+ assertThat(eb.highlight)
+ .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
+
+ eb.setHighlight(TextHighlightType.HandwritingDeletePreview, 2, 4)
+ assertThat(eb.highlight)
+ .isEqualTo(Pair(TextHighlightType.HandwritingDeletePreview, TextRange(2, 4)))
+
+ eb.clearHighlight()
+ assertThat(eb.highlight).isNull()
+ }
+
+ @Test
+ fun setHighlight_setSelection_clearsHighlight() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
+ assertThat(eb.highlight)
+ .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
+
+ eb.setSelection(0, 1)
+ assertThat(eb.highlight).isNull()
+ }
+
+ @Test
+ fun setHighlight_replace_clearsHighlight() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
+ assertThat(eb.highlight)
+ .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
+
+ eb.imeReplace(3, 5, "F")
+ assertThat(eb.highlight).isNull()
+ }
+
+ @Test
+ fun setHighlight_delete_clearsHighlight() {
+ val eb = TextFieldBuffer("ABCDE", TextRange.Zero)
+
+ eb.setHighlight(TextHighlightType.HandwritingSelectPreview, 1, 3)
+ assertThat(eb.highlight)
+ .isEqualTo(Pair(TextHighlightType.HandwritingSelectPreview, TextRange(1, 3)))
+
+ eb.imeDelete(0, 1)
+ assertThat(eb.highlight).isNull()
+ }
+}
+
+internal fun TextFieldBuffer(
+ initialValue: String = "",
+ initialSelection: TextRange = TextRange.Zero
+) = TextFieldBuffer(TextFieldCharSequence(initialValue, initialSelection))
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
index 9d70dee..a8a0970 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
@@ -148,8 +148,8 @@
state.syncMainBufferToTemporaryBuffer(newTextFieldValue)
assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
- assertThat(newTextFieldValue.selection.start).isEqualTo(state.mainBuffer.selectionStart)
- assertThat(newTextFieldValue.selection.end).isEqualTo(state.mainBuffer.selectionEnd)
+ assertThat(newTextFieldValue.selection.start).isEqualTo(state.mainBuffer.selection.start)
+ assertThat(newTextFieldValue.selection.end).isEqualTo(state.mainBuffer.selection.end)
assertThat(resetCalled).isEqualTo(2)
assertThat(selectionCalled).isEqualTo(0)
}
@@ -169,16 +169,14 @@
val initialBuffer = state.mainBuffer
// composition can not be set from app, IME owns it.
- assertThat(EditingBuffer.NOWHERE).isEqualTo(initialBuffer.compositionStart)
- assertThat(EditingBuffer.NOWHERE).isEqualTo(initialBuffer.compositionEnd)
+ assertThat(initialBuffer.composition).isNull()
val newTextFieldValue =
TextFieldCharSequence(textFieldValue, textFieldValue.selection, composition = null)
state.syncMainBufferToTemporaryBuffer(newTextFieldValue)
assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
- assertThat(EditingBuffer.NOWHERE).isEqualTo(state.mainBuffer.compositionStart)
- assertThat(EditingBuffer.NOWHERE).isEqualTo(state.mainBuffer.compositionEnd)
+ assertThat(state.mainBuffer.composition).isNull()
assertThat(resetCalled).isEqualTo(2)
assertThat(selectionCalled).isEqualTo(0)
}
@@ -197,8 +195,8 @@
val initialBuffer = state.mainBuffer
- assertThat(initialSelection.start).isEqualTo(initialBuffer.selectionStart)
- assertThat(initialSelection.end).isEqualTo(initialBuffer.selectionEnd)
+ assertThat(initialSelection.start).isEqualTo(initialBuffer.selection.start)
+ assertThat(initialSelection.end).isEqualTo(initialBuffer.selection.end)
val updatedSelection = TextRange(3, 0)
val newTextFieldValue = TextFieldCharSequence(textFieldValue, selection = updatedSelection)
@@ -206,8 +204,8 @@
state.syncMainBufferToTemporaryBuffer(newTextFieldValue)
assertThat(state.mainBuffer).isSameInstanceAs(initialBuffer)
- assertThat(updatedSelection.start).isEqualTo(initialBuffer.selectionStart)
- assertThat(updatedSelection.end).isEqualTo(initialBuffer.selectionEnd)
+ assertThat(updatedSelection.start).isEqualTo(initialBuffer.selection.start)
+ assertThat(updatedSelection.end).isEqualTo(initialBuffer.selection.end)
assertThat(resetCalled).isEqualTo(1)
assertThat(selectionCalled).isEqualTo(0)
}
@@ -241,7 +239,7 @@
}
@Test
- fun compositionIsNotCleared_when_textIsSame() {
+ fun compositionIsCleared_when_textIsSame() {
val state = TextFieldState()
val composition = TextRange(0, 2)
@@ -256,7 +254,7 @@
state.syncMainBufferToTemporaryBuffer(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.composition).isEqualTo(composition)
+ assertThat(state.composition).isNull()
}
@Test
@@ -298,7 +296,7 @@
}
@Test
- fun compositionIsNotCleared_when_onlySelectionChanged() {
+ fun compositionIsCleared_when_onlySelectionChanged() {
val state = TextFieldState()
val composition = TextRange(0, 2)
@@ -322,7 +320,7 @@
state.syncMainBufferToTemporaryBuffer(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.composition).isEqualTo(composition)
+ assertThat(state.composition).isNull()
assertThat(state.selection).isEqualTo(newSelection)
}
@@ -549,7 +547,7 @@
private fun TextFieldState(value: TextFieldCharSequence) =
TextFieldState(value.toString(), value.selection)
- private fun TextFieldState.editAsUser(block: EditingBuffer.() -> Unit) {
+ private fun TextFieldState.editAsUser(block: TextFieldBuffer.() -> Unit) {
editAsUser(inputTransformation = null, restartImeIfContentChanges = false, block = block)
}
@@ -565,8 +563,7 @@
textFieldCharSequence: TextFieldCharSequence
) {
syncMainBufferToTemporaryBuffer(
- textFieldBuffer = TextFieldBuffer(textFieldCharSequence),
- newComposition = textFieldCharSequence.composition,
+ temporaryBuffer = TextFieldBuffer(textFieldCharSequence),
textChanged = !textFieldCharSequence.contentEquals(mainBuffer.toString()),
selectionChanged = textFieldCharSequence.selection != mainBuffer.selection,
)
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/matchers/EditBufferSubject.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/matchers/TextFieldBufferSubject.kt
similarity index 68%
rename from compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/matchers/EditBufferSubject.kt
rename to compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/matchers/TextFieldBufferSubject.kt
index ac6b9ac..265fa49 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/matchers/EditBufferSubject.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/matchers/TextFieldBufferSubject.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-@file:OptIn(InternalFoundationTextApi::class)
-
package androidx.compose.foundation.text.input.internal.matchers
import androidx.compose.foundation.text.InternalFoundationTextApi
-import androidx.compose.foundation.text.input.internal.EditingBuffer
+import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.internal.PartialGapBuffer
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
@@ -27,13 +25,13 @@
import com.google.common.truth.Truth.assertAbout
import com.google.common.truth.Truth.assertThat
-@OptIn(InternalFoundationTextApi::class)
-internal fun assertThat(buffer: PartialGapBuffer): EditBufferSubject {
- return assertAbout(EditBufferSubject.SUBJECT_FACTORY).that(GapBufferWrapper(buffer))!!
+internal fun assertThat(buffer: PartialGapBuffer): TextFieldBufferSubject {
+ return assertAbout(TextFieldBufferSubject.SUBJECT_FACTORY).that(GapBufferWrapper(buffer))!!
}
-internal fun assertThat(buffer: EditingBuffer): EditBufferSubject {
- return assertAbout(EditBufferSubject.SUBJECT_FACTORY).that(EditingBufferWrapper(buffer))!!
+internal fun assertThat(buffer: TextFieldBuffer): TextFieldBufferSubject {
+ return assertAbout(TextFieldBufferSubject.SUBJECT_FACTORY)
+ .that(TextFieldBufferWrapper(buffer))!!
}
internal abstract class GetOperatorWrapper(val buffer: Any) {
@@ -42,8 +40,8 @@
override fun toString(): String = buffer.toString()
}
-private class EditingBufferWrapper(buffer: EditingBuffer) : GetOperatorWrapper(buffer) {
- override fun get(index: Int): Char = (buffer as EditingBuffer)[index]
+private class TextFieldBufferWrapper(buffer: TextFieldBuffer) : GetOperatorWrapper(buffer) {
+ override fun get(index: Int): Char = (buffer as TextFieldBuffer).asCharSequence()[index]
}
@OptIn(InternalFoundationTextApi::class)
@@ -51,15 +49,15 @@
override fun get(index: Int): Char = (buffer as PartialGapBuffer)[index]
}
-/** Truth extension for Editing Buffers. */
-internal class EditBufferSubject
+/** Truth extension for TextField Buffers. */
+internal class TextFieldBufferSubject
private constructor(failureMetadata: FailureMetadata?, private val subject: GetOperatorWrapper) :
Subject(failureMetadata, subject) {
companion object {
- internal val SUBJECT_FACTORY: Factory<EditBufferSubject, GetOperatorWrapper> =
+ internal val SUBJECT_FACTORY: Factory<TextFieldBufferSubject, GetOperatorWrapper> =
Factory { failureMetadata, subject ->
- EditBufferSubject(failureMetadata, subject)
+ TextFieldBufferSubject(failureMetadata, subject)
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
index 2f27a8d..0becec6 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
@@ -20,7 +20,9 @@
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.allCaps
+import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.internal.commitText
+import androidx.compose.foundation.text.input.internal.setSelection
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.intl.Locale
import com.google.common.truth.Truth.assertThat
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index fa72269..0881e54 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -95,9 +95,10 @@
* @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
* [Modifier.draggable].
- * @param startDragImmediately when set to false, [draggable] will start dragging only when the
- * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
- * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ * @param overscrollEffect optional effect to dispatch any excess delta or velocity to. The excess
+ * delta or velocity are a result of dragging/flinging and reaching the bounds. If you provide an
+ * [overscrollEffect], make sure to apply [androidx.compose.foundation.overscroll] to render the
+ * effect as well.
* @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
* default (if passing in null), this will snap to the closest anchor considering the velocity
* thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
@@ -108,7 +109,7 @@
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
- startDragImmediately: Boolean = state.isAnimationRunning,
+ overscrollEffect: OverscrollEffect? = null,
flingBehavior: FlingBehavior? = null
): Modifier =
this then
@@ -118,52 +119,7 @@
enabled = enabled,
reverseDirection = reverseDirection,
interactionSource = interactionSource,
- overscrollEffect = null,
- startDragImmediately = startDragImmediately,
- flingBehavior = flingBehavior
- )
-
-/**
- * Enable drag gestures between a set of predefined values.
- *
- * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
- * delta. If the [orientation] is set to [Orientation.Horizontal] and [LocalLayoutDirection]'s value
- * is [LayoutDirection.Rtl], the drag deltas will be reversed. You should use this offset to move
- * your content accordingly (see [Modifier.offset]). When the drag ends, the offset will be animated
- * to one of the anchors and when that anchor is reached, the value of the [AnchoredDraggableState]
- * will also be updated to the value corresponding to the new anchor.
- *
- * Dragging is constrained between the minimum and maximum anchors.
- *
- * @param state The associated [AnchoredDraggableState].
- * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
- * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
- * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
- * [Modifier.draggable].
- * @param startDragImmediately when set to false, [draggable] will start dragging only when the
- * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
- * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
- * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
- * default (if passing in null), this will snap to the closest anchor considering the velocity
- * thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
- */
-fun <T> Modifier.anchoredDraggable(
- state: AnchoredDraggableState<T>,
- orientation: Orientation,
- enabled: Boolean = true,
- interactionSource: MutableInteractionSource? = null,
- startDragImmediately: Boolean = state.isAnimationRunning,
- flingBehavior: FlingBehavior? = null
-): Modifier =
- this then
- AnchoredDraggableElement(
- state = state,
- orientation = orientation,
- enabled = enabled,
- reverseDirection = null,
- interactionSource = interactionSource,
- overscrollEffect = null,
- startDragImmediately = startDragImmediately,
+ overscrollEffect = overscrollEffect,
flingBehavior = flingBehavior
)
@@ -198,6 +154,7 @@
* default (if passing in null), this will snap to the closest anchor considering the velocity
* thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
*/
+@Deprecated(StartDragImmediatelyDeprecated)
fun <T> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
reverseDirection: Boolean,
@@ -241,6 +198,50 @@
* delta or velocity are a result of dragging/flinging and reaching the bounds. If you provide an
* [overscrollEffect], make sure to apply [androidx.compose.foundation.overscroll] to render the
* effect as well.
+ * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
+ * default (if passing in null), this will snap to the closest anchor considering the velocity
+ * thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ */
+fun <T> Modifier.anchoredDraggable(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource? = null,
+ overscrollEffect: OverscrollEffect? = null,
+ flingBehavior: FlingBehavior? = null
+): Modifier =
+ this then
+ AnchoredDraggableElement(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = null,
+ interactionSource = interactionSource,
+ overscrollEffect = overscrollEffect,
+ flingBehavior = flingBehavior
+ )
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. If the [orientation] is set to [Orientation.Horizontal] and [LocalLayoutDirection]'s value
+ * is [LayoutDirection.Rtl], the drag deltas will be reversed. You should use this offset to move
+ * your content accordingly (see [Modifier.offset]). When the drag ends, the offset will be animated
+ * to one of the anchors and when that anchor is reached, the value of the [AnchoredDraggableState]
+ * will also be updated to the value corresponding to the new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
+ * [Modifier.draggable].
+ * @param overscrollEffect optional effect to dispatch any excess delta or velocity to. The excess
+ * delta or velocity are a result of dragging/flinging and reaching the bounds. If you provide an
+ * [overscrollEffect], make sure to apply [androidx.compose.foundation.overscroll] to render the
+ * effect as well.
* @param startDragImmediately when set to false, [draggable] will start dragging only when the
* gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
* widget when pressing on it. See [draggable] to learn more about startDragImmediately.
@@ -248,6 +249,7 @@
* default (if passing in null), this will snap to the closest anchor considering the velocity
* thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
*/
+@Deprecated(StartDragImmediatelyDeprecated)
fun <T> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
orientation: Orientation,
@@ -275,7 +277,7 @@
private val enabled: Boolean,
private val reverseDirection: Boolean?,
private val interactionSource: MutableInteractionSource?,
- private val startDragImmediately: Boolean,
+ private val startDragImmediately: Boolean? = null,
private val overscrollEffect: OverscrollEffect?,
private val flingBehavior: FlingBehavior? = null,
) : ModifierNodeElement<AnchoredDraggableNode<T>>() {
@@ -353,7 +355,7 @@
private var reverseDirection: Boolean?,
interactionSource: MutableInteractionSource?,
private var overscrollEffect: OverscrollEffect?,
- private var startDragImmediately: Boolean,
+ private var startDragImmediately: Boolean?,
private var flingBehavior: FlingBehavior?
) :
DragGestureNode(
@@ -470,7 +472,7 @@
leftoverVelocity
}
- override fun startDragImmediately(): Boolean = startDragImmediately
+ override fun startDragImmediately(): Boolean = startDragImmediately ?: state.isAnimationRunning
fun update(
state: AnchoredDraggableState<T>,
@@ -479,7 +481,7 @@
reverseDirection: Boolean?,
interactionSource: MutableInteractionSource?,
overscrollEffect: OverscrollEffect?,
- startDragImmediately: Boolean,
+ startDragImmediately: Boolean?,
flingBehavior: FlingBehavior?,
) {
this.flingBehavior = flingBehavior
@@ -1617,6 +1619,10 @@
"settle does not accept a velocity anymore. " +
"Please use FlingBehavior#performFling instead. See AnchoredDraggableSamples.kt for example " +
"usages."
+private const val StartDragImmediatelyDeprecated =
+ "startDragImmediately has been removed " +
+ "without replacement. Modifier.anchoredDraggable sets startDragImmediately to true by " +
+ "default when animations are running."
/**
* Construct a [FlingBehavior] for use with [Modifier.anchoredDraggable].
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
index 0c588d5..d7b2e5d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
@@ -96,7 +96,7 @@
* Adds a sticky header item, which will remain pinned even when scrolling after it. The header
* will remain pinned until the next header will take its place.
*
- * @sample androidx.compose.foundation.samples.StickyHeaderSample
+ * @sample androidx.compose.foundation.samples.StickyHeaderListSample
* @param key a stable and unique key representing the item. Using the same key for multiple
* items in the list is not allowed. Type of the key should be saveable via Bundle on Android.
* If null is passed the position in the list will represent the key. When you specify the key
@@ -124,7 +124,7 @@
* Adds a sticky header item, which will remain pinned even when scrolling after it. The header
* will remain pinned until the next header will take its place.
*
- * @sample androidx.compose.foundation.samples.StickyHeaderSample
+ * @sample androidx.compose.foundation.samples.StickyHeaderListSample
* @sample androidx.compose.foundation.samples.StickyHeaderHeaderIndexSample
* @param key a stable and unique key representing the item. Using the same key for multiple
* items in the list is not allowed. Type of the key should be saveable via Bundle on Android.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 14de356..f03d8b3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -408,6 +408,13 @@
measuredItemProvider.getAndMeasure(it)
}
+ val firstVisibleIndex =
+ if (noExtraItems) positionedItems.firstOrNull()?.index
+ else visibleItems.firstOrNull()?.index
+ val lastVisibleIndex =
+ if (noExtraItems) positionedItems.lastOrNull()?.index
+ else visibleItems.lastOrNull()?.index
+
return LazyListMeasureResult(
firstVisibleItem = firstItem,
firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
@@ -426,8 +433,8 @@
scrollBackAmount = scrollBackAmount,
visibleItemsInfo =
updatedVisibleItems(
- noExtraItems = noExtraItems,
- currentVisibleItems = visibleItems,
+ firstVisibleIndex = firstVisibleIndex ?: 0,
+ lastVisibleIndex = lastVisibleIndex ?: 0,
positionedItems = positionedItems,
stickingItems = stickingItems
),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 784f123..ff524f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -28,6 +28,7 @@
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.lazy.layout.LazyLayout
import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.StickyItemsPlacement
import androidx.compose.foundation.lazy.layout.calculateLazyLayoutPinnedIndices
import androidx.compose.foundation.lazy.layout.lazyLayoutBeyondBoundsModifier
import androidx.compose.foundation.lazy.layout.lazyLayoutSemantics
@@ -42,6 +43,7 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalGraphicsContext
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalScrollCaptureInProgress
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.constrainHeight
@@ -83,6 +85,8 @@
val coroutineScope = rememberCoroutineScope()
val graphicsContext = LocalGraphicsContext.current
+ val stickyHeadersEnabled = !LocalScrollCaptureInProgress.current
+
val measurePolicy =
rememberLazyGridMeasurePolicy(
itemProviderLambda,
@@ -94,7 +98,8 @@
horizontalArrangement,
verticalArrangement,
coroutineScope,
- graphicsContext
+ graphicsContext,
+ if (stickyHeadersEnabled) StickyItemsPlacement.StickToTopPlacement else null
)
val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
@@ -166,7 +171,9 @@
/** Coroutine scope for item animations */
coroutineScope: CoroutineScope,
/** Used for creating graphics layers */
- graphicsContext: GraphicsContext
+ graphicsContext: GraphicsContext,
+ /** Configures the placement of sticky items */
+ stickyItemsScrollBehavior: StickyItemsPlacement?
) =
remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
state,
@@ -384,6 +391,7 @@
placementScopeInvalidator = state.placementScopeInvalidator,
prefetchInfoRetriever = prefetchInfoRetriever,
graphicsContext = graphicsContext,
+ stickyItemsScrollBehavior = stickyItemsScrollBehavior,
layout = { width, height, placement ->
layout(
containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt
index b814ee9..509a300 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt
@@ -422,6 +422,31 @@
contentType: (index: Int) -> Any? = { null },
itemContent: @Composable LazyGridItemScope.(index: Int) -> Unit
)
+
+ /**
+ * Adds a sticky header item, which will remain pinned even when scrolling after it. The header
+ * will remain pinned until the next header will take its place. Sticky Headers are full span
+ * items, that is, they will occupy [LazyGridItemSpanScope.maxLineSpan].
+ *
+ * @sample androidx.compose.foundation.samples.StickyHeaderGridSample
+ * @param key a stable and unique key representing the item. Using the same key for multiple
+ * items in the list is not allowed. Type of the key should be saveable via Bundle on Android.
+ * If null is passed the position in the list will represent the key. When you specify the key
+ * the scroll position will be maintained based on the key, which means if you add/remove
+ * items before the current visible item the item with the given key will be kept as the first
+ * visible one. This can be overridden by calling 'requestScrollToItem' on the
+ * 'LazyGridState'.
+ * @param contentType the type of the content of this item. The item compositions of the same
+ * type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param content the content of the header. The header index is provided, this is the item
+ * position within the total set of items in this lazy list (the global index).
+ */
+ fun stickyHeader(
+ key: Any? = null,
+ contentType: Any? = null,
+ content: @Composable LazyGridItemScope.(Int) -> Unit
+ )
}
/**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridIntervalContent.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridIntervalContent.kt
index 39c3218..f4ed2f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridIntervalContent.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridIntervalContent.kt
@@ -16,6 +16,10 @@
package androidx.compose.foundation.lazy.grid
+import androidx.collection.IntList
+import androidx.collection.MutableIntList
+import androidx.collection.emptyIntList
+import androidx.collection.mutableIntListOf
import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -28,6 +32,11 @@
internal var hasCustomSpans = false
+ private var _headerIndexes: MutableIntList? = null
+
+ val headerIndexes: IntList
+ get() = _headerIndexes ?: emptyIntList()
+
init {
apply(content)
}
@@ -69,6 +78,17 @@
if (span != null) hasCustomSpans = true
}
+ override fun stickyHeader(
+ key: Any?,
+ contentType: Any?,
+ content: @Composable LazyGridItemScope.(Int) -> Unit
+ ) {
+ val headersIndexes = _headerIndexes ?: mutableIntListOf().also { _headerIndexes = it }
+ val headerIndex = intervals.size
+ headersIndexes.add(headerIndex)
+ item(key, { GridItemSpan(maxLineSpan) }, contentType) { content.invoke(this, headerIndex) }
+ }
+
private companion object {
val DefaultSpan: LazyGridItemSpanScope.(Int) -> GridItemSpan = { GridItemSpan(1) }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index 07116974..497fc52 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy.grid
+import androidx.collection.IntList
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
@@ -27,10 +28,12 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
+@Suppress("PrimitiveInCollection")
@OptIn(ExperimentalFoundationApi::class)
internal interface LazyGridItemProvider : LazyLayoutItemProvider {
val keyIndexMap: LazyLayoutKeyIndexMap
val spanLayoutProvider: LazyGridSpanLayoutProvider
+ val headerIndexes: IntList
}
@Composable
@@ -72,6 +75,9 @@
override fun getContentType(index: Int): Any? = intervalContent.getContentType(index)
+ override val headerIndexes: IntList
+ get() = intervalContent.headerIndexes
+
@Composable
override fun Item(index: Int, key: Any) {
LazyLayoutPinnableItem(key, index, state.pinnedItems) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 549b2a7..22d00c9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -23,6 +23,9 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
+import androidx.compose.foundation.lazy.layout.StickyItemsPlacement
+import androidx.compose.foundation.lazy.layout.applyStickyItems
+import androidx.compose.foundation.lazy.layout.updatedVisibleItems
import androidx.compose.ui.graphics.GraphicsContext
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
@@ -32,7 +35,6 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.fastRoundToInt
@@ -70,6 +72,7 @@
placementScopeInvalidator: ObservableScopeInvalidator,
graphicsContext: GraphicsContext,
prefetchInfoRetriever: (line: Int) -> List<Pair<Int, Constraints>>,
+ stickyItemsScrollBehavior: StickyItemsPlacement?,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyGridMeasureResult {
requirePrecondition(beforeContentPadding >= 0) { "negative beforeContentPadding" }
@@ -360,6 +363,26 @@
}
}
+ // apply sticky items logic.
+ val stickingItems =
+ stickyItemsScrollBehavior.applyStickyItems(
+ positionedItems,
+ measuredItemProvider.headerIndices,
+ beforeContentPadding,
+ afterContentPadding,
+ layoutWidth,
+ layoutHeight
+ ) {
+ val span = measuredLineProvider.spanOf(it)
+ val childConstraints = measuredLineProvider.childConstraints(0, span)
+ measuredItemProvider.getAndMeasure(
+ index = it,
+ constraints = childConstraints,
+ lane = 0,
+ span = span
+ )
+ }
+
return LazyGridMeasureResult(
firstVisibleLine = firstLine,
firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
@@ -368,17 +391,14 @@
measureResult =
layout(layoutWidth, layoutHeight) {
positionedItems.fastForEach { it.place(this) }
+ stickingItems.fastForEach { it.place(this) }
// we attach it during the placement so LazyGridState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
visibleItemsInfo =
- if (extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) {
- positionedItems
- } else {
- positionedItems.fastFilter { it.index in firstItemIndex..lastItemIndex }
- },
+ updatedVisibleItems(firstItemIndex, lastItemIndex, positionedItems, stickingItems),
totalItemsCount = itemsCount,
reverseLayout = reverseLayout,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
index 5c33a25..d16d0c0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy.grid
+import androidx.collection.IntList
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.internal.requirePrecondition
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
@@ -86,6 +87,9 @@
val keyIndexMap: LazyLayoutKeyIndexMap
get() = itemProvider.keyIndexMap
+ val headerIndices: IntList
+ get() = itemProvider.headerIndexes
+
abstract fun createItem(
index: Int,
key: Any,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index b9dcbb46..23a9c30 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -26,6 +26,7 @@
/** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
private val buckets = ArrayList<Bucket>().apply { add(Bucket(0)) }
+
/**
* The interval at each we will store the starting element of lines. These will be then used to
* calculate the layout of arbitrary lines, by starting from the closest known "bucket start".
@@ -37,20 +38,25 @@
/** Caches the last calculated line index, useful when scrolling in main axis direction. */
private var lastLineIndex = 0
+
/** Caches the starting item index on [lastLineIndex]. */
private var lastLineStartItemIndex = 0
+
/** Caches the span of [lastLineStartItemIndex], if this was already calculated. */
private var lastLineStartKnownSpan = 0
+
/**
* Caches a calculated bucket, this is useful when scrolling in reverse main axis direction. We
* cannot only keep the last element, as we would not know previous max span.
*/
private var cachedBucketIndex = -1
+
/**
* Caches layout of [cachedBucketIndex], this is useful when scrolling in reverse main axis
* direction. We cannot only keep the last element, as we would not know previous max span.
*/
private val cachedBucket = mutableListOf<Int>()
+
/** List of 1x1 spans if we do not have custom spans. */
private var previousDefaultSpans = emptyList<GridItemSpan>()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt
index 213d7a2..36e9bef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasuredItem.kt
@@ -43,16 +43,12 @@
}
internal fun <T : LazyLayoutMeasuredItem> updatedVisibleItems(
- noExtraItems: Boolean,
- currentVisibleItems: List<T>,
+ firstVisibleIndex: Int,
+ lastVisibleIndex: Int,
positionedItems: List<T>,
- stickingItems: List<T>
+ stickingItems: List<T>,
): List<T> {
- if (positionedItems.isEmpty() || currentVisibleItems.isEmpty()) return emptyList()
- val firstVisibleIndex =
- if (noExtraItems) positionedItems.first().index else currentVisibleItems.first().index
- val lastVisibleIndex =
- if (noExtraItems) positionedItems.last().index else currentVisibleItems.last().index
+ if (positionedItems.isEmpty()) return emptyList()
val finalVisibleItems = stickingItems.toMutableList()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
index d070bc0..89174a1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
@@ -168,6 +168,7 @@
) {
return null
}
+ val mainAxisMax = viewportEndOffset - afterContentPadding
visibleItemsInfo.fastForEach {
// non scrollable items require special handling.
if (
@@ -192,7 +193,7 @@
if (!canApply) return null
}
// item is partially visible at the bottom.
- if (it.mainAxisOffset + it.mainAxisSizeWithSpacings >= viewportEndOffset) {
+ if (it.mainAxisOffset + it.mainAxisSizeWithSpacings >= mainAxisMax) {
val canApply =
if (delta < 0) { // scrolling forward
it.mainAxisOffset + it.mainAxisSizeWithSpacings - viewportEndOffset > -delta
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt
index 85cbe37..112defc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt
@@ -22,7 +22,11 @@
import androidx.compose.foundation.text.input.internal.ChangeTracker
import androidx.compose.foundation.text.input.internal.OffsetMappingCalculator
import androidx.compose.foundation.text.input.internal.PartialGapBuffer
+import androidx.compose.runtime.collection.MutableVector
+import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.util.fastForEach
import kotlin.jvm.JvmName
/**
@@ -59,7 +63,7 @@
initialChanges?.let { ChangeTracker(initialChanges) }
/** Lazily-allocated [ChangeTracker], initialized on the first access. */
- private val changeTracker: ChangeTracker
+ internal val changeTracker: ChangeTracker
get() = backingChangeTracker ?: ChangeTracker().also { backingChangeTracker = it }
/** The number of characters in the text field. */
@@ -92,6 +96,8 @@
val changes: ChangeList
get() = changeTracker
+ // region selection
+
/**
* True if the selection range has non-zero length. If this is false, then the selection
* represents the cursor.
@@ -111,7 +117,7 @@
/**
* The selected range of characters.
*
- * Places the selection around the given [range] in characters.
+ * Places the selection around the given range in characters.
*
* If the start or end of TextRange fall inside surrogate pairs or other invalid runs, the
* values will be adjusted to the nearest earlier and later characters, respectively.
@@ -126,8 +132,141 @@
set(value) {
requireValidRange(value)
selectionInChars = value
+ highlight = null
}
+ // endregion
+
+ // region composition
+
+ /**
+ * Returns the composition information as TextRange. Returns null if no composition is set.
+ *
+ * Evaluates to null if it is set to a collapsed TextRange. Clears [composingAnnotations] when
+ * set to null, including collapsed TextRange.
+ */
+ internal var composition: TextRange? = initialValue.composition
+ private set(value) {
+ // collapsed composition region is equivalent to no composition
+ if (value == null || value.collapsed) {
+ field = null
+ // Do not deallocate an existing list. We will probably use it again.
+ composingAnnotations?.clear()
+ } else {
+ field = value
+ }
+ }
+
+ /**
+ * List of annotations that are attached to the composing region. These are usually styling cues
+ * like underline or different background colors.
+ */
+ internal var composingAnnotations:
+ MutableVector<AnnotatedString.Range<AnnotatedString.Annotation>>? =
+ if (!initialValue.composingAnnotations.isNullOrEmpty()) {
+ MutableVector(initialValue.composingAnnotations.size) {
+ initialValue.composingAnnotations[it]
+ }
+ } else {
+ null
+ }
+ private set
+
+ /** Helper function that returns true if the buffer has composing region */
+ internal fun hasComposition(): Boolean = composition != null
+
+ /** Clears current composition. */
+ internal fun commitComposition() {
+ composition = null
+ }
+
+ /**
+ * Mark the specified area of the text as composition text.
+ *
+ * The empty range or reversed range is not allowed. Use [commitComposition] in case if you want
+ * to clear composition.
+ *
+ * @param start the inclusive start offset of the composition
+ * @param end the exclusive end offset of the composition
+ * @param annotations Annotations that are attached to the composing region of text. This
+ * function does not check whether the given annotations are inside the composing region. It
+ * simply adds them to the current buffer while adjusting their range according to where the
+ * new composition region is set.
+ * @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
+ * @throws IllegalArgumentException if start is larger than or equal to end. (reversed or
+ * collapsed range)
+ */
+ internal fun setComposition(start: Int, end: Int, annotations: List<PlacedAnnotation>? = null) {
+ if (start < 0 || start > buffer.length) {
+ throw IndexOutOfBoundsException(
+ "start ($start) offset is outside of text region ${buffer.length}"
+ )
+ }
+ if (end < 0 || end > buffer.length) {
+ throw IndexOutOfBoundsException(
+ "end ($end) offset is outside of text region ${buffer.length}"
+ )
+ }
+ if (start >= end) {
+ throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
+ }
+
+ composition = TextRange(start, end)
+
+ this.composingAnnotations?.clear()
+ if (!annotations.isNullOrEmpty()) {
+ if (this.composingAnnotations == null) {
+ this.composingAnnotations = mutableVectorOf()
+ }
+ annotations.fastForEach {
+ // place the annotations at the correct indices in the buffer.
+ this.composingAnnotations?.add(
+ it.copy(start = it.start + start, end = it.end + start)
+ )
+ }
+ }
+ }
+
+ // endregion
+
+ // region highlight
+
+ /**
+ * A highlighted range of text. This may be used to display handwriting gesture previews from
+ * the IME.
+ */
+ internal var highlight: Pair<TextHighlightType, TextRange>? = null
+ private set
+
+ /**
+ * Mark a range of text to be highlighted. This may be used to display handwriting gesture
+ * previews from the IME.
+ *
+ * An empty or reversed range is not allowed.
+ *
+ * @param type the highlight type
+ * @param start the inclusive start offset of the highlight
+ * @param end the exclusive end offset of the highlight
+ */
+ internal fun setHighlight(type: TextHighlightType, start: Int, end: Int) {
+ if (start >= end) {
+ throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
+ }
+ val clampedStart = start.coerceIn(0, length)
+ val clampedEnd = end.coerceIn(0, length)
+
+ highlight = Pair(type, TextRange(clampedStart, clampedEnd))
+ }
+
+ /** Clear the highlighted text range. */
+ internal fun clearHighlight() {
+ highlight = null
+ }
+
+ // endregion
+
+ // region editing
+
/**
* Replaces the text between [start] (inclusive) and [end] (exclusive) in this value with
* [text], and records the change in [changes].
@@ -169,6 +308,9 @@
}
onTextWillChange(start, end, textEnd - textStart)
buffer.replace(start, end, text, textStart, textEnd)
+
+ commitComposition()
+ highlight = null
}
/**
@@ -217,48 +359,16 @@
private fun onTextWillChange(replaceStart: Int, replaceEnd: Int, newLength: Int) {
changeTracker.trackChange(replaceStart, replaceEnd, newLength)
offsetMappingCalculator?.recordEditOperation(replaceStart, replaceEnd, newLength)
-
- // Adjust selection.
- val start = minOf(replaceStart, replaceEnd)
- val end = maxOf(replaceStart, replaceEnd)
- var selStart = selection.min
- var selEnd = selection.max
-
- if (selEnd < start) {
- // The entire selection is before the insertion point – we don't have to adjust the
- // mark at all, so skip the math.
- return
- }
-
- if (selStart <= start && end <= selEnd) {
- // The insertion is entirely inside the selection, move the end only.
- val diff = newLength - (end - start)
- // Preserve "cursorness".
- if (selStart == selEnd) {
- selStart += diff
- }
- selEnd += diff
- } else if (selStart > start && selEnd < end) {
- // Selection is entirely inside replacement, move it to the end.
- selStart = start + newLength
- selEnd = start + newLength
- } else if (selStart >= end) {
- // The entire selection is after the insertion, so shift everything forward.
- val diff = newLength - (end - start)
- selStart += diff
- selEnd += diff
- } else if (start < selStart) {
- // Insertion is around start of selection, truncate start of selection.
- selStart = start + newLength
- selEnd += newLength - (end - start)
- } else {
- // Insertion is around end of selection, truncate end of selection.
- selEnd = start
- }
- // should not validate
- selectionInChars = TextRange(selStart, selEnd)
+ // On Android, IME calls are usually followed with an explicit change to selection.
+ // Therefore it might seem unnecessary to adjust the selection here. However, this sort of
+ // behavior is not expected for edits that are coming from the developer programmatically
+ // or desktop APIs. So, we make sure that the selection is placed at a reasonable place
+ // after any kind of edit.
+ selectionInChars = adjustTextRange(selection, replaceStart, replaceEnd, newLength)
}
+ // endregion
+
/** Returns the [Char] at [index] in this buffer. */
fun charAt(index: Int): Char = buffer[index]
@@ -332,11 +442,11 @@
* this buffer's selection. Passing a different value in here _only_ affects the return value,
* it does not change the current selection in the buffer.
* @param composition The composition range for the returned [TextFieldCharSequence]. Default
- * value is no composition (null).
+ * value is this buffer's current composition.
*/
internal fun toTextFieldCharSequence(
selection: TextRange = this.selection,
- composition: TextRange? = null
+ composition: TextRange? = this.composition
): TextFieldCharSequence =
TextFieldCharSequence(buffer.toString(), selection = selection, composition = composition)
@@ -380,6 +490,76 @@
}
/**
+ * Given [originalRange], calculates its new placement in the buffer after a region starting from
+ * [replaceStart] (inclusive) ending at [replaceEnd] (exclusive) is deleted and [insertedTextLength]
+ * number of characters are inserted at [replaceStart]. The rules of the adjustment are as follows;
+ * - '||'; denotes the [originalRange]
+ * - '\/'; denotes the [replaceStart], [replaceEnd]
+ *
+ * If the [originalRange]
+ * - is before the replaced region, it remains in the same place.
+ * - abcd|efg|hijk\lmno/pqrs => abcd|efg|hijkxyzpqrs
+ * - TextRange(4, 7) => TextRange(4, 7)
+ * - is after the replaced region, it is moved by the difference in length after replacement,
+ * essentially corresponding to the same part of the text.
+ * - abcd\efg/hijk|lmno|pqrs => abcdxyzxyzxyzhijk|lmno|pqrs
+ * - TextRange(11, 15) => TextRange(17, 21)
+ * - fully wraps the replaced region, only the end is adjusted.
+ * - ab|cd\efg/hijklmno|pqrs => ab|cdxyzxyzxyzhijklmno|pqrs
+ * - TextRange(2, 15) => TextRange(2, 21)
+ * - is inside the replaced region, range is collapsed and moved to the end of the replaced region.
+ * - ab\cd|efg|hijklmno/pqrs => abxyzxyz|pqrs
+ * - TextRange(4, 7) => TextRange(8, 8)
+ * - collides with the replaced region at the start or at the end, it is adjusted so that the
+ * colliding range is not included anymore.
+ * - abcd|efg\hijk|lm/nopqrs => abcd|efg|xyzxyznopqrs
+ * - TextRange(4, 11) => TextRange(4, 7)
+ */
+internal fun adjustTextRange(
+ originalRange: TextRange,
+ replaceStart: Int,
+ replaceEnd: Int,
+ insertedTextLength: Int
+): TextRange {
+ var selStart = originalRange.min
+ var selEnd = originalRange.max
+
+ if (selEnd < replaceStart) {
+ // The entire originalRange is before the insertion point – we don't have to adjust
+ // the mark at all, so skip the math.
+ return originalRange
+ }
+
+ if (selStart <= replaceStart && replaceEnd <= selEnd) {
+ // The insertion is entirely inside the originalRange, move the end only.
+ val diff = insertedTextLength - (replaceEnd - replaceStart)
+ // Preserve "cursorness".
+ if (selStart == selEnd) {
+ selStart += diff
+ }
+ selEnd += diff
+ } else if (selStart > replaceStart && selEnd < replaceEnd) {
+ // originalRange is entirely inside replacement, move it to the end.
+ selStart = replaceStart + insertedTextLength
+ selEnd = replaceStart + insertedTextLength
+ } else if (selStart >= replaceEnd) {
+ // The entire originalRange is after the insertion, so shift everything forward.
+ val diff = insertedTextLength - (replaceEnd - replaceStart)
+ selStart += diff
+ selEnd += diff
+ } else if (replaceStart < selStart) {
+ // Insertion is around start of originalRange, truncate start of originalRange.
+ selStart = replaceStart + insertedTextLength
+ selEnd += insertedTextLength - (replaceEnd - replaceStart)
+ } else {
+ // Insertion is around end of originalRange, truncate end of originalRange.
+ selEnd = replaceStart
+ }
+ // should not validate
+ return TextRange(selStart, selEnd)
+}
+
+/**
* Insert [text] at the given [index] in this value. Pass 0 to insert [text] at the beginning of
* this buffer, and pass [TextFieldBuffer.length] to insert [text] at the end of this buffer.
*
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index aaa5947..2864629 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -21,7 +21,6 @@
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.internal.checkPrecondition
-import androidx.compose.foundation.text.input.internal.EditingBuffer
import androidx.compose.foundation.text.input.internal.undo.TextFieldEditUndoBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -38,16 +37,8 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.coerceIn
-import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDecoration
-internal fun TextFieldState(initialValue: TextFieldValue): TextFieldState {
- return TextFieldState(
- initialText = initialValue.text,
- initialSelection = initialValue.selection
- )
-}
-
/**
* The editable text state of a text field, including both the [text] itself and position of the
* cursor or selection.
@@ -81,14 +72,17 @@
internal val textUndoManager: TextUndoManager = initialTextUndoManager
/**
- * The editing buffer used for applying editor commands from IME. All edits coming from gestures
- * or IME commands, must be reflected on this buffer eventually.
+ * The buffer used for applying editor commands from IME. All edits coming from gestures or IME
+ * commands must be reflected on this buffer eventually.
*/
@VisibleForTesting
- internal var mainBuffer: EditingBuffer =
- EditingBuffer(
- text = initialText,
- selection = initialSelection.coerceIn(0, initialText.length)
+ internal var mainBuffer: TextFieldBuffer =
+ TextFieldBuffer(
+ initialValue =
+ TextFieldCharSequence(
+ text = initialText,
+ selection = initialSelection.coerceIn(0, initialText.length)
+ )
)
/**
@@ -225,8 +219,7 @@
textUndoManager.clearHistory()
}
syncMainBufferToTemporaryBuffer(
- textFieldBuffer = newValue,
- newComposition = null, // new composition will be decided by the IME
+ temporaryBuffer = newValue,
textChanged = textChanged,
selectionChanged = selectionChanged
)
@@ -256,13 +249,13 @@
* @param restartImeIfContentChanges Whether IME should be restarted if the proposed changes end
* up editing the text content. Only pass false to this argument if the source of the changes
* is IME itself.
- * @param block The function that updates the current editing buffer.
+ * @param block The function that updates the current buffer.
*/
internal inline fun editAsUser(
inputTransformation: InputTransformation?,
restartImeIfContentChanges: Boolean = true,
undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
- block: EditingBuffer.() -> Unit
+ block: TextFieldBuffer.() -> Unit
) {
mainBuffer.changeTracker.clearChanges()
mainBuffer.block()
@@ -284,16 +277,11 @@
* allocate an additional buffer like [edit] method because changes are ignored and it's not a
* public API.
*/
- internal inline fun editWithNoSideEffects(block: EditingBuffer.() -> Unit) {
+ internal inline fun editWithNoSideEffects(block: TextFieldBuffer.() -> Unit) {
mainBuffer.changeTracker.clearChanges()
mainBuffer.block()
- val afterEditValue =
- TextFieldCharSequence(
- text = mainBuffer.toString(),
- selection = mainBuffer.selection,
- composition = mainBuffer.composition
- )
+ val afterEditValue = mainBuffer.toTextFieldCharSequence()
updateValueAndNotifyListeners(
oldValue = value,
@@ -395,10 +383,7 @@
val selectionChangedByFilter = textFieldBuffer.selection != afterEditValue.selection
if (textChangedByFilter || selectionChangedByFilter) {
syncMainBufferToTemporaryBuffer(
- textFieldBuffer = textFieldBuffer,
- // Composition should be decided by the IME after the content or selection has been
- // changed programmatically, outside the knowledge of IME.
- newComposition = null,
+ temporaryBuffer = textFieldBuffer,
textChanged = textChangedByFilter,
selectionChanged = selectionChangedByFilter
)
@@ -430,10 +415,7 @@
* 3. Applying Undo/Redo actions that should not trigger side effects.
*
* Eventually all changes, no matter the source, should be committed to [value]. Also, they have
- * to trigger the content change listeners. However, we have two different buffers called
- * [EditingBuffer] and [TextFieldBuffer]. All developer facing APIs use [TextFieldBuffer] to
- * change the state but internal ones use [EditingBuffer]. This function consolidates both forms
- * of updates, then commits the result to [value].
+ * to trigger the content change listeners.
*
* Finally notifies the listeners in [notifyImeListeners] that the contents of this
* [TextFieldState] has changed.
@@ -520,67 +502,52 @@
}
/**
- * Carries changes made to a [TextFieldBuffer] into [mainBuffer], then updates the [value]. This
+ * Carries changes made to a [temporaryBuffer] into [mainBuffer], then updates the [value]. This
* usually happens when the edit source is something programmatic like [edit] or
- * [InputTransformation]. IME commands are applied directly on [mainBuffer].
+ * [InputTransformation]. Normally IME commands are applied directly on [mainBuffer].
*
- * @param textFieldBuffer Source buffer that will be used to sync the mainBuffer.
- * @param newComposition TextFieldBuffer does not allow changing composition. This parameter is
- * an opportunity to decide what the mainBuffer's new composition should be.
- * @param textChanged Whether the text content inside [textFieldBuffer] is different than
+ * @param temporaryBuffer Source buffer that will be used to sync the mainBuffer.
+ * @param textChanged Whether the text content inside [temporaryBuffer] is different than
* [mainBuffer]'s text content. Although this value can be calculated by this function, some
* callers already do the comparison before hand, so there's no need to recalculate it.
- * @param selectionChanged Whether the selection inside [textFieldBuffer] is different than
+ * @param selectionChanged Whether the selection inside [temporaryBuffer] is different than
* [mainBuffer]'s selection.
*/
@VisibleForTesting
internal fun syncMainBufferToTemporaryBuffer(
- textFieldBuffer: TextFieldBuffer,
- newComposition: TextRange?,
+ temporaryBuffer: TextFieldBuffer,
textChanged: Boolean,
selectionChanged: Boolean
) {
- val bufferString = mainBuffer.toString()
-
- val bufferState =
- TextFieldCharSequence(bufferString, mainBuffer.selection, mainBuffer.composition)
-
- val compositionChanged = newComposition != mainBuffer.composition
+ val oldValue = mainBuffer.toTextFieldCharSequence()
if (textChanged) {
// reset the buffer in its entirety
mainBuffer =
- EditingBuffer(
- text = textFieldBuffer.toString(),
- selection = textFieldBuffer.selection
+ TextFieldBuffer(
+ initialValue =
+ TextFieldCharSequence(
+ text = temporaryBuffer.toString(),
+ selection = temporaryBuffer.selection
+ ),
)
} else if (selectionChanged) {
- mainBuffer.setSelection(textFieldBuffer.selection.start, textFieldBuffer.selection.end)
+ mainBuffer.selection =
+ TextRange(temporaryBuffer.selection.start, temporaryBuffer.selection.end)
}
- if (newComposition == null || newComposition.collapsed) {
- mainBuffer.commitComposition()
- } else {
- mainBuffer.setComposition(newComposition.min, newComposition.max)
- }
+ // Composition should be decided by the IME after the content or selection has been
+ // changed programmatically, outside the knowledge of the IME.
+ mainBuffer.commitComposition()
- if (textChanged || (!selectionChanged && compositionChanged)) {
- mainBuffer.commitComposition()
- }
-
- val finalValue =
- TextFieldCharSequence(
- text = if (textChanged) textFieldBuffer.toString() else bufferString,
- selection = mainBuffer.selection,
- composition = mainBuffer.composition
- )
+ val finalValue = mainBuffer.toTextFieldCharSequence()
// We cannot use `value` as the old value here because intermediate IME changes are only
// applied on mainBuffer (this only happens if syncMainBufferToTemporaryBuffer is triggered
// after an InputTransformation). We must pass in the latest state just before finalValue is
// calculated. This is the state IME knows about and is synced with.
updateValueAndNotifyListeners(
- oldValue = bufferState,
+ oldValue = oldValue,
newValue = finalValue,
restartImeIfContentChanges = true
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditCommand.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditCommand.kt
index 970b93c..3483236 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditCommand.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditCommand.kt
@@ -16,10 +16,14 @@
package androidx.compose.foundation.text.input.internal
+import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.internal.requirePrecondition
-import androidx.compose.foundation.text.findFollowingBreak
import androidx.compose.foundation.text.findPrecedingBreak
import androidx.compose.foundation.text.input.PlacedAnnotation
+import androidx.compose.foundation.text.input.TextFieldBuffer
+import androidx.compose.foundation.text.input.adjustTextRange
+import androidx.compose.foundation.text.input.delete
+import androidx.compose.ui.text.TextRange
/**
* Commit final [text] to the text box and set the new cursor position.
@@ -30,19 +34,20 @@
* @param text The text to commit.
* @param newCursorPosition The cursor position after inserted text.
*/
-internal fun EditingBuffer.commitText(text: String, newCursorPosition: Int) {
+internal fun TextFieldBuffer.commitText(text: String, newCursorPosition: Int) {
// API description says replace ongoing composition text if there. Then, if there is no
// composition text, insert text into cursor position or replace selection.
- if (hasComposition()) {
- replace(compositionStart, compositionEnd, text)
+ val compositionRange = composition
+ if (compositionRange != null) {
+ imeReplace(compositionRange.start, compositionRange.end, text)
} else {
// In this editing buffer, insert into cursor or replace selection are equivalent.
- replace(selectionStart, selectionEnd, text)
+ imeReplace(selection.start, selection.end, text)
}
// After replace function is called, the editing buffer places the cursor at the end of the
// modified range.
- val newCursor = cursor
+ val newCursor = selection.start
// See above API description for the meaning of newCursorPosition.
val newCursorInBuffer =
@@ -52,7 +57,7 @@
newCursor + newCursorPosition - text.length
}
- cursor = newCursorInBuffer.coerceIn(0, length)
+ selection = TextRange(newCursorInBuffer.coerceIn(0, length))
}
/**
@@ -64,7 +69,7 @@
* @param start The inclusive start offset of the composing region.
* @param end The exclusive end offset of the composing region
*/
-internal fun EditingBuffer.setComposingRegion(start: Int, end: Int) {
+internal fun TextFieldBuffer.setComposingRegion(start: Int, end: Int) {
// The API description says, different from SetComposingText, SetComposingRegion must
// preserve the ongoing composition text and set new composition.
if (hasComposition()) {
@@ -95,31 +100,35 @@
* @param annotations Text annotations that IME attaches to the composing region. e.g. background
* color or underline styling.
*/
-internal fun EditingBuffer.setComposingText(
+internal fun TextFieldBuffer.setComposingText(
text: String,
newCursorPosition: Int,
annotations: List<PlacedAnnotation>? = null
) {
- if (hasComposition()) {
+ val compositionRange = composition
+ if (compositionRange != null) {
// API doc says, if there is ongoing composing text, replace it with new text.
- val compositionStart = compositionStart
- replace(compositionStart, compositionEnd, text)
+ imeReplace(compositionRange.start, compositionRange.end, text)
if (text.isNotEmpty()) {
- setComposition(compositionStart, compositionStart + text.length, annotations)
+ setComposition(
+ compositionRange.start,
+ compositionRange.start + text.length,
+ annotations
+ )
}
} else {
// If there is no composing text, insert composing text into cursor position with
// removing selected text if any.
- val selectionStart = selectionStart
- replace(selectionStart, selectionEnd, text)
+ val initialSelectionStart = selection.start
+ imeReplace(initialSelectionStart, selection.end, text)
if (text.isNotEmpty()) {
- setComposition(selectionStart, selectionStart + text.length, annotations)
+ setComposition(initialSelectionStart, initialSelectionStart + text.length, annotations)
}
}
// After replace function is called, the editing buffer places the cursor at the end of the
// modified range.
- val newCursor = cursor
+ val newCursor = selection.start
// See above API description for the meaning of newCursorPosition.
val newCursorInBuffer =
@@ -129,7 +138,7 @@
newCursor + newCursorPosition - text.length
}
- cursor = newCursorInBuffer.coerceIn(0, length)
+ selection = TextRange(newCursorInBuffer.coerceIn(0, length))
}
/**
@@ -148,7 +157,10 @@
* @param lengthAfterCursor The number of characters in UTF-16 after the cursor to be deleted. Must
* be non-negative.
*/
-internal fun EditingBuffer.deleteSurroundingText(lengthBeforeCursor: Int, lengthAfterCursor: Int) {
+internal fun TextFieldBuffer.deleteSurroundingText(
+ lengthBeforeCursor: Int,
+ lengthAfterCursor: Int
+) {
requirePrecondition(lengthBeforeCursor >= 0 && lengthAfterCursor >= 0) {
"Expected lengthBeforeCursor and lengthAfterCursor to be non-negative, were " +
"$lengthBeforeCursor and $lengthAfterCursor respectively."
@@ -156,13 +168,13 @@
// calculate the end with safe addition since lengthAfterCursor can be set to e.g. Int.MAX
// by the input
- val end = selectionEnd.addExactOrElse(lengthAfterCursor) { length }
- delete(selectionEnd, minOf(end, length))
+ val end = selection.end.addExactOrElse(lengthAfterCursor) { length }
+ imeDelete(selection.end, minOf(end, length))
// calculate the start with safe subtraction since lengthBeforeCursor can be set to e.g.
// Int.MAX by the input
- val start = selectionStart.subtractExactOrElse(lengthBeforeCursor) { 0 }
- delete(maxOf(0, start), selectionStart)
+ val start = selection.start.subtractExactOrElse(lengthBeforeCursor) { 0 }
+ imeDelete(maxOf(0, start), selection.start)
}
/**
@@ -179,7 +191,7 @@
* @param lengthAfterCursor The number of characters in Unicode code points after the cursor to be
* deleted. Must be non-negative.
*/
-internal fun EditingBuffer.deleteSurroundingTextInCodePoints(
+internal fun TextFieldBuffer.deleteSurroundingTextInCodePoints(
lengthBeforeCursor: Int,
lengthAfterCursor: Int
) {
@@ -193,16 +205,16 @@
var beforeLenInChars = 0
for (i in 0 until lengthBeforeCursor) {
beforeLenInChars++
- if (selectionStart > beforeLenInChars) {
- val lead = this[selectionStart - beforeLenInChars - 1]
- val trail = this[selectionStart - beforeLenInChars]
+ if (selection.start > beforeLenInChars) {
+ val lead = asCharSequence()[selection.start - beforeLenInChars - 1]
+ val trail = asCharSequence()[selection.start - beforeLenInChars]
if (isSurrogatePair(lead, trail)) {
beforeLenInChars++
}
} else {
// overflowing
- beforeLenInChars = selectionStart
+ beforeLenInChars = selection.start
break
}
}
@@ -210,22 +222,22 @@
var afterLenInChars = 0
for (i in 0 until lengthAfterCursor) {
afterLenInChars++
- if (selectionEnd + afterLenInChars < length) {
- val lead = this[selectionEnd + afterLenInChars - 1]
- val trail = this[selectionEnd + afterLenInChars]
+ if (selection.end + afterLenInChars < length) {
+ val lead = asCharSequence()[selection.end + afterLenInChars - 1]
+ val trail = asCharSequence()[selection.end + afterLenInChars]
if (isSurrogatePair(lead, trail)) {
afterLenInChars++
}
} else {
// overflowing
- afterLenInChars = length - selectionEnd
+ afterLenInChars = length - selection.end
break
}
}
- delete(selectionEnd, selectionEnd + afterLenInChars)
- delete(selectionStart - beforeLenInChars, selectionStart)
+ imeDelete(selection.end, selection.end + afterLenInChars)
+ imeDelete(selection.start - beforeLenInChars, selection.start)
}
/**
@@ -236,7 +248,7 @@
* See
* [`finishComposingText`](https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#finishComposingText()).
*/
-internal fun EditingBuffer.finishComposingText() {
+internal fun TextFieldBuffer.finishComposingText() {
commitComposition()
}
@@ -247,55 +259,32 @@
* there is selection, delete whole selected range. If there is no composition and selection,
* perform backspace key event at the cursor position.
*/
-internal fun EditingBuffer.backspace() {
- if (hasComposition()) {
- delete(compositionStart, compositionEnd)
- } else if (cursor == -1) {
- val delStart = selectionStart
- val delEnd = selectionEnd
- cursor = selectionStart
- delete(delStart, delEnd)
- } else if (cursor != 0) {
- val prevCursorPos = toString().findPrecedingBreak(cursor)
- delete(prevCursorPos, cursor)
+internal fun TextFieldBuffer.backspace() {
+ val compositionRange = composition
+ if (compositionRange != null) {
+ imeDelete(compositionRange.start, compositionRange.end)
+ } else if (hasSelection) {
+ val delStart = selection.start
+ val delEnd = selection.end
+ selection = TextRange(selection.start)
+ imeDelete(delStart, delEnd)
+ } else if (selection.collapsed && selection.start > 0) {
+ val prevCursorPos = toString().findPrecedingBreak(selection.start)
+ imeDelete(prevCursorPos, selection.start)
}
}
-/**
- * Moves the cursor with [amount] characters.
- *
- * If there is selection, cancel the selection first and move the cursor to the selection start
- * position. Then perform the cursor movement.
- *
- * @param amount The amount of cursor movement. If you want to move backward, pass negative value.
- */
-internal fun EditingBuffer.moveCursor(amount: Int) {
- if (cursor == -1) {
- cursor = selectionStart
- }
-
- var newCursor = selectionStart
- val bufferText = toString()
- if (amount > 0) {
- for (i in 0 until amount) {
- val next = bufferText.findFollowingBreak(newCursor)
- if (next == -1) break
- newCursor = next
- }
- } else {
- for (i in 0 until -amount) {
- val prev = bufferText.findPrecedingBreak(newCursor)
- if (prev == -1) break
- newCursor = prev
- }
- }
-
- cursor = newCursor
-}
-
/** Deletes all the text in the buffer. */
-internal fun EditingBuffer.deleteAll() {
- replace(0, length, "")
+internal fun TextFieldBuffer.deleteAll() {
+ imeReplace(0, length, "")
+}
+
+/** Sets selection while coercing the given parameters to the buffer range. */
+internal fun TextFieldBuffer.setSelection(start: Int, end: Int) {
+ val clampedStart = start.coerceIn(0, length)
+ val clampedEnd = end.coerceIn(0, length)
+
+ selection = TextRange(clampedStart, clampedEnd)
}
/**
@@ -304,3 +293,87 @@
*/
private fun isSurrogatePair(high: Char, low: Char): Boolean =
high.isHighSurrogate() && low.isLowSurrogate()
+
+/**
+ * Replace function wrapper to be called by edit commands that are originated from IME.
+ *
+ * IME editing is usually done in batches to maintain a composing range that helps with auto-correct
+ * and auto-suggest. For example when typing a word through software keyboard, the IME doesn't send
+ * a character typed event, instead it sends a replace command that swaps the currently composing
+ * word with a character added version, i.e. replace("worl" -> "world").
+ *
+ * This unfortunately confuses the change trackers. Therefore, we coerce the changes from both sides
+ * to find the absolute minimum version of the replace call so that a change tracker only reports
+ * the actual intended edit, instead of what the IME sends as a command. For example when IME tells
+ * us to replace "worl" with "world", this function converts this into "insert d".
+ *
+ * [imeReplace] also uses a different selection adjustment than regular [replace]. The selection is
+ * always placed as a cursor at the end of the change.
+ */
+@VisibleForTesting
+internal fun TextFieldBuffer.imeReplace(start: Int, end: Int, text: CharSequence) {
+ val min = minOf(start, end)
+ val max = maxOf(start, end)
+
+ // coerce the replacement bounds before tracking change. This is mostly necessary for
+ // composition based typing when each keystroke may trigger a replace function that looks
+ // like "abcd" => "abcde".
+
+ // b(351165334)
+ // Since we are starting from the left hand side to compare the strings, when "abc" is
+ // replaced with "aabc", it will be reported as an `a` is inserted at `TextRange(1)` instead
+ // of the more logical possibility; `TextRange(0)`. This replace call cannot differentiate
+ // between the two possible cases because we have no way of really knowing what was the
+ // intention of the user beyond this replace call. We prefer to choose the more logical
+ // explanation for right hand side since it's the more common direction of typing. This is
+ // guaranteed by the fact that we start our coercion from left hand side, and finally apply
+ // the right hand side.
+
+ // coerce min
+ var i = 0
+ var cMin = min
+ while (cMin < max && i < text.length && text[i] == asCharSequence()[cMin]) {
+ i++
+ cMin++
+ }
+ // coerce max
+ var j = text.length
+ var cMax = max
+ while (cMax > cMin && j > i && text[j - 1] == asCharSequence()[cMax - 1]) {
+ j--
+ cMax--
+ }
+
+ if (cMin != cMax || i != j) {
+ replace(start = cMin, end = cMax, text = text.subSequence(i, j))
+ }
+
+ // IME replace calls should always place the selection at the end of replaced region.
+ // Also default replace behavior for composition is to cancel it. This is again true for
+ // imeReplace. So we don't readjust the composition here.
+ selection = TextRange(min + text.length)
+}
+
+/**
+ * Similar to regular [TextFieldBuffer.delete] function but also maintains the composing region
+ * instead of fully wiping it.
+ */
+@VisibleForTesting
+internal fun TextFieldBuffer.imeDelete(start: Int, end: Int) {
+ val initialComposition = composition
+
+ val min = minOf(start, end)
+ val max = maxOf(start, end)
+ delete(min, max)
+
+ // composition is lost by calling delete but we should restore it for delete calls that
+ // originate from the IME
+ initialComposition?.let {
+ val adjustedComposition = adjustTextRange(initialComposition, min, max, 0)
+ if (adjustedComposition.collapsed) {
+ commitComposition()
+ } else {
+ setComposition(adjustedComposition.min, adjustedComposition.max)
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditingBuffer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditingBuffer.kt
deleted file mode 100644
index 2c72cbd..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/EditingBuffer.kt
+++ /dev/null
@@ -1,429 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text.input.internal
-
-import androidx.compose.foundation.internal.requirePrecondition
-import androidx.compose.foundation.text.input.PlacedAnnotation
-import androidx.compose.foundation.text.input.TextHighlightType
-import androidx.compose.runtime.collection.MutableVector
-import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.util.fastForEach
-
-/**
- * The editing buffer
- *
- * This class manages the all editing relate states, editing buffers, selection, styles, etc.
- */
-internal class EditingBuffer(
- /** The initial text of this editing buffer */
- text: String,
- /**
- * The initial selection range of this buffer. If you provide collapsed selection, it is treated
- * as the cursor position. The cursor and selection cannot exists at the same time. The
- * selection must points the valid index of the initialText, otherwise IndexOutOfBoundsException
- * will be thrown.
- */
- selection: TextRange
-) {
- internal companion object {
- const val NOWHERE = -1
- }
-
- var composingAnnotations: MutableVector<AnnotatedString.Range<AnnotatedString.Annotation>>? =
- null
-
- private val gapBuffer = PartialGapBuffer(text)
-
- val changeTracker = ChangeTracker()
-
- /** The inclusive selection start offset */
- var selectionStart = selection.start
- private set(value) {
- requirePrecondition(value >= 0) { "Cannot set selectionStart to a negative value" }
- field = value
- highlight = null
- }
-
- /** The exclusive selection end offset */
- var selectionEnd = selection.end
- private set(value) {
- requirePrecondition(value >= 0) { "Cannot set selectionEnd to a negative value" }
- field = value
- highlight = null
- }
-
- /**
- * A highlighted range of text. This may be used to display handwriting gesture previews from
- * the IME.
- */
- var highlight: Pair<TextHighlightType, TextRange>? = null
-
- /**
- * The inclusive composition start offset
- *
- * If there is no composing text, returns -1
- */
- var compositionStart = NOWHERE
- private set
-
- /**
- * The exclusive composition end offset
- *
- * If there is no composing text, returns -1
- */
- var compositionEnd = NOWHERE
- private set
-
- /** Helper function that returns true if the editing buffer has composition text */
- fun hasComposition(): Boolean = compositionStart != NOWHERE
-
- /** Returns the composition information as TextRange. Returns null if no composition is set. */
- val composition: TextRange?
- get() =
- if (hasComposition()) {
- TextRange(compositionStart, compositionEnd)
- } else null
-
- /** Returns the selection information as TextRange */
- val selection: TextRange
- get() = TextRange(selectionStart, selectionEnd)
-
- /** Helper accessor for cursor offset */
- /*VisibleForTesting*/
- var cursor: Int
- /**
- * Return the cursor offset.
- *
- * Since selection and cursor cannot exist at the same time, return -1 if there is a
- * selection.
- */
- get() = if (selectionStart == selectionEnd) selectionEnd else -1
- /**
- * Set the cursor offset.
- *
- * Since selection and cursor cannot exist at the same time, cancel selection if there is.
- */
- set(cursor) = setSelection(cursor, cursor)
-
- /** [] operator for the character at the index. */
- operator fun get(index: Int): Char = gapBuffer[index]
-
- /** Returns the length of the buffer. */
- val length: Int
- get() = gapBuffer.length
-
- init {
- checkRange(selection.start, selection.end)
- }
-
- /**
- * Replace the text and move the cursor to the end of inserted text.
- *
- * This function cancels selection if there is any.
- *
- * @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
- */
- fun replace(start: Int, end: Int, text: CharSequence) {
- checkRange(start, end)
- val min = minOf(start, end)
- val max = maxOf(start, end)
-
- // coerce the replacement bounds before tracking change. This is mostly necessary for
- // composition based typing when each keystroke may trigger a replace function that looks
- // like "abcd" => "abcde".
-
- // b(351165334)
- // Since we are starting from the left hand side to compare the strings, when "abc" is
- // replaced with "aabc", it will be reported as an `a` is inserted at `TextRange(1)` instead
- // of the more logical possibility; `TextRange(0)`. This replace call cannot differentiate
- // between the two possible cases because we have no way of really knowing what was the
- // intention of the user beyond this replace call. We prefer to choose the more logical
- // explanation for right hand side since it's the more common direction of typing. This is
- // guaranteed by the fact that we start our coercion from left hand side, and finally apply
- // the right hand side.
-
- // coerce min
- var i = 0
- var cMin = min
- while (cMin < max && i < text.length && text[i] == gapBuffer[cMin]) {
- i++
- cMin++
- }
- // coerce max
- var j = text.length
- var cMax = max
- while (cMax > cMin && j > i && text[j - 1] == gapBuffer[cMax - 1]) {
- j--
- cMax--
- }
-
- changeTracker.trackChange(cMin, cMax, j - i)
-
- gapBuffer.replace(min, max, text)
-
- // On Android, all text modification APIs also provides explicit cursor location. On the
- // other hand, desktop applications usually don't. So, here tentatively move the cursor to
- // the end offset of the editing area for desktop like application. In case of Android,
- // implementation will call setSelection immediately after replace function to update this
- // tentative cursor location.
- selectionStart = min + text.length
- selectionEnd = min + text.length
-
- // Similarly, if text modification happens, cancel ongoing composition. If caller wants to
- // change the composition text, it is caller's responsibility to call setComposition again
- // to set composition range after replace function.
- compositionStart = NOWHERE
- compositionEnd = NOWHERE
- // Do not deallocate an existing list. We will probably use it again.
- composingAnnotations?.clear()
-
- highlight = null
- }
-
- /**
- * Remove the given range of text.
- *
- * Different from replace method, this doesn't move cursor location to the end of modified text.
- * Instead, preserve the selection with adjusting the deleted text.
- */
- fun delete(start: Int, end: Int) {
- checkRange(start, end)
- val deleteRange = TextRange(start, end)
-
- changeTracker.trackChange(start, end, 0)
-
- gapBuffer.replace(deleteRange.min, deleteRange.max, "")
-
- val newSelection =
- updateRangeAfterDelete(TextRange(selectionStart, selectionEnd), deleteRange)
- selectionStart = newSelection.start
- selectionEnd = newSelection.end
-
- if (hasComposition()) {
- val compositionRange = TextRange(compositionStart, compositionEnd)
- val newComposition = updateRangeAfterDelete(compositionRange, deleteRange)
- if (newComposition.collapsed) {
- commitComposition()
- } else {
- compositionStart = newComposition.min
- compositionEnd = newComposition.max
- }
- }
-
- highlight = null
- }
-
- /**
- * Mark the specified area of the text as selected text.
- *
- * You can set cursor by specifying the same value to `start` and `end`. The reversed range is
- * not allowed.
- *
- * @param start the inclusive start offset of the selection
- * @param end the exclusive end offset of the selection
- */
- fun setSelection(start: Int, end: Int) {
- val clampedStart = start.coerceIn(0, length)
- val clampedEnd = end.coerceIn(0, length)
-
- selectionStart = clampedStart
- selectionEnd = clampedEnd
- }
-
- /**
- * Mark a range of text to be highlighted. This may be used to display handwriting gesture
- * previews from the IME.
- *
- * An empty or reversed range is not allowed.
- *
- * @param type the highlight type
- * @param start the inclusive start offset of the highlight
- * @param end the exclusive end offset of the highlight
- */
- fun setHighlight(type: TextHighlightType, start: Int, end: Int) {
- if (start >= end) {
- throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
- }
- val clampedStart = start.coerceIn(0, length)
- val clampedEnd = end.coerceIn(0, length)
-
- highlight = Pair(type, TextRange(clampedStart, clampedEnd))
- }
-
- /** Clear the highlighted text range. */
- fun clearHighlight() {
- highlight = null
- }
-
- /**
- * Mark the specified area of the text as composition text.
- *
- * The empty range or reversed range is not allowed. Use clearComposition in case of clearing
- * composition.
- *
- * @param start the inclusive start offset of the composition
- * @param end the exclusive end offset of the composition
- * @param annotations Annotations that are attached to the composing region of text. This
- * function does not check whether the given annotations are inside the composing region. It
- * simply adds them to the current buffer while adjusting their range according to where the
- * new composition region is set.
- * @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
- * @throws IllegalArgumentException if start is larger than or equal to end. (reversed or
- * collapsed range)
- */
- fun setComposition(start: Int, end: Int, annotations: List<PlacedAnnotation>? = null) {
- if (start < 0 || start > gapBuffer.length) {
- throw IndexOutOfBoundsException(
- "start ($start) offset is outside of text region ${gapBuffer.length}"
- )
- }
- if (end < 0 || end > gapBuffer.length) {
- throw IndexOutOfBoundsException(
- "end ($end) offset is outside of text region ${gapBuffer.length}"
- )
- }
- if (start >= end) {
- throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
- }
-
- compositionStart = start
- compositionEnd = end
-
- this.composingAnnotations?.clear()
- if (!annotations.isNullOrEmpty()) {
- if (this.composingAnnotations == null) {
- this.composingAnnotations = mutableVectorOf()
- }
- annotations.fastForEach {
- // place the annotations at the correct indices in the buffer.
- this.composingAnnotations?.add(
- it.copy(start = it.start + start, end = it.end + start)
- )
- }
- }
- }
-
- /** Commits the ongoing composition text and reset the composition range. */
- fun commitComposition() {
- compositionStart = NOWHERE
- compositionEnd = NOWHERE
- composingAnnotations?.clear()
- }
-
- override fun toString(): String = gapBuffer.toString()
-
- private fun checkRange(start: Int, end: Int) {
- if (start < 0 || start > gapBuffer.length) {
- throw IndexOutOfBoundsException(
- "start ($start) offset is outside of text region ${gapBuffer.length}"
- )
- }
-
- if (end < 0 || end > gapBuffer.length) {
- throw IndexOutOfBoundsException(
- "end ($end) offset is outside of text region ${gapBuffer.length}"
- )
- }
- }
-}
-
-/**
- * Returns the updated TextRange for [target] after the [deleted] TextRange is deleted as a Pair.
- *
- * If the [deleted] Range covers the whole target, Pair(-1,-1) is returned.
- */
-/*@VisibleForTesting*/
-internal fun updateRangeAfterDelete(target: TextRange, deleted: TextRange): TextRange {
- var targetMin = target.min
- var targetMax = target.max
-
- // Following figure shows the deletion range and composition range.
- // |---| represents deleted range.
- // |===| represents target range.
- if (deleted.intersects(target)) {
- if (deleted.contains(target)) {
- // Input:
- // Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Deleted : |-------------|
- // Target : |======|
- //
- // Result:
- // Buffer : ABCDETUVWXYZ
- // Target :
- targetMin = deleted.min
- targetMax = targetMin
- } else if (target.contains(deleted)) {
- // Input:
- // Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Deleted : |------|
- // Target : |==========|
- //
- // Result:
- // Buffer : ABCDEFGHIQRSTUVWXYZ
- // Target : |===|
- targetMax -= deleted.length
- } else if (deleted.contains(targetMin)) {
- // Input:
- // Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Deleted : |---------|
- // Target : |========|
- //
- // Result:
- // Buffer : ABCDEFPQRSTUVWXYZ
- // Target : |=====|
- targetMin = deleted.min
- targetMax -= deleted.length
- } else { // deleteRange contains myMax
- // Input:
- // Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Deleted : |---------|
- // Target : |=======|
- //
- // Result:
- // Buffer : ABCDEFGHSTUVWXYZ
- // Target : |====|
- targetMax = deleted.min
- }
- } else {
- if (targetMax <= deleted.min) {
- // Input:
- // Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Deleted : |-------|
- // Target : |=======|
- //
- // Result:
- // Buffer : ABCDEFGHIJKLTUVWXYZ
- // Target : |=======|
- // do nothing
- } else {
- // Input:
- // Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
- // Deleted : |-------|
- // Target : |=======|
- //
- // Result:
- // Buffer : AJKLMNOPQRSTUVWXYZ
- // Target : |=======|
- targetMin -= deleted.length
- targetMax -= deleted.length
- }
- }
-
- return TextRange(targetMin, targetMax)
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
index 5fc0419..f60a95c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.TextHighlightType
+import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.internal.IndexTransformationType.Deletion
import androidx.compose.foundation.text.input.internal.IndexTransformationType.Insertion
import androidx.compose.foundation.text.input.internal.IndexTransformationType.Replacement
@@ -302,7 +303,7 @@
*/
inline fun editUntransformedTextAsUser(
restartImeIfContentChanges: Boolean = true,
- block: EditingBuffer.() -> Unit
+ block: TextFieldBuffer.() -> Unit
) {
textFieldState.editAsUser(
inputTransformation = inputTransformation,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoOperation.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoOperation.kt
index 96cfeda..27b990b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoOperation.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoOperation.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.input.internal.undo
import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.foundation.text.input.internal.setSelection
import androidx.compose.foundation.text.timeNowMillis
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
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 130f0a0..a32d2e7 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
@@ -17,42 +17,75 @@
package androidx.compose.integration.macrobenchmark.target
import android.os.Bundle
+import android.webkit.WebView
+import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerScope
+import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.launch
class PagerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- val itemCount = intent.getIntExtra(ExtraItemCount, 3000)
+ val benchmarkType = intent.getStringExtra(BenchmarkType.Key)
+ val enableTab = intent.getBooleanExtra(BenchmarkType.Tab, false)
setContent {
- val pagerState = rememberPagerState { itemCount }
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- HorizontalPager(
- modifier =
- Modifier.height(400.dp)
- .semantics { contentDescription = "Pager" }
- .background(Color.White),
- state = pagerState,
- pageSize = PageSize.Fill
- ) {
- PagerItem(it)
+ val pagerState = rememberPagerState { ItemCount }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ if (enableTab) {
+ val scope = rememberCoroutineScope()
+ Button(
+ modifier = Modifier.semantics { contentDescription = "Next" },
+ onClick = {
+ scope.launch {
+ pagerState.animateScrollToPage(pagerState.currentPage + 1)
+ }
+ }
+ ) {
+ Text("Next Page")
+ }
+ }
+ when (benchmarkType) {
+ BenchmarkType.Grid -> BenchmarkPagerOfGrids(pagerState)
+ BenchmarkType.List -> BenchmarkPagerOfLists(pagerState)
+ BenchmarkType.WebView -> BenchmarkPagerOfWebViews(pagerState)
+ BenchmarkType.FullScreenImage -> BenchmarkPagerOfFullScreenImages(pagerState)
+ BenchmarkType.FixedSizeImage -> BenchmarkPagerOfFixedSizeImages(pagerState)
+ BenchmarkType.ListOfPager -> BenchmarkListOfPager()
+ else -> throw IllegalStateException("Benchmark Type not known ")
}
}
}
@@ -60,14 +93,139 @@
launchIdlenessTracking()
}
+ @Composable
+ fun BenchmarkPagerOfGrids(pagerState: PagerState) {
+ FullSizePager(pagerState) {
+ LazyVerticalGrid(GridCells.Fixed(4)) {
+ items(200) {
+ Card(modifier = Modifier.fillMaxWidth().height(64.dp).padding(8.dp)) {
+ Text(it.toString())
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun BenchmarkPagerOfLists(pagerState: PagerState) {
+ FullSizePager(pagerState) {
+ LazyColumn(modifier = Modifier.fillMaxWidth()) {
+ items(200) {
+ Card(modifier = Modifier.fillMaxWidth().height(64.dp).padding(8.dp)) {
+ Text(it.toString())
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun BenchmarkPagerOfWebViews(pagerState: PagerState) {
+ FullSizePager(pagerState) {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ WebView(context).apply {
+ settings.javaScriptEnabled = true
+ webViewClient = WebViewClient()
+
+ settings.loadWithOverviewMode = true
+ settings.useWideViewPort = true
+ settings.setSupportZoom(true)
+ }
+ },
+ update = { webView -> webView.loadUrl("https://www.google.com/") }
+ )
+ }
+ }
+
+ @Composable
+ fun BenchmarkPagerOfFullScreenImages(pagerState: PagerState) {
+ FullSizePager(pagerState) { page ->
+ val pageImage = Images[(page - ItemCount / 2).floorMod(5)]
+ Image(
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.fillMaxSize(),
+ painter = painterResource(pageImage.second),
+ contentDescription = stringResource(pageImage.third)
+ )
+ }
+ }
+
+ @Composable
+ fun BenchmarkPagerOfFixedSizeImages(pagerState: PagerState) {
+ FixedSizePager(pagerState) { page ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val pageImage = Images[(page - ItemCount / 2).floorMod(5)]
+ Image(
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.height(200.dp).fillMaxWidth(),
+ painter = painterResource(pageImage.second),
+ contentDescription = stringResource(pageImage.third)
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun FullSizePager(pagerState: PagerState, content: @Composable PagerScope.(Int) -> Unit) {
+ HorizontalPager(
+ modifier = Modifier.semantics { contentDescription = "Pager" }.background(Color.White),
+ state = pagerState,
+ pageSize = PageSize.Fill,
+ pageContent = content
+ )
+ }
+
+ @Composable
+ fun FixedSizePager(pagerState: PagerState, content: @Composable PagerScope.(Int) -> Unit) {
+ HorizontalPager(
+ modifier = Modifier.semantics { contentDescription = "Pager" }.background(Color.White),
+ state = pagerState,
+ pageSize = PageSize.Fixed(200.dp),
+ pageSpacing = 10.dp,
+ pageContent = content
+ )
+ }
+
+ @Composable
+ fun BenchmarkListOfPager() {
+ LazyColumn(Modifier.semantics { contentDescription = "List" }) {
+ items(ItemCount * ItemCount) {
+ FixedSizePager(rememberPagerState { ItemCount }) {
+ Box(Modifier.size(200.dp)) { Text("Page ${it.toString()}") }
+ }
+ }
+ }
+ }
+
companion object {
- const val ExtraItemCount = "ITEM_COUNT"
+ const val ItemCount = 100
+
+ object BenchmarkType {
+ val Key = "BenchmarkType"
+ val Tab = "EnableTab"
+ val Grid = "Pager of Grids"
+ val List = "Pager of List"
+ val WebView = "Pager of WebViews"
+ val FullScreenImage = "Pager of Full Screen Images"
+ val FixedSizeImage = "Pager of Fixed Size Images"
+ val ListOfPager = "Pager Inside A List"
+ }
+
+ val Images =
+ listOf(
+ Triple(0, R.drawable.carousel_image_1, R.string.carousel_image_1_description),
+ Triple(1, R.drawable.carousel_image_2, R.string.carousel_image_2_description),
+ Triple(2, R.drawable.carousel_image_3, R.string.carousel_image_3_description),
+ Triple(3, R.drawable.carousel_image_4, R.string.carousel_image_4_description),
+ Triple(4, R.drawable.carousel_image_5, R.string.carousel_image_5_description),
+ )
}
}
-@Composable
-private fun PagerItem(index: Int) {
- Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
- Text(text = index.toString(), color = Color.White)
+private fun Int.floorMod(other: Int): Int =
+ when (other) {
+ 0 -> this
+ else -> this - floorDiv(other) * other
}
-}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_1.jpg b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_1.jpg
new file mode 100644
index 0000000..b02612e
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_1.jpg
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_2.jpg b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_2.jpg
new file mode 100644
index 0000000..73162bb
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_2.jpg
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_3.jpg b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_3.jpg
new file mode 100644
index 0000000..d31f632
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_3.jpg
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_4.jpg b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_4.jpg
new file mode 100644
index 0000000..8362062
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_4.jpg
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_5.jpg b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_5.jpg
new file mode 100644
index 0000000..f5eb364
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/drawable-nodpi/carousel_image_5.jpg
Binary files differ
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml b/compose/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml
index 19b2ab5..88f7fac 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/res/values/strings.xml
@@ -26,4 +26,9 @@
<string name="io_settings_tos" translation_description="Open link to terms of service.">Terms of service</string>
<string name="io_settings_privacy_policy" translation_description="Open link to privacy policy.">Privacy policy</string>
<string name="io_settings_oss_licenses" translation_description="Open link to open source licenses.">Open sources licenses</string>
+ <string name="carousel_image_1_description">A racecar</string>
+ <string name="carousel_image_2_description">Dotonburi, Osaka</string>
+ <string name="carousel_image_3_description">The Grand Canyon</string>
+ <string name="carousel_image_4_description">A rock structure with its reflection mirrored over the sea</string>
+ <string name="carousel_image_5_description">A car on a long stretch of desert road</string>
</resources>
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
index ad51c92..a80379b 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/ComplexNestedListsScrollBenchmark.kt
@@ -49,7 +49,7 @@
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
compilationMode = CompilationMode.Full(),
- iterations = 8,
+ iterations = 5,
setupBlock = {
val intent = Intent()
intent.action = ACTION
@@ -59,7 +59,7 @@
val lazyColumn = device.findObject(By.desc(CONTENT_DESCRIPTION))
// Setting a gesture margin is important otherwise gesture nav is triggered.
lazyColumn.setGestureMargin(device.displayWidth / 5)
- for (i in 1..10) {
+ for (i in 1..8) {
// From center we scroll 2/3 of it which is 1/3 of the screen.
lazyColumn.drag(Point(lazyColumn.visibleCenter.x, lazyColumn.visibleCenter.y / 3))
device.wait(Until.findObject(By.desc(COMPOSE_IDLE)), 3000)
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt
index d39fb36..1cc8b56 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt
@@ -51,7 +51,7 @@
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
compilationMode = CompilationMode.Full(),
- iterations = 10,
+ iterations = 5,
setupBlock = {
val intent = Intent()
intent.action = ACTION
@@ -61,7 +61,7 @@
val lazyColumn = device.findObject(By.desc(CONTENT_DESCRIPTION))
// Setting a gesture margin is important otherwise gesture nav is triggered.
lazyColumn.setGestureMargin(device.displayWidth / 5)
- for (i in 1..10) {
+ for (i in 1..8) {
// From center we scroll 2/3 of it which is 1/3 of the screen.
lazyColumn.drag(Point(lazyColumn.visibleCenter.x, lazyColumn.visibleCenter.y / 3))
device.wait(Until.findObject(By.desc(COMPOSE_IDLE)), 3000)
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt
index 35b7f99..3ca318e 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt
@@ -14,10 +14,14 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalMetricApi::class)
+
package androidx.compose.integration.macrobenchmark
import android.content.Intent
import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.FrameTimingGfxInfoMetric
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
@@ -26,7 +30,6 @@
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
-import androidx.testutils.createCompilationParams
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -47,22 +50,167 @@
}
@Test
- fun scroll() {
+ fun pager_of_grids_gesture_scroll() {
benchmarkRule.measureRepeated(
packageName = PackageName,
- metrics = listOf(FrameTimingMetric()),
+ metrics = listOf(FrameTimingGfxInfoMetric()),
compilationMode = compilationMode,
- iterations = 10,
+ iterations = 5,
setupBlock = {
val intent = Intent()
intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.Grid)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val pager = device.findObject(By.desc(ContentDescription))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ pager.setGestureMargin(device.displayWidth / 5)
+ for (i in 1..EventRepeatCount) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ pager.swipe(Direction.LEFT, 1.0f)
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_grids_animated_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.Grid)
+ intent.putExtra(BenchmarkType.Tab, true)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val nextButton = device.findObject(By.desc(NextDescription))
+ repeat(EventRepeatCount) {
+ nextButton.click()
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_lists_gesture_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.List)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val pager = device.findObject(By.desc(ContentDescription))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ pager.setGestureMargin(device.displayWidth / 5)
+ for (i in 1..EventRepeatCount) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ pager.swipe(Direction.LEFT, 1.0f)
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_lists_animated_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.List)
+ intent.putExtra(BenchmarkType.Tab, true)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val nextButton = device.findObject(By.desc(NextDescription))
+ repeat(EventRepeatCount) {
+ nextButton.click()
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_webviews_gesture_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.WebView)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val pager = device.findObject(By.desc(ContentDescription))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ pager.setGestureMargin(device.displayWidth / 5)
+ for (i in 1..EventRepeatCount) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ pager.swipe(Direction.LEFT, 1.0f)
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_webviews_animated_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.WebView)
+ intent.putExtra(BenchmarkType.Tab, true)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val nextButton = device.findObject(By.desc(NextDescription))
+ repeat(EventRepeatCount) {
+ nextButton.click()
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_images_full_page_gesture_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.FullScreenImage)
+ intent.action = Action
startActivityAndWait(intent)
}
) {
val pager = device.findObject(By.desc(ContentDescription))
// Setting a gesture margin is important otherwise gesture nav is triggered.
pager.setGestureMargin(device.displayWidth / 5)
- for (i in 1..10) {
+ for (i in 1..EventRepeatCount) {
// From center we scroll 2/3 of it which is 1/3 of the screen.
pager.swipe(Direction.LEFT, 1.0f)
device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
@@ -70,15 +218,124 @@
}
}
+ @Test
+ fun pager_of_images_full_page_animated_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.FullScreenImage)
+ intent.putExtra(BenchmarkType.Tab, true)
+ intent.action = Action
+ startActivityAndWait(intent)
+ }
+ ) {
+ val nextButton = device.findObject(By.desc(NextDescription))
+ repeat(EventRepeatCount) {
+ nextButton.click()
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_images_fixed_size_page_gesture_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.FixedSizeImage)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val pager = device.findObject(By.desc(ContentDescription))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ pager.setGestureMargin(device.displayWidth / 5)
+ for (i in 1..EventRepeatCount) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ pager.swipe(Direction.LEFT, 1.0f)
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun pager_of_images_fixed_size_page_animated_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.FixedSizeImage)
+ intent.putExtra(BenchmarkType.Tab, true)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val nextButton = device.findObject(By.desc(NextDescription))
+ repeat(EventRepeatCount) {
+ nextButton.click()
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
+ @Test
+ fun list_of_pagers_gesture_scroll() {
+ benchmarkRule.measureRepeated(
+ packageName = PackageName,
+ metrics = listOf(FrameTimingGfxInfoMetric()),
+ compilationMode = compilationMode,
+ iterations = 5,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = Action
+ intent.putExtra(BenchmarkType.Key, BenchmarkType.ListOfPager)
+ startActivityAndWait(intent)
+ }
+ ) {
+ val pager = device.findObject(By.desc("List"))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ pager.setGestureMargin(device.displayHeight / 5)
+ for (i in 1..EventRepeatCount) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ pager.swipe(Direction.UP, 1.0f)
+ device.wait(Until.findObject(By.desc(ComposeIdle)), 3000)
+ }
+ }
+ }
+
companion object {
private const val PackageName = "androidx.compose.integration.macrobenchmark.target"
private const val Action =
"androidx.compose.integration.macrobenchmark.target.LAZY_PAGER_ACTIVITY"
private const val ContentDescription = "Pager"
+ private const val NextDescription = "Next"
private const val ComposeIdle = "COMPOSE-IDLE"
+ private const val EventRepeatCount = 10
+
+ object BenchmarkType {
+ val Key = "BenchmarkType"
+ val Tab = "EnableTab"
+ val Grid = "Pager of Grids"
+ val List = "Pager of List"
+ val WebView = "Pager of WebViews"
+ val FullScreenImage = "Pager of Full Screen Images"
+ val FixedSizeImage = "Pager of Fixed Size Images"
+ val ListOfPager = "Pager Inside A List"
+ }
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun parameters() = createCompilationParams()
+ fun parameters() = listOf(arrayOf(CompilationMode.Full()))
}
}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/VectorsListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/VectorsListScrollBenchmark.kt
index efac0b9..c62aefd 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/VectorsListScrollBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/VectorsListScrollBenchmark.kt
@@ -48,7 +48,7 @@
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric()),
compilationMode = CompilationMode.Full(),
- iterations = 8,
+ iterations = 5,
setupBlock = {
val intent = Intent()
intent.action = ACTION
@@ -58,7 +58,7 @@
val lazyColumn = device.findObject(By.desc(CONTENT_DESCRIPTION))
// Setting a gesture margin is important otherwise gesture nav is triggered.
lazyColumn.setGestureMargin(device.displayWidth / 5)
- for (i in 1..10) {
+ for (i in 1..8) {
// From center we scroll 2/3 of it which is 1/3 of the screen.
lazyColumn.drag(Point(0, lazyColumn.visibleCenter.y / 3))
device.wait(Until.findObject(By.desc(COMPOSE_IDLE)), 3000)
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
index e8f8f93..0388856 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
@@ -87,10 +87,7 @@
fun indicatorPartiallyClippedWhenPartiallyDisplayed() {
lateinit var state: PullRefreshState
rule.setContent {
- // Pull down by 100 pixels (the actual position delta is half of this because the state
- // applies a multiplier)
- state =
- rememberPullRefreshState(refreshing = false, onRefresh = {}).apply { onPull(100f) }
+ state = rememberPullRefreshState(refreshing = false, onRefresh = {})
Box(
Modifier.fillMaxSize()
.background(Color.White)
@@ -107,6 +104,11 @@
)
}
}
+
+ // Pull down by 100 pixels (the actual position delta is half of this because the state
+ // applies a multiplier)
+ state.onPull(100f)
+
// The indicator should be partially clipped
rule.onNodeWithTag(BoxTag).captureToImage().run {
val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index c69428b..8b5adeb 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -14,6 +14,12 @@
public sealed interface AnimatedPaneScope extends androidx.compose.animation.AnimatedVisibilityScope {
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldPaneScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<Role,ScaffoldValue> androidx.compose.material3.adaptive.layout.PaneScaffoldPaneScope<Role> {
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope androidx.compose.material3.adaptive.layout.PaneScaffoldTransitionScope<Role,ScaffoldValue> {
+ }
+
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class HingePolicy {
field public static final androidx.compose.material3.adaptive.layout.HingePolicy.Companion Companion;
}
@@ -35,8 +41,8 @@
}
public final class ListDetailPaneScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
}
public final class ListDetailPaneScaffoldRole {
@@ -76,7 +82,7 @@
}
public final class PaneExpansionDragHandleKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void PaneExpansionDragHandle(androidx.compose.material3.adaptive.layout.PaneExpansionState state, long color, optional androidx.compose.ui.Modifier modifier);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void PaneExpansionDragHandle(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, androidx.compose.material3.adaptive.layout.PaneExpansionState state, long color, optional androidx.compose.ui.Modifier modifier);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class PaneExpansionState implements androidx.compose.foundation.gestures.DraggableState {
@@ -113,7 +119,25 @@
}
public final class PaneKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> void AnimatedPane(androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T>, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneMotion {
+ method public androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
+ method public androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionData {
+ method public long getCurrentPosition();
+ method public long getCurrentSize();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getMotion();
+ method public long getTargetPosition();
+ method public long getTargetSize();
+ property public final long currentPosition;
+ property public final long currentSize;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion motion;
+ property public final long targetPosition;
+ property public final long targetSize;
}
@androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
@@ -144,18 +168,49 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldMotionScope {
+ method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getDelayedPositionAnimationSpec();
+ method public java.util.List<androidx.compose.material3.adaptive.layout.PaneMotionData> getPaneMotionDataList();
+ method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getPositionAnimationSpec();
+ method public long getScaffoldSize();
+ method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getSizeAnimationSpec();
+ property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> delayedPositionAnimationSpec;
+ property public abstract java.util.List<androidx.compose.material3.adaptive.layout.PaneMotionData> paneMotionDataList;
+ property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> positionAnimationSpec;
+ property public abstract long scaffoldSize;
+ property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> sizeAnimationSpec;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldPaneScope<Role> {
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getPaneMotion();
+ method public Role getPaneRole();
+ property public abstract androidx.compose.material3.adaptive.layout.PaneMotion paneMotion;
+ property public abstract Role paneRole;
+ }
+
public sealed interface PaneScaffoldScope {
method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> {
+ method public float getMotionProgress();
+ method public androidx.compose.animation.core.Transition<ScaffoldValue> getScaffoldStateTransition();
+ property public abstract float motionProgress;
+ property public abstract androidx.compose.animation.core.Transition<ScaffoldValue> scaffoldStateTransition;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldValue<T> {
+ method public operator String get(T role);
+ }
+
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldDefaults {
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy mainPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy supportingPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults INSTANCE;
}
public final class SupportingPaneScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
@@ -174,37 +229,26 @@
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldDestinationItem<T> {
- ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? content);
- method public T? getContent();
+ ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
+ method public T? getContentKey();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getPane();
- property public final T? content;
+ property public final T? contentKey;
property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldPaneScope extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
+ }
+
public enum ThreePaneScaffoldRole {
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope {
- method public androidx.compose.animation.EnterTransition getEnterTransition();
- method public androidx.compose.animation.ExitTransition getExitTransition();
- method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getPositionAnimationSpec();
- method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getRole();
- method public androidx.compose.animation.core.Transition<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> getScaffoldStateTransition();
- method public float getScaffoldStateTransitionFraction();
- method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getSizeAnimationSpec();
- property public abstract androidx.compose.animation.EnterTransition enterTransition;
- property public abstract androidx.compose.animation.ExitTransition exitTransition;
- property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> positionAnimationSpec;
- property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role;
- property public abstract androidx.compose.animation.core.Transition<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> scaffoldStateTransition;
- property public abstract float scaffoldStateTransitionFraction;
- property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> sizeAnimationSpec;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldState {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class ThreePaneScaffoldState {
ctor public ThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
@@ -217,7 +261,7 @@
property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
ctor public ThreePaneScaffoldValue(String primary, String secondary, String tertiary);
method public operator String get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index c69428b..8b5adeb 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -14,6 +14,12 @@
public sealed interface AnimatedPaneScope extends androidx.compose.animation.AnimatedVisibilityScope {
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldPaneScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<Role,ScaffoldValue> androidx.compose.material3.adaptive.layout.PaneScaffoldPaneScope<Role> {
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ExtendedPaneScaffoldScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope androidx.compose.material3.adaptive.layout.PaneScaffoldTransitionScope<Role,ScaffoldValue> {
+ }
+
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class HingePolicy {
field public static final androidx.compose.material3.adaptive.layout.HingePolicy.Companion Companion;
}
@@ -35,8 +41,8 @@
}
public final class ListDetailPaneScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
}
public final class ListDetailPaneScaffoldRole {
@@ -76,7 +82,7 @@
}
public final class PaneExpansionDragHandleKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void PaneExpansionDragHandle(androidx.compose.material3.adaptive.layout.PaneExpansionState state, long color, optional androidx.compose.ui.Modifier modifier);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void PaneExpansionDragHandle(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, androidx.compose.material3.adaptive.layout.PaneExpansionState state, long color, optional androidx.compose.ui.Modifier modifier);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class PaneExpansionState implements androidx.compose.foundation.gestures.DraggableState {
@@ -113,7 +119,25 @@
}
public final class PaneKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <S, T extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<S>> void AnimatedPane(androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<S,T>, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneMotion {
+ method public androidx.compose.animation.EnterTransition getEnterTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
+ method public androidx.compose.animation.ExitTransition getExitTransition(androidx.compose.material3.adaptive.layout.PaneScaffoldMotionScope);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class PaneMotionData {
+ method public long getCurrentPosition();
+ method public long getCurrentSize();
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getMotion();
+ method public long getTargetPosition();
+ method public long getTargetSize();
+ property public final long currentPosition;
+ property public final long currentSize;
+ property public final androidx.compose.material3.adaptive.layout.PaneMotion motion;
+ property public final long targetPosition;
+ property public final long targetSize;
}
@androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
@@ -144,18 +168,49 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldMotionScope {
+ method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getDelayedPositionAnimationSpec();
+ method public java.util.List<androidx.compose.material3.adaptive.layout.PaneMotionData> getPaneMotionDataList();
+ method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getPositionAnimationSpec();
+ method public long getScaffoldSize();
+ method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getSizeAnimationSpec();
+ property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> delayedPositionAnimationSpec;
+ property public abstract java.util.List<androidx.compose.material3.adaptive.layout.PaneMotionData> paneMotionDataList;
+ property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> positionAnimationSpec;
+ property public abstract long scaffoldSize;
+ property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> sizeAnimationSpec;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldPaneScope<Role> {
+ method public androidx.compose.material3.adaptive.layout.PaneMotion getPaneMotion();
+ method public Role getPaneRole();
+ property public abstract androidx.compose.material3.adaptive.layout.PaneMotion paneMotion;
+ property public abstract Role paneRole;
+ }
+
public sealed interface PaneScaffoldScope {
method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue extends androidx.compose.material3.adaptive.layout.PaneScaffoldValue<Role>> {
+ method public float getMotionProgress();
+ method public androidx.compose.animation.core.Transition<ScaffoldValue> getScaffoldStateTransition();
+ property public abstract float motionProgress;
+ property public abstract androidx.compose.animation.core.Transition<ScaffoldValue> scaffoldStateTransition;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldValue<T> {
+ method public operator String get(T role);
+ }
+
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldDefaults {
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy mainPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy supportingPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults INSTANCE;
}
public final class SupportingPaneScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState scaffoldState, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
@@ -174,37 +229,26 @@
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldDestinationItem<T> {
- ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? content);
- method public T? getContent();
+ ctor public ThreePaneScaffoldDestinationItem(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
+ method public T? getContentKey();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getPane();
- property public final T? content;
+ property public final T? contentKey;
property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
}
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldPaneScope extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
+ }
+
public enum ThreePaneScaffoldRole {
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.PaneScaffoldScope androidx.compose.ui.layout.LookaheadScope {
- method public androidx.compose.animation.EnterTransition getEnterTransition();
- method public androidx.compose.animation.ExitTransition getExitTransition();
- method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> getPositionAnimationSpec();
- method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getRole();
- method public androidx.compose.animation.core.Transition<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> getScaffoldStateTransition();
- method public float getScaffoldStateTransitionFraction();
- method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> getSizeAnimationSpec();
- property public abstract androidx.compose.animation.EnterTransition enterTransition;
- property public abstract androidx.compose.animation.ExitTransition exitTransition;
- property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> positionAnimationSpec;
- property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role;
- property public abstract androidx.compose.animation.core.Transition<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> scaffoldStateTransition;
- property public abstract float scaffoldStateTransitionFraction;
- property public abstract androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> sizeAnimationSpec;
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface ThreePaneScaffoldScope extends androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldScope<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole,androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue> {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ThreePaneScaffoldState {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Stable public final class ThreePaneScaffoldState {
ctor public ThreePaneScaffoldState(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue initialScaffoldValue);
method public suspend Object? animateTo(optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>? animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getCurrentState();
@@ -217,7 +261,7 @@
property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue targetState;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider {
+ @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class ThreePaneScaffoldValue implements androidx.compose.material3.adaptive.layout.PaneExpansionStateKeyProvider androidx.compose.material3.adaptive.layout.PaneScaffoldValue<androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole> {
ctor public ThreePaneScaffoldValue(String primary, String secondary, String tertiary);
method public operator String get(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole role);
method public androidx.compose.material3.adaptive.layout.PaneExpansionStateKey getPaneExpansionStateKey();
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index 9400e45..f37fbdb 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -42,14 +42,14 @@
dependencies {
implementation(libs.kotlinStdlib)
api(project(":compose:material3:adaptive:adaptive"))
- api("androidx.compose.animation:animation-core:1.7.0-rc01")
- api("androidx.compose.ui:ui:1.7.0-rc01")
- implementation("androidx.compose.animation:animation:1.7.0-rc01")
+ api("androidx.compose.animation:animation-core:1.7.0")
+ api("androidx.compose.ui:ui:1.7.0")
+ implementation("androidx.compose.animation:animation:1.7.0")
implementation("androidx.compose.foundation:foundation:1.6.5")
implementation("androidx.compose.foundation:foundation-layout:1.6.5")
implementation("androidx.compose.ui:ui-geometry:1.6.5")
implementation("androidx.compose.ui:ui-util:1.6.5")
- implementation("androidx.window:window-core:1.3.0-rc01")
+ implementation("androidx.window:window-core:1.3.0")
}
}
@@ -83,7 +83,7 @@
dependencies {
implementation(project(":compose:material3:material3"))
implementation(project(":compose:test-utils"))
- implementation(project(":window:window-testing"))
+ implementation("androidx.window:window-testing:1.3.0")
implementation(libs.junit)
implementation(libs.testRunner)
implementation(libs.truth)
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
new file mode 100644
index 0000000..5019b19
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionStateTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PaneExpansionStateTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ private val restorationTester = StateRestorationTester(rule)
+
+ @Test
+ fun test_paneExpansionStateSaver() {
+ val mockPaneExpansionStateDataMap =
+ mutableMapOf(
+ Pair(PaneExpansionStateKey.Default, PaneExpansionStateData(1, 0.2F, 3)),
+ Pair(
+ TwoPaneExpansionStateKeyImpl(
+ ThreePaneScaffoldRole.Primary,
+ ThreePaneScaffoldRole.Secondary
+ ),
+ PaneExpansionStateData(4, 0.5F, 6)
+ ),
+ Pair(
+ TwoPaneExpansionStateKeyImpl(
+ ThreePaneScaffoldRole.Secondary,
+ ThreePaneScaffoldRole.Tertiary
+ ),
+ PaneExpansionStateData(7, 0.8F, 9)
+ ),
+ Pair(
+ TwoPaneExpansionStateKeyImpl(
+ ThreePaneScaffoldRole.Tertiary,
+ ThreePaneScaffoldRole.Primary
+ ),
+ PaneExpansionStateData(10, 0.3F, 12)
+ ),
+ )
+
+ var savedMap: MutableMap<PaneExpansionStateKey, PaneExpansionStateData>? = null
+
+ restorationTester.setContent {
+ savedMap =
+ rememberSaveable(saver = PaneExpansionStateSaver()) {
+ mockPaneExpansionStateDataMap
+ }
+ }
+
+ rule.runOnUiThread {
+ // Null it to ensure recomposition happened
+ savedMap = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnUiThread {
+ mockPaneExpansionStateDataMap.entries.forEach {
+ assertThat(savedMap!![it.key]).isEqualTo(it.value)
+ }
+ }
+ }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt
new file mode 100644
index 0000000..4626533
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldMotionScreenshotTest.kt
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import android.os.Build
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+class ThreePaneScaffoldMotionScreenshotTest {
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3_ADAPTIVE)
+
+ @Test
+ fun singlePaneLayout_defaultPaneMotion_progress0() {
+ rule.setContent {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0f, MockTargetScaffoldValueSinglePane) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "singlePaneLayout_defaultPaneMotion_progress0")
+ }
+
+ @Test
+ fun singlePaneLayout_defaultPaneMotion_progress10() {
+ rule.setContent {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.1f, MockTargetScaffoldValueSinglePane) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "singlePaneLayout_defaultPaneMotion_progress10")
+ }
+
+ @Test
+ fun singlePaneLayout_defaultPaneMotion_progress15() {
+ rule.setContent {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.15f, MockTargetScaffoldValueSinglePane) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "singlePaneLayout_defaultPaneMotion_progress15")
+ }
+
+ @Test
+ fun singlePaneLayout_defaultPaneMotion_progress20() {
+ rule.setContent {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.2f, MockTargetScaffoldValueSinglePane) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "singlePaneLayout_defaultPaneMotion_progress20")
+ }
+
+ @Test
+ fun singlePaneLayout_defaultPaneMotion_progress50() {
+ rule.setContent {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.5f, MockTargetScaffoldValueSinglePane) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "singlePaneLayout_defaultPaneMotion_progress50")
+ }
+
+ @Test
+ fun singlePaneLayout_defaultPaneMotion_progress100() {
+ rule.setContent {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueSinglePane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(1f, MockTargetScaffoldValueSinglePane) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "singlePaneLayout_defaultPaneMotion_progress100")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneSwitching_progress0() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0f, MockTargetScaffoldValuePaneSwitching) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneSwitching_progress0")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneSwitching_progress10() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) {
+ scaffoldState.seekTo(0.1f, MockTargetScaffoldValuePaneSwitching)
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneSwitching_progress10")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneSwitching_progress15() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) {
+ scaffoldState.seekTo(0.15f, MockTargetScaffoldValuePaneSwitching)
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneSwitching_progress15")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneSwitching_progress20() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) {
+ scaffoldState.seekTo(0.2f, MockTargetScaffoldValuePaneSwitching)
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneSwitching_progress20")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneSwitching_progress50() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) {
+ scaffoldState.seekTo(0.5f, MockTargetScaffoldValuePaneSwitching)
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneSwitching_progress50")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneSwitching_progress100() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(1f, MockTargetScaffoldValuePaneSwitching) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneSwitching_progress100")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneShifting_progress0() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0f, MockTargetScaffoldValuePaneShifting) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneShifting_progress0")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneShifting_progress10() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.1f, MockTargetScaffoldValuePaneShifting) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneShifting_progress10")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneShifting_progress15() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) {
+ scaffoldState.seekTo(0.15f, MockTargetScaffoldValuePaneShifting)
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneShifting_progress15")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneShifting_progress20() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.2f, MockTargetScaffoldValuePaneShifting) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneShifting_progress20")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneShifting_progress50() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(0.5f, MockTargetScaffoldValuePaneShifting) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneShifting_progress50")
+ }
+
+ @Test
+ fun dualPaneLayout_defaultPaneShifting_progress100() {
+ rule.setContentWithSimulatedSize(simulatedWidth = 1024.dp, simulatedHeight = 800.dp) {
+ val scaffoldState = ThreePaneScaffoldState(MockOriginalScaffoldValueDualPane)
+ SampleThreePaneScaffold(scaffoldState)
+ LaunchedEffect(Unit) { scaffoldState.seekTo(1f, MockTargetScaffoldValuePaneShifting) }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(ThreePaneScaffoldTestTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "dualPaneLayout_defaultPaneShifting_progress100")
+ }
+
+ companion object {
+ val MockOriginalScaffoldValueSinglePane =
+ ThreePaneScaffoldValue(
+ PaneAdaptedValue.Expanded,
+ PaneAdaptedValue.Hidden,
+ PaneAdaptedValue.Hidden,
+ )
+
+ val MockTargetScaffoldValueSinglePane =
+ ThreePaneScaffoldValue(
+ PaneAdaptedValue.Hidden,
+ PaneAdaptedValue.Expanded,
+ PaneAdaptedValue.Hidden,
+ )
+
+ val MockOriginalScaffoldValueDualPane =
+ ThreePaneScaffoldValue(
+ PaneAdaptedValue.Expanded,
+ PaneAdaptedValue.Expanded,
+ PaneAdaptedValue.Hidden,
+ )
+
+ val MockTargetScaffoldValuePaneSwitching =
+ ThreePaneScaffoldValue(
+ PaneAdaptedValue.Expanded,
+ PaneAdaptedValue.Hidden,
+ PaneAdaptedValue.Expanded,
+ )
+
+ val MockTargetScaffoldValuePaneShifting =
+ ThreePaneScaffoldValue(
+ PaneAdaptedValue.Hidden,
+ PaneAdaptedValue.Expanded,
+ PaneAdaptedValue.Expanded,
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+internal fun SampleThreePaneScaffold(
+ scaffoldState: ThreePaneScaffoldState,
+) {
+ ThreePaneScaffold(
+ modifier = Modifier.fillMaxSize().testTag(ThreePaneScaffoldTestTag),
+ scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
+ scaffoldState = scaffoldState,
+ paneOrder = SupportingPaneScaffoldDefaults.PaneOrder,
+ secondaryPane = {
+ AnimatedPane(modifier = Modifier.testTag(tag = "SecondaryPane")) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.secondary
+ ) {}
+ }
+ },
+ tertiaryPane = {
+ AnimatedPane(modifier = Modifier.testTag(tag = "TertiaryPane")) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.tertiary
+ ) {}
+ }
+ }
+ ) {
+ AnimatedPane(modifier = Modifier.testTag(tag = "PrimaryPane")) {
+ Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) {}
+ }
+ }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
index 36723c0..9ce7c99 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
@@ -425,7 +425,8 @@
@Composable
internal fun SampleThreePaneScaffoldWithPaneExpansion(
paneExpansionState: PaneExpansionState,
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
) {
val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
val scaffoldValue =
@@ -445,6 +446,6 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
-internal fun MockDragHandle(state: PaneExpansionState) {
+internal fun ThreePaneScaffoldScope.MockDragHandle(state: PaneExpansionState) {
PaneExpansionDragHandle(state, MaterialTheme.colorScheme.outline)
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index 957275d..00ee35d 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -291,7 +291,8 @@
scaffoldDirective: PaneScaffoldDirective,
scaffoldValue: ThreePaneScaffoldValue,
paneOrder: ThreePaneScaffoldHorizontalOrder,
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
paneExpansionState: PaneExpansionState = PaneExpansionState(),
) {
ThreePaneScaffold(
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/DelayedSpringSpecTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/DelayedSpringSpecTest.kt
new file mode 100644
index 0000000..da4040e
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/DelayedSpringSpecTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VectorizedAnimationSpec
+import androidx.compose.animation.core.spring
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class DelayedSpringSpecTest {
+ @Test
+ fun delayedSpring_identicalWithOriginPlusDelay() {
+ val delayedRatio = 0.5f
+
+ val originalSpec =
+ spring(dampingRatio = 0.7f, stiffness = 500f, visibilityThreshold = 0.1f)
+ .vectorize(Float.VectorConverter)
+
+ val delayedSpec =
+ DelayedSpringSpec(
+ dampingRatio = 0.7f,
+ stiffness = 500f,
+ visibilityThreshold = 0.1f,
+ delayedRatio = delayedRatio,
+ )
+ .vectorize(Float.VectorConverter)
+
+ val originalDurationNanos = originalSpec.getDurationNanos()
+ val delayedNanos = (originalDurationNanos * delayedRatio).toLong()
+
+ fun assertValuesAt(playTimeNanos: Long) {
+ assertValuesAreEqual(
+ originalSpec.getValueFromNanos(playTimeNanos),
+ delayedSpec.getValueFromNanos(playTimeNanos + delayedNanos)
+ )
+ }
+
+ assertValuesAt(0)
+ assertValuesAt((originalDurationNanos * 0.2).toLong())
+ assertValuesAt((originalDurationNanos * 0.35).toLong())
+ assertValuesAt((originalDurationNanos * 0.6).toLong())
+ assertValuesAt((originalDurationNanos * 0.85).toLong())
+ assertValuesAt(originalDurationNanos)
+ }
+
+ private fun VectorizedAnimationSpec<AnimationVector1D>.getDurationNanos(): Long =
+ getDurationNanos(InitialValue, TargetValue, InitialVelocity)
+
+ private fun VectorizedAnimationSpec<AnimationVector1D>.getValueFromNanos(
+ playTimeNanos: Long
+ ): Float = getValueFromNanos(playTimeNanos, InitialValue, TargetValue, InitialVelocity).value
+
+ private fun assertValuesAreEqual(value1: Float, value2: Float) {
+ assertThat(value1).isWithin(Tolerance).of(value2)
+ }
+
+ companion object {
+ private val InitialValue = AnimationVector1D(0f)
+ private val TargetValue = AnimationVector1D(1f)
+ private val InitialVelocity = AnimationVector1D(0f)
+ private const val Tolerance = 0.001f
+ }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
index 35d588d..10d5ba2 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMotionTest.kt
@@ -18,10 +18,6 @@
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.snap
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInHorizontally
@@ -98,93 +94,93 @@
@Test
fun slideInFromLeftOffset_noEnterFromLeftPane_equalsZero() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromRight, EnterFromRight, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(EnterFromRight, EnterFromRight, EnterFromRight)
assertThat(mockPaneScaffoldMotionScope.slideInFromLeftOffset).isEqualTo(0)
}
@Test
fun slideInFromLeftOffset_withEnterFromLeftPane_useTheRightestEdge() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromLeft, EnterFromLeft, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(EnterFromLeft, EnterFromLeft, EnterFromRight)
assertThat(mockPaneScaffoldMotionScope.slideInFromLeftOffset)
.isEqualTo(
- -mockPaneScaffoldMotionScope.targetPanePositions[1].x -
- mockPaneScaffoldMotionScope.targetPaneSizes[1].width
+ -mockPaneScaffoldMotionScope.paneMotionDataList[1].targetPosition.x -
+ mockPaneScaffoldMotionScope.paneMotionDataList[1].targetSize.width
)
}
@Test
fun slideInFromLeftOffset_withEnterFromLeftDelayedPane_useTheRightestEdge() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromLeft, EnterFromLeftDelayed, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(
+ EnterFromLeft,
+ EnterFromLeftDelayed,
+ EnterFromRight
+ )
assertThat(mockPaneScaffoldMotionScope.slideInFromLeftOffset)
.isEqualTo(
- -mockPaneScaffoldMotionScope.targetPanePositions[1].x -
- mockPaneScaffoldMotionScope.targetPaneSizes[1].width
+ -mockPaneScaffoldMotionScope.paneMotionDataList[1].targetPosition.x -
+ mockPaneScaffoldMotionScope.paneMotionDataList[1].targetSize.width
)
}
@Test
fun slideInFromRightOffset_noEnterFromRightPane_equalsZero() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromLeft, EnterFromLeft, EnterFromLeft)
+ mockPaneScaffoldMotionScope.updateMotions(EnterFromLeft, EnterFromLeft, EnterFromLeft)
assertThat(mockPaneScaffoldMotionScope.slideInFromRightOffset).isEqualTo(0)
}
@Test
fun slideInFromRightOffset_withEnterFromRightPane_useTheLeftestEdge() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromLeft, EnterFromRight, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(EnterFromLeft, EnterFromRight, EnterFromRight)
assertThat(mockPaneScaffoldMotionScope.slideInFromRightOffset)
.isEqualTo(
mockPaneScaffoldMotionScope.scaffoldSize.width -
- mockPaneScaffoldMotionScope.targetPanePositions[1].x
+ mockPaneScaffoldMotionScope.paneMotionDataList[1].targetPosition.x
)
}
@Test
fun slideInFromRightOffset_withEnterFromRightDelayedPane_useTheLeftestEdge() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromLeft, EnterFromRightDelayed, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(
+ EnterFromLeft,
+ EnterFromRightDelayed,
+ EnterFromRight
+ )
assertThat(mockPaneScaffoldMotionScope.slideInFromRightOffset)
.isEqualTo(
mockPaneScaffoldMotionScope.scaffoldSize.width -
- mockPaneScaffoldMotionScope.targetPanePositions[1].x
+ mockPaneScaffoldMotionScope.paneMotionDataList[1].targetPosition.x
)
}
@Test
fun slideOutToLeftOffset_noExitToLeftPane_equalsZero() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromRight, EnterFromRight, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(EnterFromRight, EnterFromRight, EnterFromRight)
assertThat(mockPaneScaffoldMotionScope.slideOutToLeftOffset).isEqualTo(0)
}
@Test
fun slideOutToLeftOffset_withExitToLeftPane_useTheRightestEdge() {
- mockPaneScaffoldMotionScope.paneMotions = listOf(ExitToLeft, ExitToLeft, ExitToRight)
+ mockPaneScaffoldMotionScope.updateMotions(ExitToLeft, ExitToLeft, ExitToRight)
assertThat(mockPaneScaffoldMotionScope.slideOutToLeftOffset)
.isEqualTo(
- -mockPaneScaffoldMotionScope.currentPanePositions[1].x -
- mockPaneScaffoldMotionScope.currentPaneSizes[1].width
+ -mockPaneScaffoldMotionScope.paneMotionDataList[1].currentPosition.x -
+ mockPaneScaffoldMotionScope.paneMotionDataList[1].currentSize.width
)
}
@Test
fun slideOutToRightOffset_noExitToRightPane_equalsZero() {
- mockPaneScaffoldMotionScope.paneMotions =
- listOf(EnterFromRight, EnterFromRight, EnterFromRight)
+ mockPaneScaffoldMotionScope.updateMotions(EnterFromRight, EnterFromRight, EnterFromRight)
assertThat(mockPaneScaffoldMotionScope.slideOutToRightOffset).isEqualTo(0)
}
@Test
fun slideOutToRightOffset_withExitToRightPane_useTheLeftestEdge() {
- mockPaneScaffoldMotionScope.paneMotions = listOf(ExitToLeft, ExitToRight, ExitToRight)
+ mockPaneScaffoldMotionScope.updateMotions(ExitToLeft, ExitToRight, ExitToRight)
assertThat(mockPaneScaffoldMotionScope.slideOutToRightOffset)
.isEqualTo(
mockPaneScaffoldMotionScope.scaffoldSize.width -
- mockPaneScaffoldMotionScope.currentPanePositions[1].x
+ mockPaneScaffoldMotionScope.paneMotionDataList[1].currentPosition.x
)
}
}
@@ -308,25 +304,48 @@
@Suppress("PrimitiveInCollection") // No way to get underlying Long of IntSize or IntOffset
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val mockPaneScaffoldMotionScope =
- object : PaneScaffoldMotionScope {
- override val positionAnimationSpec: FiniteAnimationSpec<IntOffset> = tween()
- override val sizeAnimationSpec: FiniteAnimationSpec<IntSize> = spring()
- override val delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset> = snap()
- override val scaffoldSize: IntSize = IntSize(1000, 1000)
- override var currentPaneSizes: List<IntSize> =
- listOf(IntSize(1, 2), IntSize(3, 4), IntSize(5, 6))
- override var currentPanePositions: List<IntOffset> =
- listOf(IntOffset(3, 4), IntOffset(5, 6), IntOffset(7, 8))
- override var targetPaneSizes: List<IntSize> =
- listOf(IntSize(3, 4), IntSize(5, 6), IntSize(7, 8))
- override var targetPanePositions: List<IntOffset> =
- listOf(IntOffset(5, 6), IntOffset(7, 8), IntOffset(9, 0))
- override var paneMotions: List<PaneMotion> =
- listOf(ExitToLeft, EnterFromRight, EnterFromRight)
- override val motionProgress = 0.5F
+ ThreePaneScaffoldMotionScopeImpl().apply {
+ updateThreePaneMotion(
+ ThreePaneMotion(ExitToLeft, EnterFromRight, EnterFromRight),
+ MockThreePaneOrder
+ )
+ scaffoldSize = IntSize(1000, 1000)
+ paneMotionDataList[0].apply {
+ motion = ExitToLeft
+ currentSize = IntSize(1, 2)
+ currentPosition = IntOffset(3, 4)
+ targetSize = IntSize(3, 4)
+ targetPosition = IntOffset(5, 6)
+ }
+ paneMotionDataList[1].apply {
+ motion = ExitToLeft
+ currentSize = IntSize(3, 4)
+ currentPosition = IntOffset(5, 6)
+ targetSize = IntSize(5, 6)
+ targetPosition = IntOffset(7, 8)
+ }
+ paneMotionDataList[2].apply {
+ motion = ExitToLeft
+ currentSize = IntSize(5, 6)
+ currentPosition = IntOffset(7, 8)
+ targetSize = IntSize(7, 8)
+ targetPosition = IntOffset(9, 0)
+ }
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun ThreePaneScaffoldMotionScopeImpl.updateMotions(
+ primaryPaneMotion: PaneMotion,
+ secondaryPaneMotion: PaneMotion,
+ tertiaryPaneMotion: PaneMotion
+) {
+ updateThreePaneMotion(
+ ThreePaneMotion(primaryPaneMotion, secondaryPaneMotion, tertiaryPaneMotion),
+ MockThreePaneOrder
+ )
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val mockEnterFromLeftTransition =
slideInHorizontally(mockPaneScaffoldMotionScope.positionAnimationSpec) {
mockPaneScaffoldMotionScope.slideInFromLeftOffset
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
index ea19285..2718a66 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
@@ -32,6 +32,7 @@
@RunWith(JUnit4::class)
class PaneScaffoldDirectiveTest {
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_compactWidth() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -46,6 +47,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_mediumWidth() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -60,6 +62,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_expandedWidth() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -74,6 +77,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_tabletop() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -88,6 +92,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_compactWidth() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -102,6 +107,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_mediumWidth() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -116,6 +122,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_expandedWidth() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -130,6 +137,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_tabletop() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -144,6 +152,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_alwaysAvoidHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -158,6 +167,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_avoidOccludingHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -172,6 +182,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_avoidSeparatingHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -186,6 +197,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateStandardPaneScaffoldDirective_neverAvoidHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirective(
@@ -200,6 +212,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_alwaysAvoidHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -214,6 +227,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_avoidOccludingHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -228,6 +242,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_avoidSeparatingHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
@@ -242,6 +257,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun test_calculateDensePaneScaffoldDirective_neverAvoidHinge() {
val scaffoldDirective =
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
deleted file mode 100644
index 39bfd7d..0000000
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3.adaptive.layout
-
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.VectorizedAnimationSpec
-import androidx.compose.animation.core.spring
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-@RunWith(JUnit4::class)
-class ThreePaneMotionTest {
- @Test
- fun noPane_noMotion() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(ThreePaneMotion.NoMotion)
- }
-
- @Test
- fun singlePane_firstToSecond_movesLeft() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
- }
-
- @Test
- fun singlePane_firstToThird_movesLeft() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
- }
-
- @Test
- fun singlePane_secondToThird_movesLeft() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
- }
-
- @Test
- fun singlePane_secondToFirst_movesRight() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
- }
-
- @Test
- fun singlePane_thirdToFirst_movesRight() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
- }
-
- @Test
- fun singlePane_thirdToSecond_movesRight() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
- }
-
- @Test
- fun dualPane_hidesFirstShowsThird_movesLeft() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToLeftMotion(SpacerSize))
- }
-
- @Test
- fun dualPane_hidesThirdShowsFirst_movesRight() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(MovePanesToRightMotion(SpacerSize))
- }
-
- @Test
- fun dualPane_hidesSecondShowsThird_switchRightTwoPanes() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(SwitchRightTwoPanesMotion(SpacerSize))
- }
-
- @Test
- fun dualPane_hidesThirdShowsSecond_switchRightTwoPanes() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(SwitchRightTwoPanesMotion(SpacerSize))
- }
-
- @Test
- fun dualPane_hidesFirstShowsSecond_switchLeftTwoPanes() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(SwitchLeftTwoPanesMotion(SpacerSize))
- }
-
- @Test
- fun dualPane_hidesSecondShowsFirst_switchLeftTwoPanes() {
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(SwitchLeftTwoPanesMotion(SpacerSize))
- }
-
- @Test
- fun changeNumberOfPanes_noMotion() {
- // TODO(conradchen): Update this when we support motions in this case
- val motions =
- calculateThreePaneMotion(
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Expanded
- ),
- ThreePaneScaffoldValue(
- PaneAdaptedValue.Expanded,
- PaneAdaptedValue.Hidden,
- PaneAdaptedValue.Hidden
- ),
- PaneOrder,
- SpacerSize
- )
- assertThat(motions).isEqualTo(ThreePaneMotion.NoMotion)
- }
-
- @Test
- fun delayedSpring_identicalWithOriginPlusDelay() {
- val delayedRatio = 0.5f
-
- val originalSpec =
- spring(dampingRatio = 0.7f, stiffness = 500f, visibilityThreshold = 0.1f)
- .vectorize(Float.VectorConverter)
-
- val delayedSpec =
- DelayedSpringSpec(
- dampingRatio = 0.7f,
- stiffness = 500f,
- visibilityThreshold = 0.1f,
- delayedRatio = delayedRatio,
- )
- .vectorize(Float.VectorConverter)
-
- val originalDurationNanos = originalSpec.getDurationNanos()
- val delayedNanos = (originalDurationNanos * delayedRatio).toLong()
-
- fun assertValuesAt(playTimeNanos: Long) {
- assertValuesAreEqual(
- originalSpec.getValueFromNanos(playTimeNanos),
- delayedSpec.getValueFromNanos(playTimeNanos + delayedNanos)
- )
- }
-
- assertValuesAt(0)
- assertValuesAt((originalDurationNanos * 0.2).toLong())
- assertValuesAt((originalDurationNanos * 0.35).toLong())
- assertValuesAt((originalDurationNanos * 0.6).toLong())
- assertValuesAt((originalDurationNanos * 0.85).toLong())
- assertValuesAt(originalDurationNanos)
- }
-
- private fun VectorizedAnimationSpec<AnimationVector1D>.getDurationNanos(): Long =
- getDurationNanos(InitialValue, TargetValue, InitialVelocity)
-
- private fun VectorizedAnimationSpec<AnimationVector1D>.getValueFromNanos(
- playTimeNanos: Long
- ): Float = getValueFromNanos(playTimeNanos, InitialValue, TargetValue, InitialVelocity).value
-
- private fun assertValuesAreEqual(value1: Float, value2: Float) {
- assertThat(value1 - value2).isWithin(Tolerance)
- }
-
- companion object {
- private val InitialValue = AnimationVector1D(0f)
- private val TargetValue = AnimationVector1D(1f)
- private val InitialVelocity = AnimationVector1D(0f)
- private const val Tolerance = 0.001f
- }
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal val PaneOrder = SupportingPaneScaffoldDefaults.PaneOrder
-internal const val SpacerSize = 123
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt
index 2b3fc57..fa35e39 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt
@@ -21,7 +21,6 @@
import androidx.compose.animation.core.TargetBasedAnimation
import androidx.compose.animation.core.VectorConverter
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
@@ -35,7 +34,6 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
-import kotlin.math.roundToInt
internal fun Modifier.animateBounds(
animateFraction: () -> Float,
@@ -44,24 +42,22 @@
lookaheadScope: LookaheadScope,
enabled: Boolean
) =
- if (enabled) {
- this.then(
- AnimateBoundsElement(
- animateFraction,
- sizeAnimationSpec,
- positionAnimationSpec,
- lookaheadScope
- )
+ this.then(
+ AnimateBoundsElement(
+ animateFraction,
+ sizeAnimationSpec,
+ positionAnimationSpec,
+ lookaheadScope,
+ enabled,
)
- } else {
- this
- }
+ )
private data class AnimateBoundsElement(
private val animateFraction: () -> Float,
private val sizeAnimationSpec: FiniteAnimationSpec<IntSize>,
private val positionAnimationSpec: FiniteAnimationSpec<IntOffset>,
- private val lookaheadScope: LookaheadScope
+ private val lookaheadScope: LookaheadScope,
+ private val enabled: Boolean
) : ModifierNodeElement<AnimateBoundsNode>() {
private val inspectorInfo = debugInspectorInfo {
name = "animateBounds"
@@ -69,6 +65,7 @@
properties["sizeAnimationSpec"] = sizeAnimationSpec
properties["positionAnimationSpec"] = positionAnimationSpec
properties["lookaheadScope"] = lookaheadScope
+ properties["enabled"] = enabled
}
override fun create(): AnimateBoundsNode {
@@ -77,6 +74,7 @@
sizeAnimationSpec,
positionAnimationSpec,
lookaheadScope,
+ enabled,
)
}
@@ -85,6 +83,7 @@
node.sizeAnimationSpec = sizeAnimationSpec
node.positionAnimationSpec = positionAnimationSpec
node.lookaheadScope = lookaheadScope
+ node.enabled = enabled
}
override fun InspectorInfo.inspectableProperties() {
@@ -97,6 +96,7 @@
sizeAnimationSpec: FiniteAnimationSpec<IntSize>,
positionAnimationSpec: FiniteAnimationSpec<IntOffset>,
var lookaheadScope: LookaheadScope,
+ var enabled: Boolean
) : ApproachLayoutModifierNode, Modifier.Node() {
val sizeTracker = SizeTracker(sizeAnimationSpec)
val positionTracker = PositionTracker(positionAnimationSpec)
@@ -113,39 +113,38 @@
}
get() = positionTracker.animationSpec
- override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean =
- animateFraction() != 1f
+ // If animateBounds is not enabled, we need to do approach measure at least once so the size
+ // tracker and the position tracker will be kept updated.
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ sizeTracker.updateTargetSize(lookaheadSize)
+ return enabled && animateFraction() != 1f
+ }
+ // If animateBounds is not enabled, we need to do approach measure at least once so the size
+ // tracker and the position tracker will be kept updated.
override fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
- ) = animateFraction() != 1f
+ ): Boolean {
+ positionTracker.updateTargetOffset(lookaheadOffset(lookaheadScope))
+ return enabled && animateFraction() != 1f
+ }
override fun ApproachMeasureScope.approachMeasure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
- // When layout changes, the lookahead pass will calculate a new final size for the
- // child modifier. This lookahead size can be used to animate the size
- // change, such that the animation starts from the current size and gradually
- // change towards `lookaheadSize`.
- sizeTracker.updateTargetSize(lookaheadSize)
+ // Use the current animating fraction to get the approach size and offset of the current
+ // animating layut toward the target size and offset updated in measure().
val (width, height) = sizeTracker.updateAndGetCurrentSize(animateFraction())
- // Creates a fixed set of constraints using the animated size
val animatedConstraints = Constraints.fixed(width, height)
- // Measure child/children with animated constraints.
val placeable = measurable.measure(animatedConstraints)
return layout(placeable.width, placeable.height) {
coordinates?.let {
- positionTracker.updateTargetOffset(
- with(lookaheadScope) {
- lookaheadScopeCoordinates.localLookaheadPositionOf(it).toIntOffset()
- }
- )
placeable.place(
- with(lookaheadScope) {
- positionTracker.updateAndGetCurrentOffset(animateFraction()) -
- lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).toIntOffset()
- }
+ convertOffsetToLookaheadCoordinates(
+ positionTracker.updateAndGetCurrentOffset(animateFraction()),
+ lookaheadScope
+ )
)
}
}
@@ -214,7 +213,3 @@
return currentOffset!!
}
}
-
-private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
-
-private val InvalidIntSize = IntSize(-1, -1)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateModifierUtils.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateModifierUtils.kt
new file mode 100644
index 0000000..97d3698
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateModifierUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+
+// This file is a collection of shared functions and vals among animation-relevant modifiers.
+
+internal val InvalidIntSize = IntSize(-1, -1)
+
+internal val InvalidOffset = IntOffset(Int.MIN_VALUE, Int.MIN_VALUE)
+
+internal fun Placeable.PlacementScope.lookaheadOffset(lookaheadScope: LookaheadScope): IntOffset =
+ with(lookaheadScope) {
+ lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates!!).round()
+ }
+
+internal fun Placeable.PlacementScope.convertOffsetToLookaheadCoordinates(
+ offset: IntOffset,
+ lookaheadScope: LookaheadScope
+): IntOffset =
+ with(lookaheadScope) {
+ offset - lookaheadScopeCoordinates.localPositionOf(coordinates!!, Offset.Zero).round()
+ }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateWithFadingModifier.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateWithFadingModifier.kt
new file mode 100644
index 0000000..0bbaaab
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateWithFadingModifier.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.TargetBasedAnimation
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.tween
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ApproachLayoutModifierNode
+import androidx.compose.ui.layout.ApproachMeasureScope
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import kotlin.math.abs
+
+internal fun Modifier.animateWithFading(
+ enabled: Boolean,
+ animateFraction: () -> Float,
+ lookaheadScope: LookaheadScope,
+ fadingAnimationSpec: FiniteAnimationSpec<Float> = tween()
+) =
+ this.then(
+ AnimateWithFadingElement(animateFraction, lookaheadScope, enabled, fadingAnimationSpec)
+ )
+
+private data class AnimateWithFadingElement(
+ val animateFraction: () -> Float,
+ val lookaheadScope: LookaheadScope,
+ val enabled: Boolean,
+ val fadingAnimationSpec: FiniteAnimationSpec<Float>
+) : ModifierNodeElement<AnimateWithFadingNode>() {
+ private val inspectorInfo = debugInspectorInfo {
+ name = "animateWithFading"
+ properties["animateFraction"] = animateFraction
+ properties["lookaheadScope"] = lookaheadScope
+ properties["enabled"] = enabled
+ properties["fadingAnimationSpec"] = fadingAnimationSpec
+ }
+
+ override fun create(): AnimateWithFadingNode {
+ return AnimateWithFadingNode(animateFraction, lookaheadScope, enabled, fadingAnimationSpec)
+ }
+
+ override fun update(node: AnimateWithFadingNode) {
+ node.animateFraction = animateFraction
+ node.lookaheadScope = lookaheadScope
+ node.enabled = enabled
+ node.fadingAnimationSpec = fadingAnimationSpec
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ inspectorInfo()
+ }
+}
+
+private class AnimateWithFadingNode(
+ var animateFraction: () -> Float,
+ var lookaheadScope: LookaheadScope,
+ var enabled: Boolean,
+ fadingAnimationSpec: FiniteAnimationSpec<Float>
+) : ApproachLayoutModifierNode, Modifier.Node() {
+ private var originalOffset: IntOffset = InvalidOffset
+ private var targetOffset: IntOffset = InvalidOffset
+
+ var fadingAnimationSpec = fadingAnimationSpec
+ set(value) {
+ animation = value.createAnimation()
+ field = value
+ }
+
+ private var animation = fadingAnimationSpec.createAnimation()
+
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false
+
+ override fun Placeable.PlacementScope.isPlacementApproachInProgress(
+ lookaheadCoordinates: LayoutCoordinates
+ ): Boolean {
+ updateTargetOffset(lookaheadOffset(lookaheadScope))
+ return enabled &&
+ originalOffset != InvalidOffset &&
+ originalOffset != targetOffset &&
+ animateFraction() != 1f
+ }
+
+ override fun ApproachMeasureScope.approachMeasure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val currentAnimatedValue = animation.getValue(animateFraction())
+ return measurable.measure(constraints).run {
+ layout(width, height) {
+ coordinates?.let {
+ placeWithLayer(
+ if (currentAnimatedValue > 0f) {
+ IntOffset.Zero
+ } else {
+ originalOffset - targetOffset
+ },
+ layerBlock = { alpha = abs(currentAnimatedValue) }
+ )
+ }
+ }
+ }
+ }
+
+ fun updateTargetOffset(newOffset: IntOffset) {
+ if (targetOffset == newOffset) {
+ return
+ }
+ originalOffset = targetOffset
+ targetOffset = newOffset
+ }
+
+ private fun FiniteAnimationSpec<Float>.createAnimation() =
+ TargetBasedAnimation(this, Float.VectorConverter, -1f, 1f)
+}
+
+private fun <T, V : AnimationVector> TargetBasedAnimation<T, V>.getValue(progress: Float) =
+ getValueFromNanos((durationNanos * progress).toLong())
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
index 0403a04..c25d25c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
@@ -47,12 +47,10 @@
* @param extraPane the extra pane of the scaffold, which is supposed to hold any supplementary info
* besides the list and the detail panes, for example, a task list or a mini-calendar view of a
* mail app. See [ListDetailPaneScaffoldRole.Extra].
- * @param paneExpansionDragHandle the pane expansion drag handle to let users be able to drag to
- * change pane expansion state. Note that by default this argument will be `null`, and there won't
- * be a drag handle rendered and users won't be able to drag to change the pane split. You can
- * provide a [PaneExpansionDragHandle] here as our sample suggests. On the other hand, even if
- * there's no drag handle, you can still modify [paneExpansionState] directly to apply pane
- * expansion.
+ * @param paneExpansionDragHandle provide a custom pane expansion drag handle to allow users to
+ * resize panes and change the pane expansion state by dragging. This is `null` by default, which
+ * renders no drag handle. Even there's no drag handle, you can still change pane size directly
+ * via modifying [paneExpansionState].
* @param paneExpansionState the state object of pane expansion.
*/
@ExperimentalMaterial3AdaptiveApi
@@ -60,11 +58,12 @@
fun ListDetailPaneScaffold(
directive: PaneScaffoldDirective,
value: ThreePaneScaffoldValue,
- listPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- detailPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
modifier: Modifier = Modifier,
- extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
+ extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(value),
) {
ThreePaneScaffold(
@@ -101,16 +100,24 @@
* @param extraPane the extra pane of the scaffold, which is supposed to hold any supplementary info
* besides the list and the detail panes, for example, a task list or a mini-calendar view of a
* mail app. See [ListDetailPaneScaffoldRole.Extra].
+ * @param paneExpansionDragHandle provide a custom pane expansion drag handle to allow users to
+ * resize panes and change the pane expansion state by dragging. This is `null` by default, which
+ * renders no drag handle. Even there's no drag handle, you can still change pane size directly
+ * via modifying [paneExpansionState].
+ * @param paneExpansionState the state object of pane expansion.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun ListDetailPaneScaffold(
directive: PaneScaffoldDirective,
scaffoldState: ThreePaneScaffoldState,
- listPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- detailPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
modifier: Modifier = Modifier,
- extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
+ extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
+ paneExpansionState: PaneExpansionState = rememberPaneExpansionState(scaffoldState.targetState),
) {
ThreePaneScaffold(
modifier = modifier.fillMaxSize(),
@@ -119,6 +126,8 @@
paneOrder = ListDetailPaneScaffoldDefaults.PaneOrder,
secondaryPane = listPane,
tertiaryPane = extraPane,
+ paneExpansionDragHandle = paneExpansionDragHandle,
+ paneExpansionState = paneExpansionState,
primaryPane = detailPane
)
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
index ea17ef7..df65278 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
@@ -39,29 +39,27 @@
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
-fun ThreePaneScaffoldScope.AnimatedPane(
+fun <S, T : PaneScaffoldValue<S>> ExtendedPaneScaffoldPaneScope<S, T>.AnimatedPane(
modifier: Modifier = Modifier,
content: (@Composable AnimatedPaneScope.() -> Unit),
) {
- val keepShowing =
- scaffoldStateTransition.currentState[role] != PaneAdaptedValue.Hidden &&
- scaffoldStateTransition.targetState[role] != PaneAdaptedValue.Hidden
- val animateFraction = { scaffoldStateTransitionFraction }
+ val animatingBounds = paneMotion == DefaultPaneMotion.AnimateBounds
+ val motionProgress = { motionProgress }
scaffoldStateTransition.AnimatedVisibility(
- visible = { value: ThreePaneScaffoldValue -> value[role] != PaneAdaptedValue.Hidden },
+ visible = { value: T -> value[paneRole] != PaneAdaptedValue.Hidden },
modifier =
modifier
.animatedPane()
.animateBounds(
- animateFraction = animateFraction,
- positionAnimationSpec = positionAnimationSpec,
- sizeAnimationSpec = sizeAnimationSpec,
- lookaheadScope = this,
- enabled = keepShowing
+ motionProgress,
+ sizeAnimationSpec,
+ positionAnimationSpec,
+ this,
+ animatingBounds
)
- .then(if (keepShowing) Modifier else Modifier.clipToBounds()),
- enter = enterTransition,
- exit = exitTransition
+ .then(if (animatingBounds) Modifier else Modifier.clipToBounds()),
+ enter = with(paneMotion) { enterTransition },
+ exit = with(paneMotion) { exitTransition }
) {
AnimatedPaneScopeImpl(this).content()
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt
index 8398174..0409b5c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt
@@ -38,14 +38,22 @@
@ExperimentalMaterial3AdaptiveApi
@Composable
// TODO(b/327637983): Implement this as a customizable component as a Material3 component.
-fun PaneExpansionDragHandle(
+fun ThreePaneScaffoldScope.PaneExpansionDragHandle(
state: PaneExpansionState,
color: Color,
modifier: Modifier = Modifier,
) {
- // TODO (conradchen): support drag handle motion during scaffold and expansion state change
+ val animationProgress = { motionProgress }
Box(
- modifier = modifier.paneExpansionDragHandle(state).size(24.dp, 48.dp),
+ modifier =
+ modifier
+ .paneExpansionDragHandle(state)
+ .size(24.dp, 48.dp)
+ .animateWithFading(
+ enabled = true,
+ animateFraction = animationProgress,
+ lookaheadScope = this
+ ),
contentAlignment = Alignment.Center
) {
Box(
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
index df95da6..ca7291a 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
@@ -36,9 +36,13 @@
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlinx.coroutines.coroutineScope
@@ -115,8 +119,7 @@
key: PaneExpansionStateKey = PaneExpansionStateKey.Default,
anchors: List<PaneExpansionAnchor> = emptyList()
): PaneExpansionState {
- // TODO(conradchen): Implement this as saveables
- val dataMap = remember { mutableStateMapOf<PaneExpansionStateKey, PaneExpansionStateData>() }
+ val dataMap = rememberSaveable(saver = PaneExpansionStateSaver()) { mutableStateMapOf() }
val expansionState = remember {
val defaultData = PaneExpansionStateData()
dataMap[PaneExpansionStateKey.Default] = defaultData
@@ -130,7 +133,7 @@
/**
* This class manages the pane expansion state for pane scaffolds. By providing and modifying an
- * instance of this class, you can specify the expanded panes' expansion width or percentage when
+ * instance of this class, you can specify the expanded panes' expansion width or proportion when
* pane scaffold is displaying a dual-pane layout.
*
* This class also serves as the [DraggableState] of pane expansion handle. When a handle
@@ -331,10 +334,31 @@
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal class PaneExpansionStateData {
- var firstPaneWidthState by mutableIntStateOf(Unspecified)
- var firstPaneProportionState by mutableFloatStateOf(Float.NaN)
- var currentDraggingOffsetState by mutableIntStateOf(Unspecified)
+@Stable
+internal class PaneExpansionStateData(
+ firstPaneWidth: Int = Unspecified,
+ firstPaneProportion: Float = Float.NaN,
+ currentDraggingOffset: Int = Unspecified
+) {
+ var firstPaneWidthState by mutableIntStateOf(firstPaneWidth)
+ var firstPaneProportionState by mutableFloatStateOf(firstPaneProportion)
+ var currentDraggingOffsetState by mutableIntStateOf(currentDraggingOffset)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PaneExpansionStateData) return false
+ if (firstPaneWidthState != other.firstPaneWidthState) return false
+ if (firstPaneProportionState != other.firstPaneProportionState) return false
+ if (currentDraggingOffsetState != other.currentDraggingOffsetState) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = firstPaneWidthState
+ result = 31 * result + firstPaneProportionState.hashCode()
+ result = 31 * result + currentDraggingOffsetState
+ return result
+ }
}
/**
@@ -393,6 +417,75 @@
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@VisibleForTesting
+internal fun PaneExpansionStateSaver():
+ Saver<MutableMap<PaneExpansionStateKey, PaneExpansionStateData>, *> =
+ listSaver<MutableMap<PaneExpansionStateKey, PaneExpansionStateData>, Any>(
+ save = {
+ val dataSaver = PaneExpansionStateDataSaver()
+ buildList { it.forEach { entry -> add(with(dataSaver) { save(entry) }!!) } }
+ },
+ restore = {
+ val dataSaver = PaneExpansionStateDataSaver()
+ val map = mutableMapOf<PaneExpansionStateKey, PaneExpansionStateData>()
+ it.fastForEach { with(dataSaver) { restore(it) }!!.apply { map[key] = value } }
+ map
+ }
+ )
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun PaneExpansionStateDataSaver():
+ Saver<Map.Entry<PaneExpansionStateKey, PaneExpansionStateData>, Any> =
+ listSaver(
+ save = {
+ val keyType = it.key.type
+ listOf(
+ it.key.type,
+ if (keyType == DefaultPaneExpansionStateKey) {
+ null
+ } else {
+ with(TwoPaneExpansionStateKeyImpl.saver()) {
+ save(it.key as TwoPaneExpansionStateKeyImpl)
+ }
+ },
+ it.value.firstPaneWidthState,
+ it.value.firstPaneProportionState,
+ it.value.currentDraggingOffsetState
+ )
+ },
+ restore = {
+ val keyType = it[0] as Int
+ val key =
+ if (keyType == DefaultPaneExpansionStateKey || it[1] == null) {
+ PaneExpansionStateKey.Default
+ } else {
+ with(TwoPaneExpansionStateKeyImpl.saver()) { restore(it[1]!!) }
+ }
+ object : Map.Entry<PaneExpansionStateKey, PaneExpansionStateData> {
+ override val key: PaneExpansionStateKey = key!!
+ override val value: PaneExpansionStateData =
+ PaneExpansionStateData(
+ firstPaneWidth = it[2] as Int,
+ firstPaneProportion = it[3] as Float,
+ currentDraggingOffset = it[4] as Int
+ )
+ }
+ }
+ )
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private val PaneExpansionStateKey.type
+ get() =
+ if (this is TwoPaneExpansionStateKeyImpl) {
+ TwoPaneExpansionStateKey
+ } else {
+ DefaultPaneExpansionStateKey
+ }
+
+private const val DefaultPaneExpansionStateKey = 0
+private const val TwoPaneExpansionStateKey = 1
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun List<PaneExpansionAnchor>.toPositions(
maxExpansionWidth: Int,
density: Density
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
index 597862e..58798db 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMotion.kt
@@ -16,7 +16,6 @@
package androidx.compose.material3.adaptive.layout
-import androidx.annotation.VisibleForTesting
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.FiniteAnimationSpec
@@ -28,35 +27,102 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastForEachIndexed
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
import kotlin.math.max
import kotlin.math.min
+/**
+ * Scope for performing pane motions within a pane scaffold. It provides the spec and necessary info
+ * to decide a pane's [EnterTransition] and [ExitTransition], as well as how bounds morphing will be
+ * performed.
+ */
@Suppress("PrimitiveInCollection") // No way to get underlying Long of IntSize or IntOffset
@ExperimentalMaterial3AdaptiveApi
-internal interface PaneScaffoldMotionScope {
+sealed interface PaneScaffoldMotionScope {
+ /**
+ * The position animation spec of the associated pane to the scope. [AnimatedPane] will use this
+ * value to perform pane animations during scaffold state changes.
+ */
val positionAnimationSpec: FiniteAnimationSpec<IntOffset>
+
+ /**
+ * The size animation spec of the associated pane to the scope. [AnimatedPane] will use this
+ * value to perform pane animations during scaffold state changes.
+ */
val sizeAnimationSpec: FiniteAnimationSpec<IntSize>
+
+ /**
+ * The delayed position animation spec of the associated pane to the scope. [AnimatedPane] will
+ * use this value to perform pane position animations during scaffold state changes when an
+ * animation needs to be played with a delay.
+ */
val delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset>
+
+ /**
+ * The scaffold's current size. Note that the value of the field will only be updated during
+ * measurement of the scaffold and before the first measurement the value will be
+ * [IntSize.Zero].
+ *
+ * Note that this field is not backed by snapshot states so it's supposed to be only read
+ * proactively by the motion logic "on-the-fly" when the scaffold motion is happening.
+ */
val scaffoldSize: IntSize
- val currentPaneSizes: List<IntSize>
- val currentPanePositions: List<IntOffset>
- val targetPaneSizes: List<IntSize>
- val targetPanePositions: List<IntOffset>
- val paneMotions: List<PaneMotion>
- val motionProgress: Float
+
+ /**
+ * [PaneMotionData] of all panes in the scaffold corresponding to the scaffold's current state
+ * transition and motion settings, listed in panes' horizontal order.
+ *
+ * The size of position values of [PaneMotionData] in the list will only be update during
+ * measurement of the scaffold and before the first measurement their values will be
+ * [IntSize.Zero] or [IntOffset.Zero].
+ *
+ * Note that the aforementioned fields are not backed by snapshot states so they are supposed to
+ * be only read proactively by the motion logic "on-the-fly" when the scaffold motion is
+ * happening.
+ */
+ val paneMotionDataList: List<PaneMotionData>
+}
+
+/**
+ * A class to collect motion-relevant data of a specific pane.
+ *
+ * @property motion The specified [PaneMotion] of the pane.
+ * @property currentSize The current measured size of the pane that it should animate from.
+ * @property currentPosition The current placement of the pane that it should animate from, with the
+ * offset relative to the associated pane scaffold's local coordinates.
+ * @property targetSize The target measured size of the pane that it should animate to.
+ * @property targetPosition The target placement of the pane that it should animate to, with the
+ * offset relative to the associated pane scaffold's local coordinates.
+ */
+@ExperimentalMaterial3AdaptiveApi
+class PaneMotionData internal constructor() {
+ var motion: PaneMotion = DefaultPaneMotion.NoMotion
+ internal set
+
+ var currentSize: IntSize = IntSize.Zero
+ internal set
+
+ var currentPosition: IntOffset = IntOffset.Zero
+ internal set
+
+ var targetSize: IntSize = IntSize.Zero
+ internal set
+
+ var targetPosition: IntOffset = IntOffset.Zero
+ internal set
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal val PaneScaffoldMotionScope.slideInFromLeftOffset: Int
get() {
// Find the right edge offset of the rightmost pane that enters from its left
- for (i in paneMotions.lastIndex downTo 0) {
+ paneMotionDataList.fastForEachReversed {
if (
- paneMotions[i] == DefaultPaneMotion.EnterFromLeft ||
- paneMotions[i] == DefaultPaneMotion.EnterFromLeftDelayed
+ it.motion == DefaultPaneMotion.EnterFromLeft ||
+ it.motion == DefaultPaneMotion.EnterFromLeftDelayed
) {
- return -targetPanePositions[i].x - targetPaneSizes[i].width
+ return -it.targetPosition.x - it.targetSize.width
}
}
return 0
@@ -66,12 +132,12 @@
internal val PaneScaffoldMotionScope.slideInFromRightOffset: Int
get() {
// Find the left edge offset of the leftmost pane that enters from its right
- paneMotions.fastForEachIndexed { i, paneMotion ->
+ paneMotionDataList.fastForEach {
if (
- paneMotion == DefaultPaneMotion.EnterFromRight ||
- paneMotion == DefaultPaneMotion.EnterFromRightDelayed
+ it.motion == DefaultPaneMotion.EnterFromRight ||
+ it.motion == DefaultPaneMotion.EnterFromRightDelayed
) {
- return scaffoldSize.width - targetPanePositions[i].x
+ return scaffoldSize.width - it.targetPosition.x
}
}
return 0
@@ -81,9 +147,9 @@
internal val PaneScaffoldMotionScope.slideOutToLeftOffset: Int
get() {
// Find the right edge offset of the rightmost pane that exits to its left
- for (i in paneMotions.lastIndex downTo 0) {
- if (paneMotions[i] == DefaultPaneMotion.ExitToLeft) {
- return -currentPanePositions[i].x - currentPaneSizes[i].width
+ paneMotionDataList.fastForEachReversed {
+ if (it.motion == DefaultPaneMotion.ExitToLeft) {
+ return -it.currentPosition.x - it.currentSize.width
}
}
return 0
@@ -93,17 +159,21 @@
internal val PaneScaffoldMotionScope.slideOutToRightOffset: Int
get() {
// Find the left edge offset of the leftmost pane that exits to its right
- paneMotions.fastForEachIndexed { i, paneMotion ->
- if (paneMotion == DefaultPaneMotion.ExitToRight) {
- return scaffoldSize.width - currentPanePositions[i].x
+ paneMotionDataList.fastForEach {
+ if (it.motion == DefaultPaneMotion.ExitToRight) {
+ return scaffoldSize.width - it.currentPosition.x
}
}
return 0
}
+/** Interface to specify a custom pane enter/exit motion when a pane's visibility changes. */
@ExperimentalMaterial3AdaptiveApi
-internal interface PaneMotion {
+interface PaneMotion {
+ /** The [EnterTransition] of a pane under the given [PaneScaffoldMotionScope] */
val PaneScaffoldMotionScope.enterTransition: EnterTransition
+
+ /** The [ExitTransition] of a pane under the given [PaneScaffoldMotionScope] */
val PaneScaffoldMotionScope.exitTransition: ExitTransition
}
@@ -168,7 +238,6 @@
}
@ExperimentalMaterial3AdaptiveApi
-@VisibleForTesting
internal fun <T> calculatePaneMotion(
previousScaffoldValue: PaneScaffoldValue<T>,
currentScaffoldValue: PaneScaffoldValue<T>,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index 04b653e..3fd930e 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -16,7 +16,10 @@
package androidx.compose.material3.adaptive.layout
+import androidx.compose.animation.core.Transition
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectorInfo
@@ -25,7 +28,48 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-/** Scope for the panes of pane scaffolds. */
+/**
+ * Extended scope for the panes of pane scaffolds. All pane scaffolds will implement this interface
+ * to provide necessary info for panes to correctly render their content, motion, etc.
+ *
+ * @param Role The type of roles that denotes panes in the associated pane scaffold.
+ * @param ScaffoldValue The type of scaffold values that denotes the [PaneAdaptedValue]s in the
+ * associated pane scaffold.
+ * @see ThreePaneScaffoldPaneScope
+ * @see PaneScaffoldScope
+ * @see PaneScaffoldMotionScope
+ * @see PaneScaffoldTransitionScope
+ * @see PaneScaffoldPaneScope
+ * @see LookaheadScope
+ */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface ExtendedPaneScaffoldPaneScope<Role, ScaffoldValue : PaneScaffoldValue<Role>> :
+ ExtendedPaneScaffoldScope<Role, ScaffoldValue>, PaneScaffoldPaneScope<Role>
+
+/**
+ * Extended scope for pane scaffolds. All pane scaffolds will implement this interface to provide
+ * necessary info for its sub-composables to correctly render their content, motion, etc.
+ *
+ * @param Role The type of roles that denotes panes in the associated pane scaffold.
+ * @param ScaffoldValue The type of scaffold values that denotes the [PaneAdaptedValue]s in the
+ * associated pane scaffold.
+ * @see ThreePaneScaffoldScope
+ * @see PaneScaffoldScope
+ * @see PaneScaffoldMotionScope
+ * @see PaneScaffoldTransitionScope
+ * @see LookaheadScope
+ */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface ExtendedPaneScaffoldScope<Role, ScaffoldValue : PaneScaffoldValue<Role>> :
+ PaneScaffoldScope,
+ PaneScaffoldMotionScope,
+ PaneScaffoldTransitionScope<Role, ScaffoldValue>,
+ LookaheadScope
+
+/**
+ * The base scope of pane scaffolds, which provides scoped functions that supported by pane
+ * scaffolds.
+ */
sealed interface PaneScaffoldScope {
/**
* This modifier specifies the preferred width for a pane, and the pane scaffold implementation
@@ -39,6 +83,32 @@
fun Modifier.preferredWidth(width: Dp): Modifier
}
+/**
+ * The transition scope of pane scaffold implementations, which provides the current transition info
+ * of the associated pane scaffold.
+ */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface PaneScaffoldTransitionScope<Role, ScaffoldValue : PaneScaffoldValue<Role>> {
+ /** The current scaffold state transition between [PaneScaffoldValue]s. */
+ val scaffoldStateTransition: Transition<ScaffoldValue>
+
+ /** The current motion progress. */
+ val motionProgress: Float
+}
+
+/**
+ * The pane scope of the current pane under the scope, which provides the pane relevant info like
+ * its role and [PaneMotion].
+ */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface PaneScaffoldPaneScope<Role> {
+ /** The role of the current pane in the scope. */
+ val paneRole: Role
+
+ /** The specified pane motion of the current pane in the scope. */
+ val paneMotion: PaneMotion
+}
+
internal abstract class PaneScaffoldScopeImpl : PaneScaffoldScope {
override fun Modifier.preferredWidth(width: Dp): Modifier {
require(width == Dp.Unspecified || width > 0.dp) { "invalid width" }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
index d0af5ed..a2a06c7 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATED") // Deprecated import WindowWidthSizeClass.
+
package androidx.compose.material3.adaptive.layout
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
@@ -27,7 +29,6 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.window.core.layout.WindowWidthSizeClass
/**
* Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
@@ -44,6 +45,7 @@
* @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
*/
@ExperimentalMaterial3AdaptiveApi
+@Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
fun calculatePaneScaffoldDirective(
windowAdaptiveInfo: WindowAdaptiveInfo,
verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
@@ -51,11 +53,11 @@
val maxHorizontalPartitions: Int
val horizontalPartitionSpacerSize: Dp
when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
- WindowWidthSizeClass.COMPACT -> {
+ androidx.window.core.layout.WindowWidthSizeClass.COMPACT -> {
maxHorizontalPartitions = 1
horizontalPartitionSpacerSize = 0.dp
}
- WindowWidthSizeClass.MEDIUM -> {
+ androidx.window.core.layout.WindowWidthSizeClass.MEDIUM -> {
maxHorizontalPartitions = 1
horizontalPartitionSpacerSize = 0.dp
}
@@ -108,12 +110,14 @@
* @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
*/
@ExperimentalMaterial3AdaptiveApi
+@Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo: WindowAdaptiveInfo,
verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
): PaneScaffoldDirective {
val isMediumWidth =
- windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM
+ windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass ==
+ androidx.window.core.layout.WindowWidthSizeClass.MEDIUM
return with(calculatePaneScaffoldDirective(windowAdaptiveInfo, verticalHingePolicy)) {
copy(
maxHorizontalPartitions = if (isMediumWidth) 2 else maxHorizontalPartitions,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldValue.kt
index d3b1d1f..314207b 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldValue.kt
@@ -18,7 +18,13 @@
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+/**
+ * Interface to provide adapted value of panes.
+ *
+ * @see ThreePaneScaffoldValue
+ */
@ExperimentalMaterial3AdaptiveApi
-internal interface PaneScaffoldValue<T> {
+sealed interface PaneScaffoldValue<T> {
+ /** Returns the [PaneAdaptedValue] of the given [role] of a pane. */
operator fun get(role: T): PaneAdaptedValue
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
index 40d8c68..de8d35d 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
@@ -41,12 +41,10 @@
* @param extraPane the extra pane of the scaffold, which is supposed to hold any additional content
* besides the main and the supporting panes, for example, a styling panel in a doc app. See
* [SupportingPaneScaffoldRole.Extra].
- * @param paneExpansionDragHandle the pane expansion drag handle to let users be able to drag to
- * change pane expansion state. Note that by default this argument will be `null`, and there won't
- * be a drag handle rendered and users won't be able to drag to change the pane split. You can
- * provide a [PaneExpansionDragHandle] here as our sample suggests. On the other hand, even if
- * there's no drag handle, you can still modify [paneExpansionState] directly to apply pane
- * expansion.
+ * @param paneExpansionDragHandle provide a custom pane expansion drag handle to allow users to
+ * resize panes and change the pane expansion state by dragging. This is `null` by default, which
+ * renders no drag handle. Even there's no drag handle, you can still change pane size directly
+ * via modifying [paneExpansionState].
* @param paneExpansionState the state object of pane expansion.
*/
@ExperimentalMaterial3AdaptiveApi
@@ -54,11 +52,12 @@
fun SupportingPaneScaffold(
directive: PaneScaffoldDirective,
value: ThreePaneScaffoldValue,
- mainPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- supportingPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
modifier: Modifier = Modifier,
- extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
+ extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(value),
) {
ThreePaneScaffold(
@@ -94,16 +93,24 @@
* @param extraPane the extra pane of the scaffold, which is supposed to hold any additional content
* besides the main and the supporting panes, for example, a styling panel in a doc app. See
* [SupportingPaneScaffoldRole.Extra].
+ * @param paneExpansionDragHandle provide a custom pane expansion drag handle to allow users to
+ * resize panes and change the pane expansion state by dragging. This is `null` by default, which
+ * renders no drag handle. Even there's no drag handle, you can still change pane size directly
+ * via modifying [paneExpansionState].
+ * @param paneExpansionState the state object of pane expansion.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun SupportingPaneScaffold(
directive: PaneScaffoldDirective,
scaffoldState: ThreePaneScaffoldState,
- mainPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- supportingPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
modifier: Modifier = Modifier,
- extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
+ extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
+ paneExpansionState: PaneExpansionState = rememberPaneExpansionState(scaffoldState.targetState),
) {
ThreePaneScaffold(
modifier = modifier.fillMaxSize(),
@@ -112,6 +119,8 @@
paneOrder = SupportingPaneScaffoldDefaults.PaneOrder,
secondaryPane = supportingPane,
tertiaryPane = extraPane,
+ paneExpansionDragHandle = paneExpansionDragHandle,
+ paneExpansionState = paneExpansionState,
primaryPane = mainPane
)
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
index 28ecdc3..c3a54ac 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
@@ -16,8 +16,6 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Spring
@@ -25,296 +23,142 @@
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.VectorizedFiniteAnimationSpec
import androidx.compose.animation.core.VisibilityThreshold
-import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastForEachIndexed
-/** Holds the transitions that can be applied to the different panes. */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+internal fun calculateThreePaneMotion(
+ targetScaffoldValue: ThreePaneScaffoldValue,
+ paneOrder: ThreePaneScaffoldHorizontalOrder
+): ThreePaneMotion {
+ class ThreePaneScaffoldValueHolder(var value: ThreePaneScaffoldValue)
+
+ val layoutDirection = LocalLayoutDirection.current
+ val ltrPaneOrder =
+ remember(paneOrder, layoutDirection) { paneOrder.toLtrOrder(layoutDirection) }
+ val previousScaffoldValue = remember { ThreePaneScaffoldValueHolder(targetScaffoldValue) }
+ val threePaneMotion =
+ remember(targetScaffoldValue, ltrPaneOrder) {
+ val previousValue = previousScaffoldValue.value
+ previousScaffoldValue.value = targetScaffoldValue
+ val paneMotions = calculatePaneMotion(previousValue, targetScaffoldValue, ltrPaneOrder)
+ ThreePaneMotion(
+ paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Primary)],
+ paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Secondary)],
+ paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Tertiary)]
+ )
+ }
+ return threePaneMotion
+}
+
@ExperimentalMaterial3AdaptiveApi
@Immutable
-internal open class ThreePaneMotion
-internal constructor(
- internal val positionAnimationSpec: FiniteAnimationSpec<IntOffset> = snap(),
- internal val sizeAnimationSpec: FiniteAnimationSpec<IntSize> = snap(),
- private val firstPaneEnterTransition: EnterTransition = EnterTransition.None,
- private val firstPaneExitTransition: ExitTransition = ExitTransition.None,
- private val secondPaneEnterTransition: EnterTransition = EnterTransition.None,
- private val secondPaneExitTransition: ExitTransition = ExitTransition.None,
- private val thirdPaneEnterTransition: EnterTransition = EnterTransition.None,
- private val thirdPaneExitTransition: ExitTransition = ExitTransition.None
+internal class ThreePaneMotion(
+ val primaryPaneMotion: PaneMotion,
+ val secondaryPaneMotion: PaneMotion,
+ val tertiaryPaneMotion: PaneMotion,
+ val sizeAnimationSpec: FiniteAnimationSpec<IntSize> =
+ ThreePaneMotionDefaults.PaneSizeAnimationSpec,
+ val positionAnimationSpec: FiniteAnimationSpec<IntOffset> =
+ ThreePaneMotionDefaults.PanePositionAnimationSpec,
+ val delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset> =
+ ThreePaneMotionDefaults.PanePositionAnimationSpecDelayed
) {
+ fun copy(
+ primaryPaneMotion: PaneMotion = this.primaryPaneMotion,
+ secondaryPaneMotion: PaneMotion = this.secondaryPaneMotion,
+ tertiaryPaneMotion: PaneMotion = this.tertiaryPaneMotion,
+ sizeAnimationSpec: FiniteAnimationSpec<IntSize> = this.sizeAnimationSpec,
+ positionAnimationSpec: FiniteAnimationSpec<IntOffset> = this.positionAnimationSpec,
+ delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset> =
+ this.delayedPositionAnimationSpec
+ ): ThreePaneMotion =
+ ThreePaneMotion(
+ primaryPaneMotion,
+ secondaryPaneMotion,
+ tertiaryPaneMotion,
+ sizeAnimationSpec,
+ positionAnimationSpec,
+ delayedPositionAnimationSpec
+ )
- /**
- * Resolves and returns the [EnterTransition] for the given [ThreePaneScaffoldRole] at the given
- * [ThreePaneScaffoldHorizontalOrder].
- */
- fun enterTransition(
- role: ThreePaneScaffoldRole,
- paneOrder: ThreePaneScaffoldHorizontalOrder
- ): EnterTransition {
- // Quick return in case this instance is the NoMotion one.
- if (this === NoMotion) return EnterTransition.None
-
- return when (paneOrder.indexOf(role)) {
- 0 -> firstPaneEnterTransition
- 1 -> secondPaneEnterTransition
- else -> thirdPaneEnterTransition
+ operator fun get(role: ThreePaneScaffoldRole): PaneMotion =
+ when (role) {
+ ThreePaneScaffoldRole.Primary -> primaryPaneMotion
+ ThreePaneScaffoldRole.Secondary -> secondaryPaneMotion
+ ThreePaneScaffoldRole.Tertiary -> tertiaryPaneMotion
}
- }
-
- /**
- * Resolves and returns the [ExitTransition] for the given [ThreePaneScaffoldRole] at the given
- * [ThreePaneScaffoldHorizontalOrder].
- */
- fun exitTransition(
- role: ThreePaneScaffoldRole,
- paneOrder: ThreePaneScaffoldHorizontalOrder
- ): ExitTransition {
- // Quick return in case this instance is the NoMotion one.
- if (this === NoMotion) return ExitTransition.None
-
- return when (paneOrder.indexOf(role)) {
- 0 -> firstPaneExitTransition
- 1 -> secondPaneExitTransition
- else -> thirdPaneExitTransition
- }
- }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ThreePaneMotion) return false
- if (this.positionAnimationSpec != other.positionAnimationSpec) return false
- if (this.sizeAnimationSpec != other.sizeAnimationSpec) return false
- if (this.firstPaneEnterTransition != other.firstPaneEnterTransition) return false
- if (this.firstPaneExitTransition != other.firstPaneExitTransition) return false
- if (this.secondPaneEnterTransition != other.secondPaneEnterTransition) return false
- if (this.secondPaneExitTransition != other.secondPaneExitTransition) return false
- if (this.thirdPaneEnterTransition != other.thirdPaneEnterTransition) return false
- if (this.thirdPaneExitTransition != other.thirdPaneExitTransition) return false
+ if (primaryPaneMotion != other.primaryPaneMotion) return false
+ if (secondaryPaneMotion != other.secondaryPaneMotion) return false
+ if (tertiaryPaneMotion != other.tertiaryPaneMotion) return false
+ if (sizeAnimationSpec != other.sizeAnimationSpec) return false
+ if (positionAnimationSpec != other.positionAnimationSpec) return false
+ if (delayedPositionAnimationSpec != other.delayedPositionAnimationSpec) return false
return true
}
override fun hashCode(): Int {
- var result = positionAnimationSpec.hashCode()
+ var result = primaryPaneMotion.hashCode()
+ result = 31 * result + secondaryPaneMotion.hashCode()
+ result = 31 * result + tertiaryPaneMotion.hashCode()
result = 31 * result + sizeAnimationSpec.hashCode()
- result = 31 * result + firstPaneEnterTransition.hashCode()
- result = 31 * result + firstPaneExitTransition.hashCode()
- result = 31 * result + secondPaneEnterTransition.hashCode()
- result = 31 * result + secondPaneExitTransition.hashCode()
- result = 31 * result + thirdPaneEnterTransition.hashCode()
- result = 31 * result + thirdPaneExitTransition.hashCode()
+ result = 31 * result + positionAnimationSpec.hashCode()
+ result = 31 * result + delayedPositionAnimationSpec.hashCode()
return result
}
- companion object {
- /**
- * A ThreePaneMotion with all transitions set to [EnterTransition.None] and
- * [ExitTransition.None].
- */
- val NoMotion = ThreePaneMotion()
-
- @JvmStatic
- protected fun slideInFromLeft(spacerSize: Int) =
- slideInHorizontally(ThreePaneMotionDefaults.PanePositionAnimationSpec) {
- -it - spacerSize
- }
-
- @JvmStatic
- protected fun slideInFromLeftDelayed(spacerSize: Int) =
- slideInHorizontally(ThreePaneMotionDefaults.PanePositionAnimationSpecDelayed) {
- -it - spacerSize
- }
-
- @JvmStatic
- protected fun slideInFromRight(spacerSize: Int) =
- slideInHorizontally(ThreePaneMotionDefaults.PanePositionAnimationSpec) {
- it + spacerSize
- }
-
- @JvmStatic
- protected fun slideInFromRightDelayed(spacerSize: Int) =
- slideInHorizontally(ThreePaneMotionDefaults.PanePositionAnimationSpecDelayed) {
- it + spacerSize
- }
-
- @JvmStatic
- protected fun slideOutToLeft(spacerSize: Int) =
- slideOutHorizontally(ThreePaneMotionDefaults.PanePositionAnimationSpec) {
- -it - spacerSize
- }
-
- @JvmStatic
- protected fun slideOutToRight(spacerSize: Int) =
- slideOutHorizontally(ThreePaneMotionDefaults.PanePositionAnimationSpec) {
- it + spacerSize
- }
- }
-}
-
-@ExperimentalMaterial3AdaptiveApi
-@Immutable
-internal class MovePanesToLeftMotion(private val spacerSize: Int) :
- ThreePaneMotion(
- ThreePaneMotionDefaults.PanePositionAnimationSpec,
- ThreePaneMotionDefaults.PaneSizeAnimationSpec,
- slideInFromRight(spacerSize),
- slideOutToLeft(spacerSize),
- slideInFromRight(spacerSize),
- slideOutToLeft(spacerSize),
- slideInFromRight(spacerSize),
- slideOutToLeft(spacerSize)
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is MovePanesToLeftMotion) return false
- if (this.spacerSize != other.spacerSize) return false
- return true
+ override fun toString(): String {
+ return "ThreePaneMotion(" +
+ "primaryPaneMotion=$primaryPaneMotion, " +
+ "secondaryPaneMotion=$secondaryPaneMotion, " +
+ "tertiaryPaneMotion=$tertiaryPaneMotion, " +
+ "sizeAnimationSpec=$sizeAnimationSpec, " +
+ "positionAnimationSpec=$positionAnimationSpec, " +
+ "delayedPositionAnimationSpec=$delayedPositionAnimationSpec)"
}
- override fun hashCode(): Int {
- return spacerSize
- }
-}
-
-@ExperimentalMaterial3AdaptiveApi
-@Immutable
-internal class MovePanesToRightMotion(private val spacerSize: Int) :
- ThreePaneMotion(
- ThreePaneMotionDefaults.PanePositionAnimationSpec,
- ThreePaneMotionDefaults.PaneSizeAnimationSpec,
- slideInFromLeft(spacerSize),
- slideOutToRight(spacerSize),
- slideInFromLeft(spacerSize),
- slideOutToRight(spacerSize),
- slideInFromLeft(spacerSize),
- slideOutToRight(spacerSize)
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is MovePanesToRightMotion) return false
- if (this.spacerSize != other.spacerSize) return false
- return true
- }
-
- override fun hashCode(): Int {
- return spacerSize
- }
-}
-
-@ExperimentalMaterial3AdaptiveApi
-@Immutable
-internal class SwitchLeftTwoPanesMotion(private val spacerSize: Int) :
- ThreePaneMotion(
- ThreePaneMotionDefaults.PanePositionAnimationSpec,
- ThreePaneMotionDefaults.PaneSizeAnimationSpec,
- slideInFromLeftDelayed(spacerSize),
- slideOutToLeft(spacerSize),
- slideInFromLeftDelayed(spacerSize),
- slideOutToLeft(spacerSize),
- EnterTransition.None,
- ExitTransition.None
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is SwitchLeftTwoPanesMotion) return false
- if (this.spacerSize != other.spacerSize) return false
- return true
- }
-
- override fun hashCode(): Int {
- return spacerSize
- }
-}
-
-@ExperimentalMaterial3AdaptiveApi
-@Immutable
-internal class SwitchRightTwoPanesMotion(private val spacerSize: Int) :
- ThreePaneMotion(
- ThreePaneMotionDefaults.PanePositionAnimationSpec,
- ThreePaneMotionDefaults.PaneSizeAnimationSpec,
- EnterTransition.None,
- ExitTransition.None,
- slideInFromRightDelayed(spacerSize),
- slideOutToRight(spacerSize),
- slideInFromRightDelayed(spacerSize),
- slideOutToRight(spacerSize)
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is SwitchRightTwoPanesMotion) return false
- if (this.spacerSize != other.spacerSize) return false
- return true
- }
-
- override fun hashCode(): Int {
- return spacerSize
- }
+ internal fun toPaneMotionList(ltrOrder: ThreePaneScaffoldHorizontalOrder): List<PaneMotion> =
+ listOf(this[ltrOrder.firstPane], this[ltrOrder.secondPane], this[ltrOrder.thirdPane])
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal fun calculateThreePaneMotion(
- previousScaffoldValue: ThreePaneScaffoldValue,
- currentScaffoldValue: ThreePaneScaffoldValue,
- paneOrder: ThreePaneScaffoldHorizontalOrder,
- spacerSize: Int
-): ThreePaneMotion {
- if (previousScaffoldValue.equals(currentScaffoldValue)) {
- return ThreePaneMotion.NoMotion
- }
- val previousExpandedCount = previousScaffoldValue.expandedCount
- val currentExpandedCount = currentScaffoldValue.expandedCount
- if (previousExpandedCount != currentExpandedCount) {
- // TODO(conradchen): Address this case
- return ThreePaneMotion.NoMotion
- }
- return when (previousExpandedCount) {
- 1 ->
- when (PaneAdaptedValue.Expanded) {
- previousScaffoldValue[paneOrder.firstPane] -> {
- MovePanesToLeftMotion(spacerSize)
- }
- previousScaffoldValue[paneOrder.thirdPane] -> {
- MovePanesToRightMotion(spacerSize)
- }
- currentScaffoldValue[paneOrder.thirdPane] -> {
- MovePanesToLeftMotion(spacerSize)
- }
- else -> {
- MovePanesToRightMotion(spacerSize)
- }
- }
- 2 ->
- when {
- previousScaffoldValue[paneOrder.firstPane] == PaneAdaptedValue.Expanded &&
- currentScaffoldValue[paneOrder.firstPane] == PaneAdaptedValue.Expanded -> {
- // The first pane stays, the right two panes switch
- SwitchRightTwoPanesMotion(spacerSize)
- }
- previousScaffoldValue[paneOrder.thirdPane] == PaneAdaptedValue.Expanded &&
- currentScaffoldValue[paneOrder.thirdPane] == PaneAdaptedValue.Expanded -> {
- // The third pane stays, the left two panes switch
- SwitchLeftTwoPanesMotion(spacerSize)
- }
+@Suppress("PrimitiveInCollection") // No way to get underlying Long of IntSize or IntOffset
+internal class ThreePaneScaffoldMotionScopeImpl : PaneScaffoldMotionScope {
+ internal lateinit var threePaneMotion: ThreePaneMotion
+ private set
- // Implies the second pane stays hereafter
- currentScaffoldValue[paneOrder.thirdPane] == PaneAdaptedValue.Expanded -> {
- // The third pane shows, all panes move left
- MovePanesToLeftMotion(spacerSize)
- }
- else -> {
- // The first pane shows, all panes move right
- MovePanesToRightMotion(spacerSize)
- }
- }
- else -> {
- // Should not happen
- ThreePaneMotion.NoMotion
- }
+ override val sizeAnimationSpec: FiniteAnimationSpec<IntSize>
+ get() = threePaneMotion.sizeAnimationSpec
+
+ override val positionAnimationSpec: FiniteAnimationSpec<IntOffset>
+ get() = threePaneMotion.positionAnimationSpec
+
+ override val delayedPositionAnimationSpec: FiniteAnimationSpec<IntOffset>
+ get() = threePaneMotion.delayedPositionAnimationSpec
+
+ override var scaffoldSize: IntSize = IntSize.Zero
+ override val paneMotionDataList: List<PaneMotionData> =
+ listOf(PaneMotionData(), PaneMotionData(), PaneMotionData())
+
+ internal fun updateThreePaneMotion(
+ threePaneMotion: ThreePaneMotion,
+ ltrOrder: ThreePaneScaffoldHorizontalOrder
+ ) {
+ val paneMotions = threePaneMotion.toPaneMotionList(ltrOrder)
+ this.paneMotionDataList.fastForEachIndexed { index, it -> it.motion = paneMotions[index] }
+ this.threePaneMotion = threePaneMotion
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index d85c569..62f8235 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -16,11 +16,6 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.Transition
-import androidx.compose.animation.core.snap
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -39,7 +34,6 @@
import androidx.compose.ui.layout.MultiContentMeasurePolicy
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
@@ -83,11 +77,13 @@
scaffoldDirective: PaneScaffoldDirective,
scaffoldValue: ThreePaneScaffoldValue,
paneOrder: ThreePaneScaffoldHorizontalOrder,
- secondaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- tertiaryPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
+ secondaryPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ tertiaryPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
+ paneMotions: ThreePaneMotion = calculateThreePaneMotion(scaffoldValue, paneOrder),
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
- primaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
+ primaryPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
) {
val scaffoldState = remember { ThreePaneScaffoldState(scaffoldValue) }
LaunchedEffect(key1 = scaffoldValue) { scaffoldState.animateTo(scaffoldValue) }
@@ -98,6 +94,7 @@
paneOrder = paneOrder,
secondaryPane = secondaryPane,
tertiaryPane = tertiaryPane,
+ paneMotions = paneMotions,
paneExpansionState = paneExpansionState,
paneExpansionDragHandle = paneExpansionDragHandle,
primaryPane = primaryPane
@@ -111,117 +108,73 @@
scaffoldDirective: PaneScaffoldDirective,
scaffoldState: ThreePaneScaffoldState,
paneOrder: ThreePaneScaffoldHorizontalOrder,
- secondaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- tertiaryPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
+ secondaryPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ tertiaryPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
+ paneMotions: ThreePaneMotion = calculateThreePaneMotion(scaffoldState.targetState, paneOrder),
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
- primaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
+ primaryPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val ltrPaneOrder =
remember(paneOrder, layoutDirection) { paneOrder.toLtrOrder(layoutDirection) }
- val previousScaffoldValue = remember { ThreePaneScaffoldValueHolder(scaffoldState.targetState) }
- val spacerSize =
- with(LocalDensity.current) { scaffoldDirective.horizontalPartitionSpacerSize.roundToPx() }
- val paneMotion =
- remember(scaffoldState.targetState, ltrPaneOrder, spacerSize) {
- val previousValue = previousScaffoldValue.value
- previousScaffoldValue.value = scaffoldState.targetState
- calculateThreePaneMotion(
- previousScaffoldValue = previousValue,
- currentScaffoldValue = scaffoldState.targetState,
- paneOrder = ltrPaneOrder,
- spacerSize = spacerSize
- )
- }
+ val motionScope =
+ remember { ThreePaneScaffoldMotionScopeImpl() }
+ .apply { updateThreePaneMotion(paneMotions, ltrPaneOrder) }
val currentTransition = scaffoldState.rememberTransition()
+ val transitionScope =
+ remember { ThreePaneScaffoldTransitionScopeImpl() }
+ .apply {
+ transitionState = scaffoldState
+ scaffoldStateTransition = currentTransition
+ }
LookaheadScope {
+ val scaffoldScope =
+ remember(currentTransition, this) {
+ ThreePaneScaffoldScopeImpl(motionScope, transitionScope, this)
+ }
// Create PaneWrappers for each of the panes and map the transitions according to each pane
// role and order.
val contents =
listOf<@Composable () -> Unit>(
{
- remember(currentTransition, this@LookaheadScope) {
- ThreePaneScaffoldScopeImpl(
+ remember(scaffoldScope) {
+ ThreePaneScaffoldPaneScopeImpl(
ThreePaneScaffoldRole.Primary,
- currentTransition,
- scaffoldState,
- this@LookaheadScope
+ scaffoldScope
)
}
- .apply {
- positionAnimationSpec = paneMotion.positionAnimationSpec
- sizeAnimationSpec = paneMotion.sizeAnimationSpec
- enterTransition =
- paneMotion.enterTransition(
- ThreePaneScaffoldRole.Primary,
- ltrPaneOrder
- )
- exitTransition =
- paneMotion.exitTransition(
- ThreePaneScaffoldRole.Primary,
- ltrPaneOrder
- )
- }
+ .apply { updatePaneMotion(paneMotions) }
.primaryPane()
},
{
- remember(currentTransition, this@LookaheadScope) {
- ThreePaneScaffoldScopeImpl(
+ remember(scaffoldScope) {
+ ThreePaneScaffoldPaneScopeImpl(
ThreePaneScaffoldRole.Secondary,
- currentTransition,
- scaffoldState,
- this@LookaheadScope
+ scaffoldScope
)
}
- .apply {
- positionAnimationSpec = paneMotion.positionAnimationSpec
- sizeAnimationSpec = paneMotion.sizeAnimationSpec
- enterTransition =
- paneMotion.enterTransition(
- ThreePaneScaffoldRole.Secondary,
- ltrPaneOrder
- )
- exitTransition =
- paneMotion.exitTransition(
- ThreePaneScaffoldRole.Secondary,
- ltrPaneOrder
- )
- }
+ .apply { updatePaneMotion(paneMotions) }
.secondaryPane()
},
{
if (tertiaryPane != null) {
- remember(currentTransition, this@LookaheadScope) {
- ThreePaneScaffoldScopeImpl(
+ remember(scaffoldScope) {
+ ThreePaneScaffoldPaneScopeImpl(
ThreePaneScaffoldRole.Tertiary,
- currentTransition,
- scaffoldState,
- this@LookaheadScope
+ scaffoldScope
)
}
- .apply {
- positionAnimationSpec = paneMotion.positionAnimationSpec
- sizeAnimationSpec = paneMotion.sizeAnimationSpec
- enterTransition =
- paneMotion.enterTransition(
- ThreePaneScaffoldRole.Tertiary,
- ltrPaneOrder
- )
- exitTransition =
- paneMotion.exitTransition(
- ThreePaneScaffoldRole.Tertiary,
- ltrPaneOrder
- )
- }
+ .apply { updatePaneMotion(paneMotions) }
.tertiaryPane()
}
},
{
if (paneExpansionDragHandle != null) {
- paneExpansionDragHandle(paneExpansionState)
+ scaffoldScope.paneExpansionDragHandle(paneExpansionState)
}
}
)
@@ -233,6 +186,7 @@
scaffoldState.targetState,
paneExpansionState,
ltrPaneOrder,
+ motionScope
)
}
.apply {
@@ -246,32 +200,17 @@
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private class ThreePaneScaffoldValueHolder(var value: ThreePaneScaffoldValue)
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class ThreePaneContentMeasurePolicy(
scaffoldDirective: PaneScaffoldDirective,
scaffoldValue: ThreePaneScaffoldValue,
val paneExpansionState: PaneExpansionState,
paneOrder: ThreePaneScaffoldHorizontalOrder,
+ val paneMotionScope: ThreePaneScaffoldMotionScopeImpl
) : MultiContentMeasurePolicy {
var scaffoldDirective by mutableStateOf(scaffoldDirective)
var scaffoldValue by mutableStateOf(scaffoldValue)
var paneOrder by mutableStateOf(paneOrder)
- /**
- * Data class that is used to store the position and width of an expanded pane to be reused when
- * the pane is being hidden.
- */
- data class PanePlacement(var positionX: Int = 0, var measuredWidth: Int = 0)
-
- private val placementsCache =
- mapOf(
- ThreePaneScaffoldRole.Primary to PanePlacement(),
- ThreePaneScaffoldRole.Secondary to PanePlacement(),
- ThreePaneScaffoldRole.Tertiary to PanePlacement()
- )
-
override fun MeasureScope.measure(
measurables: List<List<Measurable>>,
constraints: Constraints
@@ -284,6 +223,7 @@
if (coordinates == null) {
return@layout
}
+ paneMotionScope.scaffoldSize = IntSize(constraints.maxWidth, constraints.maxHeight)
val visiblePanes =
getPanesMeasurables(
paneOrder = paneOrder,
@@ -604,12 +544,12 @@
) {
with(measurable) {
measureAndPlace(
- localBounds.width,
- localBounds.height,
- localBounds.left,
- localBounds.top,
- if (isLookingAhead) placementsCache else null
- )
+ localBounds.width,
+ localBounds.height,
+ localBounds.left,
+ localBounds.top,
+ )
+ .save(measurable.role, isLookingAhead)
}
}
@@ -651,12 +591,12 @@
measurables.fastForEach {
with(it) {
measureAndPlace(
- it.measuringWidth,
- partitionBounds.height,
- positionX,
- partitionBounds.top,
- if (isLookingAhead) placementsCache else null
- )
+ it.measuringWidth,
+ partitionBounds.height,
+ positionX,
+ partitionBounds.top,
+ )
+ .save(it.role, isLookingAhead)
}
positionX += it.measuredWidth + spacerSize
}
@@ -675,20 +615,30 @@
// When panes are not animated, we don't need to measure and place them.
return
}
- val cachedPanePlacement = placementsCache[it.role]!!
with(it) {
+ val measuredData = paneMotionScope.paneMotionDataList[paneOrder.indexOf(it.role)]
measureAndPlace(
- cachedPanePlacement.measuredWidth,
+ measuredData.targetSize.width,
partitionHeight,
- cachedPanePlacement.positionX,
+ measuredData.targetPosition.x,
partitionTop,
- null,
ThreePaneScaffoldDefaults.HiddenPaneZIndex
)
}
}
}
+ private fun PaneMeasurement.save(role: ThreePaneScaffoldRole, isLookingAhead: Boolean) {
+ val paneMotionData = paneMotionScope.paneMotionDataList[paneOrder.indexOf(role)]
+ if (isLookingAhead) {
+ paneMotionData.targetSize = this.size
+ paneMotionData.targetPosition = this.offset
+ } else {
+ paneMotionData.currentSize = this.size
+ paneMotionData.currentPosition = this.offset
+ }
+ }
+
private fun Placeable.PlacementScope.getLocalBounds(bounds: Rect): IntRect {
return bounds.translate(coordinates!!.windowToLocal(Offset.Zero)).roundToIntRect()
}
@@ -764,9 +714,8 @@
height: Int,
positionX: Int,
positionY: Int,
- placementsCache: Map<ThreePaneScaffoldRole, ThreePaneContentMeasurePolicy.PanePlacement>?,
zIndex: Float = 0f
- ) {
+ ): PaneMeasurement {
measuredWidth = width
measuredHeight = height
placedPositionX = positionX
@@ -774,73 +723,11 @@
measurable.measure(Constraints.fixed(width, height)).place(positionX, positionY, zIndex)
measuredAndPlaced = true
- // Cache the values to be used when this measurable's role is being hidden.
- // See placeHiddenPanes.
- if (placementsCache != null) {
- val cachedPanePlacement = placementsCache[role]!!
- cachedPanePlacement.measuredWidth = width
- cachedPanePlacement.positionX = positionX
- }
+ return PaneMeasurement(IntSize(width, height), IntOffset(positionX, positionY))
}
}
-/** Scope for the panes of [ThreePaneScaffold]. */
-@ExperimentalMaterial3AdaptiveApi
-sealed interface ThreePaneScaffoldScope : PaneScaffoldScope, LookaheadScope {
- /** The [ThreePaneScaffoldRole] of the current pane in the scope. */
- val role: ThreePaneScaffoldRole
-
- /** The current scaffold state transition between [ThreePaneScaffoldValue]s. */
- val scaffoldStateTransition: Transition<ThreePaneScaffoldValue>
-
- /** The current fraction of the scaffold state transition. */
- val scaffoldStateTransitionFraction: Float
-
- /**
- * The position animation spec of the associated pane to the scope. [AnimatedPane] will use this
- * value to perform pane animations during scaffold state changes.
- */
- val positionAnimationSpec: FiniteAnimationSpec<IntOffset>
-
- /**
- * The size animation spec of the associated pane to the scope. [AnimatedPane] will use this
- * value to perform pane animations during scaffold state changes.
- */
- val sizeAnimationSpec: FiniteAnimationSpec<IntSize>
-
- /**
- * The [EnterTransition] of the associated pane. [AnimatedPane] will use this value to perform
- * pane entering animations when it's showing during scaffold state changes.
- */
- val enterTransition: EnterTransition
-
- /**
- * The [ExitTransition] of the associated pane. [AnimatedPane] will use this value to perform
- * pane exiting animations when it's hiding during scaffold state changes.
- */
- val exitTransition: ExitTransition
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private class ThreePaneScaffoldScopeImpl(
- override val role: ThreePaneScaffoldRole,
- override val scaffoldStateTransition: Transition<ThreePaneScaffoldValue>,
- private val scaffoldState: ThreePaneScaffoldState,
- lookaheadScope: LookaheadScope
-) : ThreePaneScaffoldScope, LookaheadScope by lookaheadScope, PaneScaffoldScopeImpl() {
- override val scaffoldStateTransitionFraction: Float
- get() =
- if (scaffoldState.currentState == scaffoldState.targetState) {
- 1f
- } else {
- scaffoldState.progressFraction
- }
-
- override var positionAnimationSpec: FiniteAnimationSpec<IntOffset> by mutableStateOf(snap())
- override var sizeAnimationSpec: FiniteAnimationSpec<IntSize> by mutableStateOf(snap())
- override var enterTransition by mutableStateOf(EnterTransition.None)
- override var exitTransition by mutableStateOf(ExitTransition.None)
-}
+private data class PaneMeasurement(val size: IntSize, val offset: IntOffset)
/**
* Provides default values of [ThreePaneScaffold] and the calculation functions of
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
index 3c0b995..8ebafe4 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldDestinationItem.kt
@@ -22,31 +22,31 @@
* An item representing a navigation destination in a [ThreePaneScaffold].
*
* @param pane the pane destination of the navigation.
- * @param content the optional content, or an id representing the content of the destination. The
- * type [T] must be storable in a Bundle.
+ * @param contentKey the optional key or id representing the content of the destination. The type
+ * [T] must be storable in a Bundle.
*/
@ExperimentalMaterial3AdaptiveApi
class ThreePaneScaffoldDestinationItem<out T>(
val pane: ThreePaneScaffoldRole,
- val content: T? = null,
+ val contentKey: T? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ThreePaneScaffoldDestinationItem<*>) return false
if (pane != other.pane) return false
- if (content != other.content) return false
+ if (contentKey != other.contentKey) return false
return true
}
override fun hashCode(): Int {
var result = pane.hashCode()
- result = 31 * result + (content?.hashCode() ?: 0)
+ result = 31 * result + (contentKey?.hashCode() ?: 0)
return result
}
override fun toString(): String {
- return "ThreePaneScaffoldDestinationItem(pane=$pane, content=$content)"
+ return "ThreePaneScaffoldDestinationItem(pane=$pane, contentKey=$contentKey)"
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
new file mode 100644
index 0000000..8454908
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.animation.core.Transition
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.LookaheadScope
+
+/** Scope for the panes of [ThreePaneScaffold]. */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface ThreePaneScaffoldScope :
+ ExtendedPaneScaffoldScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue>
+
+/** Scope for the panes of [ThreePaneScaffold]. */
+@ExperimentalMaterial3AdaptiveApi
+sealed interface ThreePaneScaffoldPaneScope :
+ ThreePaneScaffoldScope,
+ ExtendedPaneScaffoldPaneScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue>
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal class ThreePaneScaffoldScopeImpl(
+ motionScope: PaneScaffoldMotionScope,
+ transitionScope: PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue>,
+ lookaheadScope: LookaheadScope
+) :
+ ThreePaneScaffoldScope,
+ PaneScaffoldMotionScope by motionScope,
+ PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue> by transitionScope,
+ LookaheadScope by lookaheadScope,
+ PaneScaffoldScopeImpl()
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal class ThreePaneScaffoldPaneScopeImpl(
+ override val paneRole: ThreePaneScaffoldRole,
+ scaffoldScope: ThreePaneScaffoldScope,
+) : ThreePaneScaffoldPaneScope, ThreePaneScaffoldScope by scaffoldScope {
+ override var paneMotion: PaneMotion by mutableStateOf(DefaultPaneMotion.ExitToLeft)
+ private set
+
+ fun updatePaneMotion(paneMotions: ThreePaneMotion) {
+ paneMotion = paneMotions[paneRole]
+ }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal class ThreePaneScaffoldTransitionScopeImpl :
+ PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue> {
+ override val motionProgress: Float
+ get() =
+ if (transitionState.currentState == transitionState.targetState) {
+ 1f
+ } else {
+ transitionState.progressFraction
+ }
+
+ override lateinit var scaffoldStateTransition: Transition<ThreePaneScaffoldValue>
+ lateinit var transitionState: ThreePaneScaffoldState
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt
index b7b8385..6363568 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldState.kt
@@ -24,12 +24,14 @@
import androidx.compose.foundation.MutatorMutex
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
/**
* The state of a three pane scaffold. It serves as the [SeekableTransitionState] to manipulate the
* [Transition] between [ThreePaneScaffoldValue]s.
*/
@ExperimentalMaterial3AdaptiveApi
+@Stable
class ThreePaneScaffoldState
internal constructor(
private val transitionState: SeekableTransitionState<ThreePaneScaffoldValue>,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
index 294c99b..2f2af3d 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldValue.kt
@@ -18,6 +18,8 @@
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
import androidx.compose.ui.util.fastForEachReversed
@ExperimentalMaterial3AdaptiveApi
@@ -202,7 +204,7 @@
if (tertiary == PaneAdaptedValue.Expanded) {
expandedPanes[count] = ThreePaneScaffoldRole.Tertiary
}
- PaneExpansionStateKeyImpl(expandedPanes[0]!!, expandedPanes[1]!!)
+ TwoPaneExpansionStateKeyImpl(expandedPanes[0]!!, expandedPanes[1]!!)
}
}
@@ -234,20 +236,34 @@
ThreePaneScaffoldRole.Secondary -> secondary
ThreePaneScaffoldRole.Tertiary -> tertiary
}
+}
- private class PaneExpansionStateKeyImpl(
- val firstExpandedPane: ThreePaneScaffoldRole,
- val secondExpandedPane: ThreePaneScaffoldRole
- ) : PaneExpansionStateKey {
- override fun hashCode(): Int {
- return firstExpandedPane.hashCode() * 31 + secondExpandedPane.hashCode()
- }
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal class TwoPaneExpansionStateKeyImpl(
+ val firstExpandedPane: ThreePaneScaffoldRole,
+ val secondExpandedPane: ThreePaneScaffoldRole
+) : PaneExpansionStateKey {
+ override fun hashCode(): Int {
+ return firstExpandedPane.hashCode() * 31 + secondExpandedPane.hashCode()
+ }
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- val otherKey = other as? PaneExpansionStateKeyImpl ?: return false
- return firstExpandedPane == otherKey.firstExpandedPane &&
- secondExpandedPane == otherKey.secondExpandedPane
- }
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherKey = other as? TwoPaneExpansionStateKeyImpl ?: return false
+ return firstExpandedPane == otherKey.firstExpandedPane &&
+ secondExpandedPane == otherKey.secondExpandedPane
+ }
+
+ companion object {
+ fun saver(): Saver<TwoPaneExpansionStateKeyImpl, Any> =
+ listSaver(
+ save = { listOf(it.firstExpandedPane, it.secondExpandedPane) },
+ restore = {
+ TwoPaneExpansionStateKeyImpl(
+ firstExpandedPane = it[0],
+ secondExpandedPane = it[1]
+ )
+ }
+ )
}
}
diff --git a/compose/material3/adaptive/adaptive-navigation/api/current.txt b/compose/material3/adaptive/adaptive-navigation/api/current.txt
index 962ebd8..ac6ed05 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/current.txt
@@ -2,8 +2,8 @@
package androidx.compose.material3.adaptive.navigation {
public final class AndroidThreePaneScaffold_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableListDetailPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableListDetailPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
@@ -28,7 +28,7 @@
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getScaffoldValue();
method public boolean isDestinationHistoryAware();
method public boolean navigateBack(optional String backNavigationBehavior);
- method public void navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? content);
+ method public void navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue peekPreviousScaffoldValue(optional String backNavigationBehavior);
method public void setDestinationHistoryAware(boolean);
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? currentDestination;
@@ -38,9 +38,9 @@
}
public final class ThreePaneScaffoldNavigatorKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
}
diff --git a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
index 962ebd8..ac6ed05 100644
--- a/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-navigation/api/restricted_current.txt
@@ -2,8 +2,8 @@
package androidx.compose.material3.adaptive.navigation {
public final class AndroidThreePaneScaffold_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableListDetailPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableListDetailPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigableSupportingPaneScaffold(androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> navigator, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope,kotlin.Unit>? extraPane, optional String defaultBackBehavior, optional kotlin.jvm.functions.Function2<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,? super androidx.compose.material3.adaptive.layout.PaneExpansionState,kotlin.Unit>? paneExpansionDragHandle, optional androidx.compose.material3.adaptive.layout.PaneExpansionState paneExpansionState);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @kotlin.jvm.JvmInline public final value class BackNavigationBehavior {
@@ -28,7 +28,7 @@
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue getScaffoldValue();
method public boolean isDestinationHistoryAware();
method public boolean navigateBack(optional String backNavigationBehavior);
- method public void navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? content);
+ method public void navigateTo(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane, optional T? contentKey);
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue peekPreviousScaffoldValue(optional String backNavigationBehavior);
method public void setDestinationHistoryAware(boolean);
property public abstract androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<T>? currentDestination;
@@ -38,9 +38,9 @@
}
public final class ThreePaneScaffoldNavigatorKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberListDetailPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<java.lang.Object> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware);
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static <T> androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator<T> rememberSupportingPaneScaffoldNavigator(optional androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, optional androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies, optional boolean isDestinationHistoryAware, optional java.util.List<? extends androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem<? extends T>> initialDestinationHistory);
}
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index 67fef66..dbbcdc2 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -78,7 +78,7 @@
dependencies {
implementation(project(":compose:material3:material3"))
implementation(project(":compose:test-utils"))
- implementation(project(":window:window-testing"))
+ implementation("androidx.window:window-testing:1.3.0")
implementation(libs.junit)
implementation(libs.testRunner)
implementation(libs.truth)
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
index 54e2cf5..5296dfd 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
@@ -63,7 +63,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isTrue()
}
}
@@ -92,7 +92,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isFalse()
}
}
@@ -122,7 +122,7 @@
.isEqualTo(PaneAdaptedValue.Hidden)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isTrue()
}
}
@@ -152,7 +152,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isTrue()
}
}
@@ -177,7 +177,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isTrue()
scaffoldNavigator.navigateBack()
}
@@ -187,7 +187,7 @@
.isEqualTo(PaneAdaptedValue.Hidden)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.List)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
assertThat(canNavigateBack).isFalse()
}
}
@@ -211,7 +211,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(scaffoldNavigator.canNavigateBack()).isFalse()
}
}
@@ -237,7 +237,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopLatest)).isTrue()
scaffoldNavigator.navigateBack(BackNavigationBehavior.PopLatest)
}
@@ -247,7 +247,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.List)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
}
}
@@ -271,7 +271,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(
scaffoldNavigator.canNavigateBack(
BackNavigationBehavior.PopUntilCurrentDestinationChange
@@ -284,7 +284,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.List)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
}
}
@@ -307,7 +307,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(
scaffoldNavigator.canNavigateBack(
BackNavigationBehavior.PopUntilCurrentDestinationChange
@@ -337,7 +337,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(
scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopUntilContentChange)
)
@@ -348,7 +348,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -372,7 +372,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(
scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopUntilContentChange)
)
@@ -383,7 +383,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -406,7 +406,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(
scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopUntilContentChange)
)
@@ -439,7 +439,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.List)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
}
@@ -451,7 +451,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
scaffoldNavigator.navigateBack()
}
@@ -463,7 +463,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.List)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
}
}
@@ -492,7 +492,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.List)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 0)
}
@@ -504,7 +504,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
scaffoldNavigator.navigateBack()
}
@@ -516,7 +516,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -664,7 +664,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
// Switches to dual pane
mockCurrentScaffoldDirective.value = MockDualPaneScaffoldDirective
}
@@ -673,7 +673,7 @@
assertThat(scaffoldNavigator.canNavigateBack()).isFalse()
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(ListDetailPaneScaffoldRole.Detail)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
}
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
index d8c1561..ef9c1bf 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
@@ -63,7 +63,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isTrue()
}
}
@@ -92,7 +92,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
assertThat(canNavigateBack).isFalse()
}
}
@@ -123,7 +123,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra, 1)
}
@@ -132,7 +132,7 @@
.isEqualTo(PaneAdaptedValue.Hidden)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(canNavigateBack).isTrue()
}
}
@@ -163,7 +163,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra, 1)
}
@@ -172,7 +172,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(canNavigateBack).isTrue()
}
}
@@ -199,7 +199,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(canNavigateBack).isTrue()
scaffoldNavigator.navigateBack()
}
@@ -209,7 +209,7 @@
.isEqualTo(PaneAdaptedValue.Hidden)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
assertThat(canNavigateBack).isFalse()
}
}
@@ -236,7 +236,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
assertThat(scaffoldNavigator.canNavigateBack()).isFalse()
}
}
@@ -265,7 +265,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
assertThat(scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopLatest)).isTrue()
scaffoldNavigator.navigateBack(BackNavigationBehavior.PopLatest)
}
@@ -275,7 +275,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -305,7 +305,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(
scaffoldNavigator.canNavigateBack(
BackNavigationBehavior.PopUntilCurrentDestinationChange
@@ -318,7 +318,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
}
}
@@ -341,7 +341,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(
scaffoldNavigator.canNavigateBack(
BackNavigationBehavior.PopUntilCurrentDestinationChange
@@ -377,7 +377,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
assertThat(
scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopUntilContentChange)
)
@@ -388,7 +388,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -415,7 +415,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(
scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopUntilContentChange)
)
@@ -426,7 +426,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -452,7 +452,7 @@
composeRule.runOnIdle {
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
assertThat(
scaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopUntilContentChange)
)
@@ -488,7 +488,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
}
@@ -500,7 +500,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
scaffoldNavigator.navigateBack()
}
@@ -512,7 +512,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
}
}
@@ -544,7 +544,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Supporting)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(0)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(0)
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Main)
}
@@ -556,7 +556,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
scaffoldNavigator.navigateBack()
}
@@ -568,7 +568,7 @@
)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Extra)
- assertThat(scaffoldNavigator.currentDestination?.content).isEqualTo(1)
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isEqualTo(1)
}
}
@@ -728,7 +728,7 @@
.isEqualTo(PaneAdaptedValue.Expanded)
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
// Switches to dual pane
mockCurrentScaffoldDirective.value = MockDualPaneScaffoldDirective
}
@@ -737,7 +737,7 @@
assertThat(scaffoldNavigator.canNavigateBack()).isFalse()
assertThat(scaffoldNavigator.currentDestination?.pane)
.isEqualTo(SupportingPaneScaffoldRole.Main)
- assertThat(scaffoldNavigator.currentDestination?.content).isNull()
+ assertThat(scaffoldNavigator.currentDestination?.contentKey).isNull()
}
}
}
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt b/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt
index e55f244..48ba07a 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation/AndroidThreePaneScaffold.android.kt
@@ -22,6 +22,7 @@
import androidx.compose.material3.adaptive.layout.PaneExpansionDragHandle
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold as BaseSupportingPaneScaffold
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.runtime.Composable
@@ -56,12 +57,13 @@
@Composable
fun NavigableListDetailPaneScaffold(
navigator: ThreePaneScaffoldNavigator<Any>,
- listPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- detailPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
modifier: Modifier = Modifier,
- extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
+ extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
defaultBackBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilContentChange,
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue),
) {
// TODO(b/330584029): support predictive back
@@ -108,12 +110,13 @@
@Composable
fun NavigableSupportingPaneScaffold(
navigator: ThreePaneScaffoldNavigator<Any>,
- mainPane: @Composable ThreePaneScaffoldScope.() -> Unit,
- supportingPane: @Composable ThreePaneScaffoldScope.() -> Unit,
+ mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
+ supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
modifier: Modifier = Modifier,
- extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
+ extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
defaultBackBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilContentChange,
- paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
+ paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
+ null,
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(navigator.scaffoldValue),
) {
// TODO(b/330584029): support predictive back
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
index e3ba0c4..66f554d7 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/BackNavigationBehavior.kt
@@ -55,7 +55,7 @@
/**
* Pop destinations from the backstack until there is a content change.
*
- * A "content change" is defined as either a change in the content of the current
+ * A "content change" is defined as either a change in the `contentKey` of the current
* [ThreePaneScaffoldDestinationItem], or a change in the scaffold value (similar to
* [PopUntilScaffoldValueChange]).
*/
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
index 81a8de2..42378e4 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
@@ -59,9 +59,9 @@
* and the default implementation to get better understanding and address the intricacies of
* navigation in an adaptive scenario.
*
- * @param T the type representing the content, or id of the content, for a navigation destination.
- * This type must be storable in a Bundle. Used to customize navigation behavior (for example,
- * [BackNavigationBehavior]). If this customization is unneeded, you can pass [Nothing].
+ * @param T the type representing the content key/id for a navigation destination. This type must be
+ * storable in a Bundle. Used to customize navigation behavior (for example,
+ * [BackNavigationBehavior]). If this customization is unneeded, you can pass [Any].
*/
@ExperimentalMaterial3AdaptiveApi
@Stable
@@ -116,10 +116,9 @@
* pane currently being used.
*
* @param pane the new destination pane.
- * @param content the optional content, or an id representing the content of the new
- * destination.
+ * @param contentKey the optional key or id representing the content of the new destination.
*/
- fun navigateTo(pane: ThreePaneScaffoldRole, content: T? = null)
+ fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T? = null)
/**
* Returns `true` if there is a previous destination to navigate back to.
@@ -157,9 +156,9 @@
* default navigator is supposed to be used independently from any navigation frameworks and handles
* the navigation purely inside the [ListDetailPaneScaffold].
*
- * @param T the type representing the content, or id of the content, for a navigation destination.
- * This type must be storable in a Bundle. Used to customize navigation behavior (for example,
- * [BackNavigationBehavior]). If this customization is unneeded, you can pass [Nothing].
+ * @param T the type representing the content key/id for a navigation destination. This type must be
+ * storable in a Bundle. Used to customize navigation behavior (for example,
+ * [BackNavigationBehavior]). If this customization is unneeded, you can pass [Any].
* @param scaffoldDirective the current layout directives to follow. The default value will be
* calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from the
@@ -212,8 +211,8 @@
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
ListDetailPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
-): ThreePaneScaffoldNavigator<Nothing> =
- rememberListDetailPaneScaffoldNavigator<Nothing>(
+): ThreePaneScaffoldNavigator<Any> =
+ rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware,
@@ -225,9 +224,9 @@
* default navigator is supposed to be used independently from any navigation frameworks and handles
* the navigation purely inside the [SupportingPaneScaffold].
*
- * @param T the type representing the content, or id of the content, for a navigation destination.
- * This type must be storable in a Bundle. Used to customize navigation behavior (for example,
- * [BackNavigationBehavior]). If this customization is unneeded, you can pass [Nothing].
+ * @param T the type representing the content key/id for a navigation destination. This type must be
+ * storable in a Bundle. Used to customize navigation behavior (for example,
+ * [BackNavigationBehavior]). If this customization is unneeded, you can pass [Any].
* @param scaffoldDirective the current layout directives to follow. The default value will be
* calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from the
@@ -280,8 +279,8 @@
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
SupportingPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
-): ThreePaneScaffoldNavigator<Nothing> =
- rememberSupportingPaneScaffoldNavigator<Nothing>(
+): ThreePaneScaffoldNavigator<Any> =
+ rememberSupportingPaneScaffoldNavigator<Any>(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware,
@@ -349,8 +348,8 @@
return if (index == -1) scaffoldValue else calculateScaffoldValue(index)
}
- override fun navigateTo(pane: ThreePaneScaffoldRole, content: T?) {
- destinationHistory.add(ThreePaneScaffoldDestinationItem(pane, content))
+ override fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) {
+ destinationHistory.add(ThreePaneScaffoldDestinationItem(pane, contentKey))
}
override fun canNavigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean =
@@ -392,8 +391,8 @@
}
BackNavigationBehavior.PopUntilContentChange ->
for (previousDestinationIndex in destinationHistory.lastIndex - 1 downTo 0) {
- val content = destinationHistory[previousDestinationIndex].content
- if (content != currentDestination?.content) {
+ val contentKey = destinationHistory[previousDestinationIndex].contentKey
+ if (contentKey != currentDestination?.contentKey) {
return previousDestinationIndex
}
// A scaffold value change also counts as a content change.
@@ -461,12 +460,12 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal fun <T> destinationItemSaver(): Saver<ThreePaneScaffoldDestinationItem<T>, Any> =
listSaver(
- save = { listOf(it.pane, it.content) },
+ save = { listOf(it.pane, it.contentKey) },
restore = {
@Suppress("UNCHECKED_CAST")
(ThreePaneScaffoldDestinationItem(
pane = it[0] as ThreePaneScaffoldRole,
- content = it[1] as T?
+ contentKey = it[1] as T?
))
}
)
diff --git a/activity/activity-ktx/api/1.10.0-beta01.txt b/compose/material3/adaptive/adaptive-render-strategy/api/current.txt
similarity index 100%
rename from activity/activity-ktx/api/1.10.0-beta01.txt
rename to compose/material3/adaptive/adaptive-render-strategy/api/current.txt
diff --git a/activity/activity/api/res-1.10.0-beta01.txt b/compose/material3/adaptive/adaptive-render-strategy/api/res-current.txt
similarity index 100%
rename from activity/activity/api/res-1.10.0-beta01.txt
rename to compose/material3/adaptive/adaptive-render-strategy/api/res-current.txt
diff --git a/activity/activity-ktx/api/1.10.0-beta01.txt b/compose/material3/adaptive/adaptive-render-strategy/api/restricted_current.txt
similarity index 100%
copy from activity/activity-ktx/api/1.10.0-beta01.txt
copy to compose/material3/adaptive/adaptive-render-strategy/api/restricted_current.txt
diff --git a/compose/material3/adaptive/adaptive-render-strategy/build.gradle b/compose/material3/adaptive/adaptive-render-strategy/build.gradle
new file mode 100644
index 0000000..7634ff6
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-render-strategy/build.gradle
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+}
+
+androidXMultiplatform {
+ android()
+ jvmStubs()
+
+ defaultPlatform(PlatformIdentifier.ANDROID)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.8.1")
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidInstrumentedTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ commonStubsMain {
+ dependsOn(commonMain)
+ }
+
+ jvmStubsMain {
+ dependsOn(commonStubsMain)
+ }
+ }
+}
+
+android {
+ namespace "androidx.compose.material3.adaptive.render.strategy"
+}
+
+androidx {
+ name = "androidx.compose.material3.adaptive:adaptive-render-strategy"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "Material AdaptiveRenderStrategy library"
+}
diff --git a/compose/material3/adaptive/adaptive-render-strategy/src/commonMain/kotlin/androidx/compose/material3/adaptive/androidx-compose-material3-adaptive-adaptive-render-strategy-documentation.md b/compose/material3/adaptive/adaptive-render-strategy/src/commonMain/kotlin/androidx/compose/material3/adaptive/androidx-compose-material3-adaptive-adaptive-render-strategy-documentation.md
new file mode 100644
index 0000000..0fb75e6
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-render-strategy/src/commonMain/kotlin/androidx/compose/material3/adaptive/androidx-compose-material3-adaptive-adaptive-render-strategy-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+androidx.compose.material3.adaptive adaptive-render-strategy
+
+# Package androidx.compose.material3.adaptive.render.strategy
+
+Material AdaptiveRenderStrategy library
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 7ab026e..1cb594e 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -43,7 +43,7 @@
implementation(libs.kotlinStdlib)
api("androidx.compose.foundation:foundation:1.6.5")
api("androidx.compose.ui:ui-geometry:1.6.5")
- api("androidx.window:window-core:1.3.0-rc01")
+ api("androidx.window:window-core:1.3.0")
}
}
@@ -63,7 +63,7 @@
dependencies {
api("androidx.annotation:annotation:1.8.1")
api("androidx.annotation:annotation-experimental:1.4.1")
- api("androidx.window:window:1.3.0-rc01")
+ api("androidx.window:window:1.3.0")
}
}
@@ -78,7 +78,7 @@
dependencies {
implementation(project(":compose:material3:material3"))
implementation(project(":compose:test-utils"))
- implementation(project(":window:window-testing"))
+ implementation("androidx.window:window-testing:1.3.0")
implementation(libs.junit)
implementation(libs.testRunner)
implementation(libs.truth)
diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
deleted file mode 100644
index bf9047b..0000000
--- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3.adaptive
-
-import android.app.Activity
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Rect
-import androidx.annotation.UiContext
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.IntSize
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.window.layout.WindowMetrics
-import androidx.window.layout.WindowMetricsCalculator
-import androidx.window.layout.WindowMetricsCalculatorDecorator
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CollectWindowSizeAsStateTest {
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun test_collectWindowSizeAsState() {
- var actualWindowSize: IntSize = IntSize.Zero
-
- val mockWindowSize = mutableStateOf(MockWindowSize1)
- WindowMetricsCalculator.overrideDecorator(
- MockWindowMetricsCalculatorDecorator(mockWindowSize)
- )
-
- rule.setContent {
- val testConfiguration = Configuration(LocalConfiguration.current)
- testConfiguration.screenWidthDp = mockWindowSize.value.width
- testConfiguration.screenHeightDp = mockWindowSize.value.height
- CompositionLocalProvider(LocalConfiguration provides testConfiguration) {
- actualWindowSize = currentWindowSize()
- }
- }
-
- rule.runOnIdle { assertThat(actualWindowSize).isEqualTo(MockWindowSize1) }
-
- mockWindowSize.value = MockWindowSize2
-
- rule.runOnIdle { assertThat(actualWindowSize).isEqualTo(MockWindowSize2) }
- }
-
- companion object {
- val MockWindowSize1 = IntSize(1000, 600)
- val MockWindowSize2 = IntSize(800, 400)
- }
-}
-
-internal class MockWindowMetricsCalculatorDecorator(private val mockWindowSize: State<IntSize>) :
- WindowMetricsCalculatorDecorator {
- override fun decorate(calculator: WindowMetricsCalculator): WindowMetricsCalculator {
- return MockWindowMetricsCalculator(mockWindowSize)
- }
-}
-
-internal class MockWindowMetricsCalculator(private val mockWindowSize: State<IntSize>) :
- WindowMetricsCalculator {
- override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
- return WindowMetrics(
- Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
- density = 1f
- )
- }
-
- override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
- return computeCurrentWindowMetrics(activity)
- }
-
- override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
- return WindowMetrics(
- Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
- density = 1f
- )
- }
-}
diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CurrentWindowAdaptiveInfoTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CurrentWindowAdaptiveInfoTest.kt
deleted file mode 100644
index 8ef9012..0000000
--- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CurrentWindowAdaptiveInfoTest.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3.adaptive
-
-import android.content.res.Configuration
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.toSize
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.window.core.layout.WindowSizeClass
-import androidx.window.layout.FoldingFeature
-import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.WindowMetricsCalculator
-import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.RuleChain
-import org.junit.rules.TestRule
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CurrentWindowAdaptiveInfoTest {
- private val composeRule = createComposeRule()
- private val layoutInfoRule = WindowLayoutInfoPublisherRule()
-
- @get:Rule val testRule: TestRule
-
- init {
- testRule = RuleChain.outerRule(layoutInfoRule).around(composeRule)
- }
-
- @Test
- fun test_currentWindowAdaptiveInfo() {
- lateinit var actualAdaptiveInfo: WindowAdaptiveInfo
- val mockWindowSize = mutableStateOf(MockWindowSize1)
- WindowMetricsCalculator.overrideDecorator(
- MockWindowMetricsCalculatorDecorator(mockWindowSize)
- )
-
- composeRule.setContent {
- val testConfiguration = Configuration(LocalConfiguration.current)
- testConfiguration.screenWidthDp = mockWindowSize.value.width
- testConfiguration.screenHeightDp = mockWindowSize.value.height
- CompositionLocalProvider(
- LocalDensity provides MockDensity,
- LocalConfiguration provides testConfiguration
- ) {
- actualAdaptiveInfo = currentWindowAdaptiveInfo()
- }
- }
-
- layoutInfoRule.overrideWindowLayoutInfo(WindowLayoutInfo(MockFoldingFeatures1))
-
- composeRule.runOnIdle {
- val mockSize = with(MockDensity) { MockWindowSize1.toSize().toDpSize() }
- assertThat(actualAdaptiveInfo.windowSizeClass)
- .isEqualTo(WindowSizeClass.compute(mockSize.width.value, mockSize.height.value))
- assertThat(actualAdaptiveInfo.windowPosture)
- .isEqualTo(calculatePosture(MockFoldingFeatures1))
- }
-
- layoutInfoRule.overrideWindowLayoutInfo(WindowLayoutInfo(MockFoldingFeatures2))
- mockWindowSize.value = MockWindowSize2
-
- composeRule.runOnIdle {
- val mockSize = with(MockDensity) { MockWindowSize2.toSize().toDpSize() }
- assertThat(actualAdaptiveInfo.windowSizeClass)
- .isEqualTo(WindowSizeClass.compute(mockSize.width.value, mockSize.height.value))
- assertThat(actualAdaptiveInfo.windowPosture)
- .isEqualTo(calculatePosture(MockFoldingFeatures2))
- }
- }
-
- companion object {
- private val MockFoldingFeatures1 =
- listOf(
- MockFoldingFeature(orientation = FoldingFeature.Orientation.HORIZONTAL),
- MockFoldingFeature(orientation = FoldingFeature.Orientation.VERTICAL),
- MockFoldingFeature(orientation = FoldingFeature.Orientation.HORIZONTAL)
- )
-
- private val MockFoldingFeatures2 =
- listOf(
- MockFoldingFeature(
- isSeparating = false,
- orientation = FoldingFeature.Orientation.HORIZONTAL,
- state = FoldingFeature.State.FLAT
- ),
- )
-
- private val MockWindowSize1 = IntSize(400, 800)
- private val MockWindowSize2 = IntSize(800, 400)
-
- private val MockDensity = Density(1f, 1f)
- }
-}
diff --git a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidWindowAdaptiveInfo.android.kt b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidWindowAdaptiveInfo.android.kt
index 9781752..f47f656 100644
--- a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidWindowAdaptiveInfo.android.kt
+++ b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidWindowAdaptiveInfo.android.kt
@@ -33,6 +33,7 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
+@Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
actual fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo {
val windowSize = with(LocalDensity.current) { currentWindowSize().toSize().toDpSize() }
return WindowAdaptiveInfo(
diff --git a/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt b/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
index 246ccc2..ffa2d48 100644
--- a/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
+++ b/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
@@ -23,7 +23,7 @@
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
-import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -51,7 +51,7 @@
override fun toggleState() {}
@Composable
- fun ThreePaneScaffoldScope.TestPane(color: Color) {
+ fun ThreePaneScaffoldPaneScope.TestPane(color: Color) {
val content = @Composable { Box(modifier = Modifier.fillMaxSize().background(color)) }
if (animated) {
AnimatedPane(Modifier) { content() }
diff --git a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
index 77e13de..772978e 100644
--- a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
+++ b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
@@ -290,7 +290,7 @@
}
composable(listDetailRoute) {
val listScrollState = rememberScrollState()
- val selectedItem = scaffoldNavigator.currentDestination?.content
+ val selectedItem = scaffoldNavigator.currentDestination?.contentKey
// Back behavior can be customized based on the scaffold's layout.
// In this example, back navigation goes item-by-item when both
@@ -328,7 +328,7 @@
if (item != selectedItem) {
scaffoldNavigator.navigateTo(
pane = ListDetailPaneScaffoldRole.Detail,
- content = item,
+ contentKey = item,
)
}
}
diff --git a/compose/material3/material3-adaptive-navigation-suite/build.gradle b/compose/material3/material3-adaptive-navigation-suite/build.gradle
index 1c8f9ff..6d42780 100644
--- a/compose/material3/material3-adaptive-navigation-suite/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/build.gradle
@@ -44,7 +44,7 @@
api(project(":compose:material3:material3"))
api(project(":compose:material3:adaptive:adaptive"))
implementation("androidx.compose.ui:ui-util:1.6.0")
- implementation("androidx.window:window-core:1.3.0-beta02")
+ implementation("androidx.window:window-core:1.3.0")
}
}
@@ -77,7 +77,7 @@
dependsOn(commonTest)
dependencies {
implementation(project(":compose:test-utils"))
- implementation(project(":window:window-testing"))
+ implementation("androidx.window:window-testing:1.3.0")
implementation(libs.junit)
implementation(libs.testRunner)
implementation(libs.truth)
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
index d65e6a8..57e571d 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
@@ -44,7 +44,7 @@
implementation(project(":compose:material3:material3-adaptive-navigation-suite"))
implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
- implementation(project(":window:window-core"))
+ implementation("androidx.window:window-core:1.3.0")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
}
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
index df28658..7d112c9 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION") // Suppress for WindowWidthSizeClass
+
package androidx.compose.material3.adaptive.navigationsuite.samples
import androidx.annotation.Sampled
@@ -76,6 +78,7 @@
@Preview
@Sampled
@Composable
+@Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
fun NavigationSuiteScaffoldCustomConfigSample() {
var selectedItem by remember { mutableIntStateOf(0) }
val navItems = listOf("Songs", "Artists", "Playlists")
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 1eb9fa1..a68b7ddc 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -30,6 +30,7 @@
class NavigationSuiteScaffoldTest {
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_compactWidth_compactHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(400f, 400f))
@@ -39,6 +40,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_compactWidth_mediumHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(400f, 800f))
@@ -48,6 +50,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_compactWidth_expandedHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(400f, 1000f))
@@ -57,6 +60,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_mediumWidth_compactHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(800f, 400f))
@@ -66,6 +70,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_mediumWidth_mediumHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(800f, 800f))
@@ -75,6 +80,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_mediumWidth_expandedHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(800f, 1000f))
@@ -84,6 +90,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_expandedWidth_compactHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(1000f, 400f))
@@ -93,6 +100,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_expandedWidth_mediumHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(1000f, 800f))
@@ -102,6 +110,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_expandedWidth_expandedHeight() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(windowSizeClass = WindowSizeClass.compute(1000f, 1000f))
@@ -111,6 +120,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_tableTop() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(
@@ -123,6 +133,7 @@
}
@Test
+ @Suppress("DEPRECATION") // WindowSizeClass#compute is deprecated
fun navigationLayoutTypeTest_tableTop_expandedWidth() {
val mockAdaptiveInfo =
createMockAdaptiveInfo(
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index 5e96cd5..40758e0 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION") // Suppress for imports of WindowWidthSizeClass
+
package androidx.compose.material3.adaptive.navigationsuite
import androidx.compose.foundation.interaction.Interaction
@@ -407,6 +409,7 @@
* @param adaptiveInfo the provided [WindowAdaptiveInfo]
* @see NavigationSuiteScaffold
*/
+ @Suppress("DEPRECATION") // WindowWidthSizeClass deprecated
fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
return with(adaptiveInfo) {
if (
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index a9ee23e..f6366cc 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -271,10 +271,6 @@
public final class ButtonShapes {
ctor public ButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
- method public androidx.compose.ui.graphics.Shape component1();
- method public androidx.compose.ui.graphics.Shape component2();
- method public androidx.compose.ui.graphics.Shape component3();
- method public androidx.compose.material3.ButtonShapes copy(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
method public androidx.compose.ui.graphics.Shape getCheckedShape();
method public androidx.compose.ui.graphics.Shape getPressedShape();
method public androidx.compose.ui.graphics.Shape getShape();
@@ -975,53 +971,70 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFilledShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getLargeIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getLargePressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getLargeRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getLargeSquareShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getMediumIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMediumPressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMediumRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMediumSquareShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getOutlinedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getSmallIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallPressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallSquareShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getStandardShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getXLargeIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXLargePressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXLargeRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXLargeSquareShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getXSmallIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallPressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallSquareShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonLocalContentColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonLocalContentColors();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long largeContainerSize(optional int widthOption);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long mediumContainerSize(optional int widthOption);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconButtonLocalContentColorBorder(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonLocalContentColors();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonLocalContentColorBorder(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonLocalContentColors();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonShapes shapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long smallContainerSize(optional int widthOption);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long xLargeContainerSize(optional int widthOption);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long xSmallContainerSize(optional int widthOption);
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float largeIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largePressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeSquareShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float mediumIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape mediumPressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape mediumRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape mediumSquareShape;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float smallIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallPressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallSquareShape;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape standardShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float xLargeIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xLargePressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xLargeRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xLargeSquareShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float xSmallIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xSmallPressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xSmallRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xSmallSquareShape;
field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
@@ -1042,17 +1055,31 @@
public final class IconButtonKt {
method @androidx.compose.runtime.Composable public static void FilledIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FilledIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledTonalIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FilledTonalIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledTonalIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, 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 public static void OutlinedIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class IconButtonShapes {
+ ctor public IconButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
+ method public androidx.compose.ui.graphics.Shape getCheckedShape();
+ method public androidx.compose.ui.graphics.Shape getPressedShape();
+ method public androidx.compose.ui.graphics.Shape getShape();
+ property public final androidx.compose.ui.graphics.Shape checkedShape;
+ property public final androidx.compose.ui.graphics.Shape pressedShape;
+ property public final androidx.compose.ui.graphics.Shape shape;
+ }
+
public final class IconKt {
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.painter.Painter painter, androidx.compose.ui.graphics.ColorProducer? tint, String? contentDescription, optional androidx.compose.ui.Modifier modifier);
@@ -1253,6 +1280,7 @@
}
public final class MaterialShapesKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.Morph, float progress, optional androidx.compose.ui.graphics.Path path, optional int startAngle);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Shape toShape(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
}
@@ -2092,9 +2120,8 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class SplitButtonDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void AnimatedTrailingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean checked, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.shape.CornerSize startCornerSize, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void LeadingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void TrailingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void LeadingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SplitButtonShapes shapes, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void TrailingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean checked, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SplitButtonShapes shapes, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method public androidx.compose.foundation.shape.CornerSize getInnerCornerSize();
method public androidx.compose.foundation.layout.PaddingValues getLeadingButtonContentPadding();
method public float getLeadingIconSize();
@@ -2102,8 +2129,8 @@
method public float getSpacing();
method public androidx.compose.foundation.layout.PaddingValues getTrailingButtonContentPadding();
method public float getTrailingIconSize();
- method public androidx.compose.foundation.shape.RoundedCornerShape leadingButtonShape(optional androidx.compose.foundation.shape.CornerSize endCornerSize);
- method public androidx.compose.foundation.shape.RoundedCornerShape trailingButtonShape(optional androidx.compose.foundation.shape.CornerSize startCornerSize);
+ method public androidx.compose.material3.SplitButtonShapes leadingButtonShapes(optional androidx.compose.foundation.shape.CornerSize endCornerSize);
+ method public androidx.compose.material3.SplitButtonShapes trailingButtonShapes(optional androidx.compose.foundation.shape.CornerSize startCornerSize);
property public final androidx.compose.foundation.shape.CornerSize InnerCornerSize;
property public final androidx.compose.foundation.layout.PaddingValues LeadingButtonContentPadding;
property public final float LeadingIconSize;
@@ -2122,6 +2149,20 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TonalSplitButton(kotlin.jvm.functions.Function0<kotlin.Unit> onLeadingButtonClick, kotlin.jvm.functions.Function0<kotlin.Unit> onTrailingButtonClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> leadingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> trailingContent, boolean checked, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.shape.CornerSize innerCornerSize, optional float spacing);
}
+ public final class SplitButtonShapes {
+ ctor public SplitButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape? pressedShape, androidx.compose.ui.graphics.Shape? checkedShape);
+ method public androidx.compose.ui.graphics.Shape component1();
+ method public androidx.compose.ui.graphics.Shape? component2();
+ method public androidx.compose.ui.graphics.Shape? component3();
+ method public androidx.compose.material3.SplitButtonShapes copy(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape? pressedShape, androidx.compose.ui.graphics.Shape? checkedShape);
+ method public androidx.compose.ui.graphics.Shape? getCheckedShape();
+ method public androidx.compose.ui.graphics.Shape? getPressedShape();
+ method public androidx.compose.ui.graphics.Shape getShape();
+ property public final androidx.compose.ui.graphics.Shape? checkedShape;
+ property public final androidx.compose.ui.graphics.Shape? pressedShape;
+ property public final androidx.compose.ui.graphics.Shape shape;
+ }
+
public final class SuggestionChipDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.ChipColors elevatedSuggestionChipColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ChipColors elevatedSuggestionChipColors(optional long containerColor, optional long labelColor, optional long iconContentColor, optional long disabledContainerColor, optional long disabledLabelColor, optional long disabledIconContentColor);
@@ -2590,7 +2631,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getTonalShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
- method public androidx.compose.material3.ButtonShapes shapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors tonalToggleButtonColors();
@@ -2792,39 +2833,71 @@
}
@androidx.compose.runtime.Immutable public final class Typography {
- ctor public Typography();
+ ctor @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public Typography();
ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+ ctor @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle displayLargeEmphasized, optional androidx.compose.ui.text.TextStyle displayMediumEmphasized, optional androidx.compose.ui.text.TextStyle displaySmallEmphasized, optional androidx.compose.ui.text.TextStyle headlineLargeEmphasized, optional androidx.compose.ui.text.TextStyle headlineMediumEmphasized, optional androidx.compose.ui.text.TextStyle headlineSmallEmphasized, optional androidx.compose.ui.text.TextStyle titleLargeEmphasized, optional androidx.compose.ui.text.TextStyle titleMediumEmphasized, optional androidx.compose.ui.text.TextStyle titleSmallEmphasized, optional androidx.compose.ui.text.TextStyle bodyLargeEmphasized, optional androidx.compose.ui.text.TextStyle bodyMediumEmphasized, optional androidx.compose.ui.text.TextStyle bodySmallEmphasized, optional androidx.compose.ui.text.TextStyle labelLargeEmphasized, optional androidx.compose.ui.text.TextStyle labelMediumEmphasized, optional androidx.compose.ui.text.TextStyle labelSmallEmphasized);
method public androidx.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle displayLargeEmphasized, optional androidx.compose.ui.text.TextStyle displayMediumEmphasized, optional androidx.compose.ui.text.TextStyle displaySmallEmphasized, optional androidx.compose.ui.text.TextStyle headlineLargeEmphasized, optional androidx.compose.ui.text.TextStyle headlineMediumEmphasized, optional androidx.compose.ui.text.TextStyle headlineSmallEmphasized, optional androidx.compose.ui.text.TextStyle titleLargeEmphasized, optional androidx.compose.ui.text.TextStyle titleMediumEmphasized, optional androidx.compose.ui.text.TextStyle titleSmallEmphasized, optional androidx.compose.ui.text.TextStyle bodyLargeEmphasized, optional androidx.compose.ui.text.TextStyle bodyMediumEmphasized, optional androidx.compose.ui.text.TextStyle bodySmallEmphasized, optional androidx.compose.ui.text.TextStyle labelLargeEmphasized, optional androidx.compose.ui.text.TextStyle labelMediumEmphasized, optional androidx.compose.ui.text.TextStyle labelSmallEmphasized);
method public androidx.compose.ui.text.TextStyle getBodyLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getBodyLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getBodyMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getBodyMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getBodySmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getBodySmallEmphasized();
method public androidx.compose.ui.text.TextStyle getDisplayLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getDisplayLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getDisplayMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getDisplayMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getDisplaySmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getDisplaySmallEmphasized();
method public androidx.compose.ui.text.TextStyle getHeadlineLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getHeadlineLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getHeadlineMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getHeadlineMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getHeadlineSmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getHeadlineSmallEmphasized();
method public androidx.compose.ui.text.TextStyle getLabelLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getLabelLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getLabelMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getLabelMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getLabelSmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getLabelSmallEmphasized();
method public androidx.compose.ui.text.TextStyle getTitleLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getTitleLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getTitleMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getTitleMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getTitleSmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getTitleSmallEmphasized();
property public final androidx.compose.ui.text.TextStyle bodyLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle bodyLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle bodyMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle bodyMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle bodySmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle bodySmallEmphasized;
property public final androidx.compose.ui.text.TextStyle displayLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle displayLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle displayMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle displayMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle displaySmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle displaySmallEmphasized;
property public final androidx.compose.ui.text.TextStyle headlineLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle headlineLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle headlineMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle headlineMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle headlineSmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle headlineSmallEmphasized;
property public final androidx.compose.ui.text.TextStyle labelLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle labelLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle labelMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle labelMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle labelSmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle labelSmallEmphasized;
property public final androidx.compose.ui.text.TextStyle titleLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle titleLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle titleMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle titleMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle titleSmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle titleSmallEmphasized;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WavyProgressIndicatorDefaults {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index a9ee23e..f6366cc 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -271,10 +271,6 @@
public final class ButtonShapes {
ctor public ButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
- method public androidx.compose.ui.graphics.Shape component1();
- method public androidx.compose.ui.graphics.Shape component2();
- method public androidx.compose.ui.graphics.Shape component3();
- method public androidx.compose.material3.ButtonShapes copy(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
method public androidx.compose.ui.graphics.Shape getCheckedShape();
method public androidx.compose.ui.graphics.Shape getPressedShape();
method public androidx.compose.ui.graphics.Shape getShape();
@@ -975,53 +971,70 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFilledShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getLargeIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getLargePressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getLargeRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getLargeSquareShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getMediumIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMediumPressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMediumRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getMediumSquareShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getOutlinedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getSmallIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallPressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallSquareShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getStandardShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getXLargeIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXLargePressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXLargeRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXLargeSquareShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public float getXSmallIconSize();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallPressedShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallRoundShape();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getXSmallSquareShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonLocalContentColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonLocalContentColors();
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long largeContainerSize(optional int widthOption);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long mediumContainerSize(optional int widthOption);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconButtonLocalContentColorBorder(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonLocalContentColors();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonLocalContentColorBorder(boolean enabled, boolean checked);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonLocalContentColors();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonShapes shapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long smallContainerSize(optional int widthOption);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long xLargeContainerSize(optional int widthOption);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public long xSmallContainerSize(optional int widthOption);
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float largeIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largePressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeSquareShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float mediumIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape mediumPressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape mediumRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape mediumSquareShape;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float smallIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallPressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallSquareShape;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape standardShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float xLargeIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xLargePressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xLargeRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xLargeSquareShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final float xSmallIconSize;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xSmallPressedShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xSmallRoundShape;
property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape xSmallSquareShape;
field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
@@ -1042,17 +1055,31 @@
public final class IconButtonKt {
method @androidx.compose.runtime.Composable public static void FilledIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FilledIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledTonalIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void FilledTonalIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledTonalIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void OutlinedIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, androidx.compose.material3.IconButtonShapes shapes, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.IconToggleButtonColors colors, 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 public static void OutlinedIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class IconButtonShapes {
+ ctor public IconButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
+ method public androidx.compose.ui.graphics.Shape getCheckedShape();
+ method public androidx.compose.ui.graphics.Shape getPressedShape();
+ method public androidx.compose.ui.graphics.Shape getShape();
+ property public final androidx.compose.ui.graphics.Shape checkedShape;
+ property public final androidx.compose.ui.graphics.Shape pressedShape;
+ property public final androidx.compose.ui.graphics.Shape shape;
+ }
+
public final class IconKt {
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.painter.Painter painter, androidx.compose.ui.graphics.ColorProducer? tint, String? contentDescription, optional androidx.compose.ui.Modifier modifier);
@@ -1253,6 +1280,7 @@
}
public final class MaterialShapesKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.Morph, float progress, optional androidx.compose.ui.graphics.Path path, optional int startAngle);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Path toPath(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.Shape toShape(androidx.graphics.shapes.RoundedPolygon, optional int startAngle);
}
@@ -2092,9 +2120,8 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class SplitButtonDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void AnimatedTrailingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean checked, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.shape.CornerSize startCornerSize, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void LeadingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void TrailingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional boolean enabled, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void LeadingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SplitButtonShapes shapes, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public void TrailingButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, boolean checked, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SplitButtonShapes shapes, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method public androidx.compose.foundation.shape.CornerSize getInnerCornerSize();
method public androidx.compose.foundation.layout.PaddingValues getLeadingButtonContentPadding();
method public float getLeadingIconSize();
@@ -2102,8 +2129,8 @@
method public float getSpacing();
method public androidx.compose.foundation.layout.PaddingValues getTrailingButtonContentPadding();
method public float getTrailingIconSize();
- method public androidx.compose.foundation.shape.RoundedCornerShape leadingButtonShape(optional androidx.compose.foundation.shape.CornerSize endCornerSize);
- method public androidx.compose.foundation.shape.RoundedCornerShape trailingButtonShape(optional androidx.compose.foundation.shape.CornerSize startCornerSize);
+ method public androidx.compose.material3.SplitButtonShapes leadingButtonShapes(optional androidx.compose.foundation.shape.CornerSize endCornerSize);
+ method public androidx.compose.material3.SplitButtonShapes trailingButtonShapes(optional androidx.compose.foundation.shape.CornerSize startCornerSize);
property public final androidx.compose.foundation.shape.CornerSize InnerCornerSize;
property public final androidx.compose.foundation.layout.PaddingValues LeadingButtonContentPadding;
property public final float LeadingIconSize;
@@ -2122,6 +2149,20 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void TonalSplitButton(kotlin.jvm.functions.Function0<kotlin.Unit> onLeadingButtonClick, kotlin.jvm.functions.Function0<kotlin.Unit> onTrailingButtonClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> leadingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> trailingContent, boolean checked, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.shape.CornerSize innerCornerSize, optional float spacing);
}
+ public final class SplitButtonShapes {
+ ctor public SplitButtonShapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape? pressedShape, androidx.compose.ui.graphics.Shape? checkedShape);
+ method public androidx.compose.ui.graphics.Shape component1();
+ method public androidx.compose.ui.graphics.Shape? component2();
+ method public androidx.compose.ui.graphics.Shape? component3();
+ method public androidx.compose.material3.SplitButtonShapes copy(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape? pressedShape, androidx.compose.ui.graphics.Shape? checkedShape);
+ method public androidx.compose.ui.graphics.Shape? getCheckedShape();
+ method public androidx.compose.ui.graphics.Shape? getPressedShape();
+ method public androidx.compose.ui.graphics.Shape getShape();
+ property public final androidx.compose.ui.graphics.Shape? checkedShape;
+ property public final androidx.compose.ui.graphics.Shape? pressedShape;
+ property public final androidx.compose.ui.graphics.Shape shape;
+ }
+
public final class SuggestionChipDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.ChipColors elevatedSuggestionChipColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ChipColors elevatedSuggestionChipColors(optional long containerColor, optional long labelColor, optional long iconContentColor, optional long disabledContainerColor, optional long disabledLabelColor, optional long disabledIconContentColor);
@@ -2590,7 +2631,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getTonalShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors outlinedToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
- method public androidx.compose.material3.ButtonShapes shapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonShapes shapes(androidx.compose.ui.graphics.Shape shape, androidx.compose.ui.graphics.Shape pressedShape, androidx.compose.ui.graphics.Shape checkedShape);
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors toggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.ToggleButtonColors tonalToggleButtonColors();
@@ -2792,39 +2833,71 @@
}
@androidx.compose.runtime.Immutable public final class Typography {
- ctor public Typography();
+ ctor @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public Typography();
ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+ ctor @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle displayLargeEmphasized, optional androidx.compose.ui.text.TextStyle displayMediumEmphasized, optional androidx.compose.ui.text.TextStyle displaySmallEmphasized, optional androidx.compose.ui.text.TextStyle headlineLargeEmphasized, optional androidx.compose.ui.text.TextStyle headlineMediumEmphasized, optional androidx.compose.ui.text.TextStyle headlineSmallEmphasized, optional androidx.compose.ui.text.TextStyle titleLargeEmphasized, optional androidx.compose.ui.text.TextStyle titleMediumEmphasized, optional androidx.compose.ui.text.TextStyle titleSmallEmphasized, optional androidx.compose.ui.text.TextStyle bodyLargeEmphasized, optional androidx.compose.ui.text.TextStyle bodyMediumEmphasized, optional androidx.compose.ui.text.TextStyle bodySmallEmphasized, optional androidx.compose.ui.text.TextStyle labelLargeEmphasized, optional androidx.compose.ui.text.TextStyle labelMediumEmphasized, optional androidx.compose.ui.text.TextStyle labelSmallEmphasized);
method public androidx.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle displayLargeEmphasized, optional androidx.compose.ui.text.TextStyle displayMediumEmphasized, optional androidx.compose.ui.text.TextStyle displaySmallEmphasized, optional androidx.compose.ui.text.TextStyle headlineLargeEmphasized, optional androidx.compose.ui.text.TextStyle headlineMediumEmphasized, optional androidx.compose.ui.text.TextStyle headlineSmallEmphasized, optional androidx.compose.ui.text.TextStyle titleLargeEmphasized, optional androidx.compose.ui.text.TextStyle titleMediumEmphasized, optional androidx.compose.ui.text.TextStyle titleSmallEmphasized, optional androidx.compose.ui.text.TextStyle bodyLargeEmphasized, optional androidx.compose.ui.text.TextStyle bodyMediumEmphasized, optional androidx.compose.ui.text.TextStyle bodySmallEmphasized, optional androidx.compose.ui.text.TextStyle labelLargeEmphasized, optional androidx.compose.ui.text.TextStyle labelMediumEmphasized, optional androidx.compose.ui.text.TextStyle labelSmallEmphasized);
method public androidx.compose.ui.text.TextStyle getBodyLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getBodyLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getBodyMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getBodyMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getBodySmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getBodySmallEmphasized();
method public androidx.compose.ui.text.TextStyle getDisplayLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getDisplayLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getDisplayMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getDisplayMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getDisplaySmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getDisplaySmallEmphasized();
method public androidx.compose.ui.text.TextStyle getHeadlineLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getHeadlineLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getHeadlineMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getHeadlineMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getHeadlineSmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getHeadlineSmallEmphasized();
method public androidx.compose.ui.text.TextStyle getLabelLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getLabelLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getLabelMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getLabelMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getLabelSmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getLabelSmallEmphasized();
method public androidx.compose.ui.text.TextStyle getTitleLarge();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getTitleLargeEmphasized();
method public androidx.compose.ui.text.TextStyle getTitleMedium();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getTitleMediumEmphasized();
method public androidx.compose.ui.text.TextStyle getTitleSmall();
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public androidx.compose.ui.text.TextStyle getTitleSmallEmphasized();
property public final androidx.compose.ui.text.TextStyle bodyLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle bodyLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle bodyMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle bodyMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle bodySmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle bodySmallEmphasized;
property public final androidx.compose.ui.text.TextStyle displayLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle displayLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle displayMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle displayMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle displaySmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle displaySmallEmphasized;
property public final androidx.compose.ui.text.TextStyle headlineLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle headlineLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle headlineMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle headlineMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle headlineSmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle headlineSmallEmphasized;
property public final androidx.compose.ui.text.TextStyle labelLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle labelLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle labelMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle labelMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle labelSmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle labelSmallEmphasized;
property public final androidx.compose.ui.text.TextStyle titleLarge;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle titleLargeEmphasized;
property public final androidx.compose.ui.text.TextStyle titleMedium;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle titleMediumEmphasized;
property public final androidx.compose.ui.text.TextStyle titleSmall;
+ property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final androidx.compose.ui.text.TextStyle titleSmallEmphasized;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WavyProgressIndicatorDefaults {
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/IconButtonDemos.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/IconButtonDemos.kt
index e3815ad..c4350f2 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/IconButtonDemos.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/IconButtonDemos.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -34,11 +35,14 @@
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledIconToggleButton
+import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconButtonDefaults.IconButtonWidthOption.Companion.Narrow
import androidx.compose.material3.IconButtonDefaults.IconButtonWidthOption.Companion.Wide
+import androidx.compose.material3.IconButtonShapes
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedIconToggleButton
@@ -46,6 +50,8 @@
import androidx.compose.material3.minimumInteractiveComponentSize
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.Alignment
import androidx.compose.ui.Modifier
@@ -318,10 +324,7 @@
// xsmall round icon button
OutlinedIconButton(
onClick = { /* doSomething() */ },
- modifier =
- Modifier
- // .minimumInteractiveComponentSize()
- .size(IconButtonDefaults.xSmallContainerSize()),
+ modifier = Modifier.size(IconButtonDefaults.xSmallContainerSize()),
shape = IconButtonDefaults.xSmallRoundShape
) {
Icon(
@@ -334,10 +337,7 @@
// Small round icon button
OutlinedIconButton(
onClick = { /* doSomething() */ },
- modifier =
- Modifier
- // .minimumInteractiveComponentSize()
- .size(IconButtonDefaults.smallContainerSize()),
+ modifier = Modifier.size(IconButtonDefaults.smallContainerSize()),
shape = IconButtonDefaults.smallRoundShape
) {
Icon(
@@ -482,12 +482,13 @@
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun IconToggleButtonsDemo() {
+fun IconButtonAndToggleButtonsDemo() {
Column {
val rowScrollState = rememberScrollState()
val padding = 16.dp
- // unselected round row
+
Row(
modifier =
Modifier.height(150.dp)
@@ -502,7 +503,7 @@
Text("Outline")
Text("Standard")
}
- // unselected round row
+ // icon buttons
Row(
modifier =
Modifier.height(150.dp)
@@ -511,32 +512,100 @@
horizontalArrangement = Arrangement.spacedBy(padding),
verticalAlignment = Alignment.CenterVertically
) {
- Text("Unselected")
+ Spacer(Modifier.width(76.dp))
- FilledIconToggleButton(checked = false, onCheckedChange = { /* change the state */ }) {
+ FilledIconButton(onClick = {}) {
+ Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
+ }
+
+ FilledTonalIconButton(onClick = {}) {
+ Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
+ }
+
+ OutlinedIconButton(onClick = {}) {
+ Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
+ }
+
+ IconButton(onClick = {}) {
+ Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
+ }
+ }
+
+ // unselected icon toggle buttons
+ Row(
+ modifier =
+ Modifier.height(150.dp)
+ .horizontalScroll(rowScrollState)
+ .padding(horizontal = padding),
+ horizontalArrangement = Arrangement.spacedBy(padding),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ var checked by remember { mutableStateOf(false) }
+
+ Text(
+ text =
+ if (!checked) {
+ "Unselected"
+ } else {
+ "Selected"
+ },
+ modifier = Modifier.defaultMinSize(minWidth = 76.dp),
+ )
+
+ FilledIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
}
FilledTonalIconToggleButton(
- checked = false,
- onCheckedChange = { /* change the state */ }
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
) {
Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
}
OutlinedIconToggleButton(
- checked = false,
- onCheckedChange = { /* change the state */ }
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
) {
Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
}
- IconToggleButton(checked = false, onCheckedChange = { /* change the state */ }) {
+ IconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
}
}
- // unselected round row
+ // selected icon toggle buttons
Row(
modifier =
Modifier.height(150.dp)
@@ -545,23 +614,67 @@
horizontalArrangement = Arrangement.spacedBy(padding),
verticalAlignment = Alignment.CenterVertically
) {
- Text("Selected")
- FilledIconToggleButton(checked = true, onCheckedChange = { /* change the state */ }) {
- Icon(Icons.Filled.Edit, contentDescription = "Localized description")
- }
+ var checked by remember { mutableStateOf(true) }
- FilledTonalIconToggleButton(
- checked = true,
- onCheckedChange = { /* change the state */ }
+ Text(
+ text =
+ if (!checked) {
+ "Unselected"
+ } else {
+ "Selected"
+ },
+ modifier = Modifier.defaultMinSize(minWidth = 76.dp)
+ )
+
+ FilledIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
) {
Icon(Icons.Filled.Edit, contentDescription = "Localized description")
}
- OutlinedIconToggleButton(checked = true, onCheckedChange = { /* change the state */ }) {
+ FilledTonalIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
Icon(Icons.Filled.Edit, contentDescription = "Localized description")
}
- IconToggleButton(checked = true, onCheckedChange = { /* change the state */ }) {
+ OutlinedIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+ }
+
+ IconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
Icon(Icons.Filled.Edit, contentDescription = "Localized description")
}
}
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/Material3Demos.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/Material3Demos.kt
index d9ba3c0..a84867f 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/Material3Demos.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/Material3Demos.kt
@@ -26,7 +26,6 @@
ComposableDemo("Color Scheme") { ColorSchemeDemo() },
ComposableDemo("FAB Menu") { FloatingActionButtonMenuDemo() },
ComposableDemo("Pull To Refresh") { PullToRefreshDemo() },
- ComposableDemo("Shape") { ShapeDemo() },
ComposableDemo("Swipe To Dismiss") { SwipeToDismissDemo() },
ComposableDemo("Tooltip") { TooltipDemo() },
ComposableDemo("Text fields") { MaterialTextFieldDemo() },
@@ -35,8 +34,18 @@
listOf(
ComposableDemo("Sizes") { IconButtonMeasurementsDemo() },
ComposableDemo("Corners") { IconButtonCornerRadiusDemo() },
- ComposableDemo("Icon toggle buttons") { IconToggleButtonsDemo() },
+ ComposableDemo("Icon button & icon toggle buttons") {
+ IconButtonAndToggleButtonsDemo()
+ },
)
),
+ DemoCategory(
+ "Shapes",
+ listOf(
+ ComposableDemo("Shape") { ShapeDemo() },
+ ComposableDemo("Material Shape") { MaterialShapeDemo() },
+ ComposableDemo("Material Shape Morphing") { MaterialShapeMorphDemo() },
+ )
+ )
),
)
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ShapeDemos.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ShapeDemos.kt
index 59a1689..372e058 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ShapeDemos.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ShapeDemos.kt
@@ -16,21 +16,52 @@
package androidx.compose.material3.demos
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccessAlarms
+import androidx.compose.material.icons.outlined.AccessibilityNew
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialShapes
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.material3.toPath
+import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.graphics.shapes.Morph
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -61,3 +92,102 @@
Button(onClick = {}, shape = CircleShape) { Text("Full") }
}
}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun MaterialShapeDemo() {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val shape = MaterialShapes.Clover4Leaf.toShape()
+
+ // A Material Button with a shape that is provided from the MaterialShapes
+ Button(
+ onClick = { /* on-click*/ },
+ modifier = Modifier.requiredSize(48.dp),
+ shape = shape,
+ border = BorderStroke(width = 2.dp, MaterialTheme.colorScheme.error)
+ ) {
+ Icon(
+ Icons.Outlined.AccessibilityNew,
+ modifier = Modifier.requiredSize(24.dp),
+ contentDescription = "Localized description",
+ )
+ }
+
+ // A basic Box with a shape that is provided from the MaterialShapes
+ Box(
+ modifier =
+ Modifier.requiredSize(64.dp)
+ .border(BorderStroke(width = 2.dp, Color.Red), shape)
+ .clip(shape)
+ .background(MaterialTheme.colorScheme.primary),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ Icons.Outlined.AccessibilityNew,
+ contentDescription = "Localized description",
+ modifier = Modifier.requiredSize(36.dp),
+ tint = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+
+ // Path drawing on Canvas with a shape that is provided from the MaterialShapes
+ val shapePath = MaterialShapes.Clover4Leaf.toPath()
+ val workPath = Path()
+ val color = MaterialTheme.colorScheme.outline
+ val borderWidth = with(LocalDensity.current) { 2.dp.toPx() }
+ Canvas(Modifier.requiredSize(64.dp)) {
+ // The path is normalized, so we need to scale it to the size of the canvas.
+ workPath.rewind()
+ workPath.addPath(shapePath)
+ workPath.transform(matrix = Matrix().apply { scale(size.width, size.height) })
+ drawPath(workPath, color = color, style = Stroke(width = borderWidth))
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun MaterialShapeMorphDemo() {
+ val morph = remember { Morph(MaterialShapes.Circle, MaterialShapes.Cookie9Sided) }
+ val interactionSource = remember { MutableInteractionSource() }
+ val isPressed by interactionSource.collectIsPressedAsState()
+ val animatedProgress =
+ animateFloatAsState(
+ targetValue = if (isPressed) 1f else 0f,
+ label = "progress",
+ animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec()
+ )
+ val morphShape = remember {
+ object : Shape {
+ private val path = Path()
+ private val matrix = Matrix()
+
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ matrix.reset()
+ matrix.scale(size.width, size.height)
+ morph.toPath(animatedProgress.value, path)
+ path.transform(matrix)
+ return Outline.Generic(path)
+ }
+ }
+ }
+ Button(
+ onClick = { /* on-click*/ },
+ modifier = Modifier.requiredSize(48.dp),
+ shape = morphShape,
+ interactionSource = interactionSource
+ ) {
+ Icon(
+ Icons.Outlined.AccessAlarms,
+ modifier = Modifier.requiredSize(24.dp),
+ contentDescription = "Localized description",
+ )
+ }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
index f159640..14ccfa7 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
@@ -29,6 +29,7 @@
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.IconButtonShapes
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedIconToggleButton
@@ -48,7 +49,7 @@
@Composable
fun IconButtonSample() {
IconButton(onClick = { /* doSomething() */ }) {
- Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
}
}
@@ -70,7 +71,7 @@
shape = IconButtonDefaults.xSmallSquareShape
) {
Icon(
- Icons.Outlined.Lock,
+ Icons.Filled.Lock,
contentDescription = "Localized description",
modifier = Modifier.size(IconButtonDefaults.xSmallIconSize)
)
@@ -93,7 +94,7 @@
shape = IconButtonDefaults.mediumRoundShape
) {
Icon(
- Icons.Outlined.Lock,
+ Icons.Filled.Lock,
contentDescription = "Localized description",
modifier = Modifier.size(IconButtonDefaults.mediumIconSize)
)
@@ -111,7 +112,7 @@
shape = IconButtonDefaults.largeRoundShape
) {
Icon(
- Icons.Outlined.Lock,
+ Icons.Filled.Lock,
contentDescription = "Localized description",
modifier = Modifier.size(IconButtonDefaults.largeIconSize)
)
@@ -124,7 +125,7 @@
fun TintedIconButtonSample() {
IconButton(onClick = { /* doSomething() */ }) {
Icon(
- rememberVectorPainter(image = Icons.Outlined.Lock),
+ rememberVectorPainter(image = Icons.Filled.Lock),
contentDescription = "Localized description",
tint = Color.Red
)
@@ -148,9 +149,23 @@
@Preview
@Sampled
@Composable
+fun IconToggleButtonWithAnimatedShapeSample() {
+ var checked by remember { mutableStateOf(false) }
+ IconToggleButton(checked = checked, onCheckedChange = { checked = it }) {
+ if (checked) {
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
+ } else {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ }
+}
+
+@Preview
+@Sampled
+@Composable
fun FilledIconButtonSample() {
FilledIconButton(onClick = { /* doSomething() */ }) {
- Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
}
}
@@ -168,12 +183,36 @@
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun FilledIconToggleButtonWithAnimatedShapeSample() {
+ var checked by remember { mutableStateOf(false) }
+ FilledIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape,
+ )
+ ) {
+ if (checked) {
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
+ } else {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ }
+}
+
@Preview
@Sampled
@Composable
fun FilledTonalIconButtonSample() {
FilledTonalIconButton(onClick = { /* doSomething() */ }) {
- Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
}
}
@@ -191,12 +230,36 @@
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun FilledTonalIconToggleButtonWithAnimatedShapeSample() {
+ var checked by remember { mutableStateOf(false) }
+ FilledTonalIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape,
+ )
+ ) {
+ if (checked) {
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
+ } else {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ }
+}
+
@Preview
@Sampled
@Composable
fun OutlinedIconButtonSample() {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
}
}
@@ -213,3 +276,27 @@
}
}
}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun OutlinedIconToggleButtonWithAnimatedShapeSample() {
+ var checked by remember { mutableStateOf(false) }
+ OutlinedIconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape,
+ )
+ ) {
+ if (checked) {
+ Icon(Icons.Filled.Lock, contentDescription = "Localized description")
+ } else {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt
index 13c3a29..3342355 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SplitButtonSamples.kt
@@ -21,8 +21,8 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Edit
-import androidx.compose.material.icons.outlined.KeyboardArrowDown
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedSplitButton
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -58,7 +58,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description",
)
@@ -67,7 +67,7 @@
}
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
onClick = { checked = !checked },
checked = checked,
modifier =
@@ -82,7 +82,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
@@ -107,7 +107,7 @@
onTrailingButtonClick = { checked = !checked },
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description"
)
@@ -121,7 +121,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
@@ -145,7 +145,7 @@
onTrailingButtonClick = { checked = !checked },
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description"
)
@@ -159,7 +159,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
@@ -183,7 +183,7 @@
onTrailingButtonClick = { checked = !checked },
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description"
)
@@ -197,7 +197,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
@@ -221,7 +221,7 @@
onTrailingButtonClick = { checked = !checked },
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description"
)
@@ -235,7 +235,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
@@ -262,7 +262,7 @@
}
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
onClick = { checked = !checked },
checked = checked,
) {
@@ -272,7 +272,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
@@ -297,14 +297,14 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
}
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
onClick = { checked = !checked },
checked = checked,
) {
@@ -314,7 +314,7 @@
label = "Trailing Icon Rotation"
)
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = rotation
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt
index 7a40103..1a5c3a4b 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ToggleButtonSamples.kt
@@ -104,7 +104,7 @@
@Composable
fun ToggleButtonWithIconSample() {
var checked by remember { mutableStateOf(false) }
- ToggleButton(checked = checked, onCheckedChange = { checked = it }) {
+ ElevatedToggleButton(checked = checked, onCheckedChange = { checked = it }) {
Icon(
if (checked) Icons.Filled.Favorite else Icons.Outlined.Favorite,
contentDescription = "Localized description",
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
index 0c8bf40..0bd5601 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
@@ -24,7 +24,6 @@
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material3.tokens.TextButtonTokens
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -91,7 +90,9 @@
TextButton(onClick = { /* doSomething() */ }) {
Text("Confirm")
buttonContentColor = LocalContentColor.current
- expectedButtonContentColor = TextButtonTokens.LabelColor.value
+ // TODO change this back to the TextButtonTokens.LabelColor once the tokens
+ // are updated
+ expectedButtonContentColor = MaterialTheme.colorScheme.primary
}
},
containerColor = Color.Yellow,
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
index e7f9bd2..9b52bf3 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ButtonTest.kt
@@ -415,7 +415,9 @@
.isEqualTo(
ButtonColors(
containerColor = Color.Transparent,
- contentColor = TextButtonTokens.LabelColor.value,
+ // TODO change this back to the TextButtonTokens.LabelColor once the tokens
+ // are updated
+ contentColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = Color.Transparent,
disabledContentColor =
TextButtonTokens.DisabledLabelColor.value.copy(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
index 7257d71..1cf7f4b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -32,9 +32,9 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.tokens.ExtendedFabPrimaryTokens
-import androidx.compose.material3.tokens.FabPrimaryLargeTokens
-import androidx.compose.material3.tokens.FabPrimarySmallTokens
-import androidx.compose.material3.tokens.FabPrimaryTokens
+import androidx.compose.material3.tokens.FabBaselineTokens
+import androidx.compose.material3.tokens.FabLargeTokens
+import androidx.compose.material3.tokens.FabSmallTokens
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@@ -128,12 +128,12 @@
Icon(Icons.Filled.Favorite, null, modifier = Modifier.testTag("icon"))
}
}
- .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
+ .assertIsSquareWithSize(FabBaselineTokens.ContainerHeight)
rule
.onNodeWithTag("icon", useUnmergedTree = true)
- .assertHeightIsEqualTo(FabPrimaryTokens.IconSize)
- .assertWidthIsEqualTo(FabPrimaryTokens.IconSize)
+ .assertHeightIsEqualTo(FabBaselineTokens.IconSize)
+ .assertWidthIsEqualTo(FabBaselineTokens.IconSize)
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -148,12 +148,12 @@
}
}
// Expecting the size to be equal to the token size.
- .assertIsSquareWithSize(FabPrimarySmallTokens.ContainerHeight)
+ .assertIsSquareWithSize(FabSmallTokens.ContainerHeight)
rule
.onNodeWithTag("icon", useUnmergedTree = true)
- .assertHeightIsEqualTo(FabPrimarySmallTokens.IconSize)
- .assertWidthIsEqualTo(FabPrimarySmallTokens.IconSize)
+ .assertHeightIsEqualTo(FabSmallTokens.IconSize)
+ .assertWidthIsEqualTo(FabSmallTokens.IconSize)
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -184,7 +184,7 @@
)
}
}
- .assertIsSquareWithSize(FabPrimaryLargeTokens.ContainerHeight)
+ .assertIsSquareWithSize(FabLargeTokens.ContainerHeight)
rule
.onNodeWithTag("icon", useUnmergedTree = true)
@@ -206,7 +206,7 @@
rule
.onNodeWithTag("FAB")
.assertHeightIsEqualTo(ExtendedFabPrimaryTokens.ContainerHeight)
- .assertWidthIsAtLeast(FabPrimaryTokens.ContainerHeight)
+ .assertWidthIsAtLeast(FabBaselineTokens.ContainerHeight)
}
@Test
@@ -468,7 +468,7 @@
)
}
- rule.onNodeWithTag("FAB").assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
+ rule.onNodeWithTag("FAB").assertIsSquareWithSize(FabBaselineTokens.ContainerHeight)
rule
.onNodeWithTag("icon", useUnmergedTree = true)
@@ -504,9 +504,9 @@
rule
.onNodeWithTag("FAB")
- .assertIsSquareWithSize(FabPrimaryTokens.ContainerHeight)
- .assertHeightIsEqualTo(FabPrimaryTokens.ContainerHeight)
- .assertWidthIsEqualTo(FabPrimaryTokens.ContainerWidth)
+ .assertIsSquareWithSize(FabBaselineTokens.ContainerHeight)
+ .assertHeightIsEqualTo(FabBaselineTokens.ContainerHeight)
+ .assertWidthIsEqualTo(FabBaselineTokens.ContainerWidth)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -783,7 +783,7 @@
shapeSizeY = with(rule.density) { 60.dp.toPx() },
centerX = with(rule.density) { 70.dp.toPx() },
centerY = with(rule.density) { 70.dp.toPx() },
- shapeOverlapPixelCount = with(rule.density) { 2.dp.toPx() }
+ shapeOverlapPixelCount = with(rule.density) { 3.dp.toPx() }
)
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
index d7ae25d..5eaa44a 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonTest.kt
@@ -306,10 +306,10 @@
}
@Test
- fun iconButton_defaultColors() {
+ fun iconButton_defaultLocalContentColors() {
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalContentColor provides Color.Blue) {
- Truth.assertThat(IconButtonDefaults.iconButtonColors())
+ Truth.assertThat(IconButtonDefaults.iconButtonLocalContentColors())
.isEqualTo(
IconButtonColors(
containerColor = Color.Transparent,
@@ -324,15 +324,35 @@
}
@Test
+ fun iconButton_defaultColors() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Truth.assertThat(IconButtonDefaults.iconButtonColors())
+ .isEqualTo(
+ IconButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = StandardIconButtonTokens.Color.value,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor =
+ StandardIconButtonTokens.DisabledColor.value.copy(
+ alpha = StandardIconButtonTokens.DisabledOpacity
+ )
+ )
+ )
+ }
+ }
+
+ @Test
fun iconButtonColors_localContentColor() {
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalContentColor provides Color.Blue) {
- val colors = IconButtonDefaults.iconButtonColors()
+ val colors = IconButtonDefaults.iconButtonLocalContentColors()
assert(colors.contentColor == Color.Blue)
}
CompositionLocalProvider(LocalContentColor provides Color.Red) {
- val colors = IconButtonDefaults.iconButtonColors(containerColor = Color.Green)
+ val colors =
+ IconButtonDefaults.iconButtonLocalContentColors()
+ .copy(containerColor = Color.Green)
assert(colors.containerColor == Color.Green)
assert(colors.contentColor == Color.Red)
}
@@ -340,10 +360,10 @@
}
@Test
- fun iconButtonColors_customValues() {
+ fun iconButtonColors_customValues_useLocalContentColor() {
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalContentColor provides Color.Blue) {
- val colors = IconButtonDefaults.iconButtonColors()
+ val colors = IconButtonDefaults.iconButtonLocalContentColors()
assert(colors.contentColor == Color.Blue)
assert(
colors.disabledContentColor ==
@@ -368,6 +388,25 @@
}
@Test
+ fun iconButtonColors_customValues() {
+ rule.setMaterialContent(lightColorScheme()) {
+ CompositionLocalProvider(LocalContentColor provides Color.Red) {
+ val colors =
+ IconButtonDefaults.iconButtonColors(
+ containerColor = Color.Blue,
+ contentColor = Color.Green
+ )
+ assert(colors.containerColor == Color.Blue)
+ assert(colors.contentColor == Color.Green)
+ assert(
+ colors.disabledContentColor ==
+ Color.Green.copy(StandardIconButtonTokens.DisabledOpacity)
+ )
+ }
+ }
+ }
+
+ @Test
fun iconButtonColors_copy() {
rule.setMaterialContent(lightColorScheme()) {
val colors = IconButtonDefaults.iconButtonColors().copy()
@@ -501,10 +540,10 @@
}
@Test
- fun iconToggleButton_defaultColors() {
+ fun iconToggleButton_defaultLocalContentColors() {
rule.setMaterialContent(lightColorScheme()) {
val localContentColor = LocalContentColor.current
- Truth.assertThat(IconButtonDefaults.iconToggleButtonColors())
+ Truth.assertThat(IconButtonDefaults.iconToggleButtonLocalContentColors())
.isEqualTo(
IconToggleButtonColors(
containerColor = Color.Transparent,
@@ -522,6 +561,26 @@
}
@Test
+ fun iconToggleButton_defaultColors() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Truth.assertThat(IconButtonDefaults.iconToggleButtonColors())
+ .isEqualTo(
+ IconToggleButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = StandardIconButtonTokens.UnselectedColor.value,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor =
+ StandardIconButtonTokens.DisabledColor.value.copy(
+ alpha = StandardIconButtonTokens.DisabledOpacity
+ ),
+ checkedContainerColor = Color.Transparent,
+ checkedContentColor = StandardIconButtonTokens.SelectedColor.value
+ )
+ )
+ }
+ }
+
+ @Test
fun filledIconButton_xsmall_visualBounds() {
val expectedWidth =
with(rule.density) { IconButtonDefaults.xSmallContainerSize().width.roundToPx() }
@@ -968,6 +1027,50 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun filledIconToggleButton_checked_medium_squareShape() {
+ var shape: Shape = CircleShape
+ val background = Color.Yellow
+ val iconButtonColor = Color.Blue
+ rule.setMaterialContent(lightColorScheme()) {
+ shape = IconButtonDefaults.mediumSquareShape
+ Surface(color = background) {
+ Box {
+ FilledIconToggleButton(
+ checked = true,
+ onCheckedChange = { /* doSomething() */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.mediumSquareShape,
+ pressedShape = IconButtonDefaults.mediumPressedShape,
+ checkedShape = IconButtonDefaults.mediumSquareShape
+ ),
+ modifier =
+ Modifier.semantics(mergeDescendants = true) {}
+ .testTag(IconTestTag)
+ .size(IconButtonDefaults.mediumContainerSize()),
+ colors =
+ IconButtonDefaults.iconToggleButtonColors(
+ checkedContainerColor = iconButtonColor
+ )
+ ) {}
+ }
+ }
+ }
+
+ rule
+ .onNodeWithTag(IconTestTag)
+ .captureToImage()
+ .assertShape(
+ density = rule.density,
+ shape = shape,
+ shapeColor = iconButtonColor,
+ backgroundColor = background,
+ shapeOverlapPixelCount = with(rule.density) { 1.dp.toPx() }
+ )
+ }
+
@Test
fun filledTonalIconToggleButton_defaultColors() {
rule.setMaterialContent(lightColorScheme()) {
@@ -1092,23 +1195,6 @@
}
@Test
- fun outlinedIconButton_defaultColors() {
- rule.setMaterialContent(lightColorScheme()) {
- val localContentColor = LocalContentColor.current
- Truth.assertThat(IconButtonDefaults.outlinedIconButtonColors())
- .isEqualTo(
- IconButtonColors(
- containerColor = Color.Transparent,
- contentColor = localContentColor,
- disabledContainerColor = Color.Transparent,
- disabledContentColor =
- localContentColor.copy(alpha = OutlinedIconButtonTokens.DisabledOpacity)
- )
- )
- }
- }
-
- @Test
fun outlinedIconToggleButton_size() {
rule
.setMaterialContentForSizeAssertions {
@@ -1216,13 +1302,74 @@
}
@Test
- fun outlinedIconToggleButton_defaultColors() {
+ fun outlinedIconButton_defaultLocalContentColors() {
rule.setMaterialContent(lightColorScheme()) {
val localContentColor = LocalContentColor.current
+ Truth.assertThat(IconButtonDefaults.outlinedIconButtonLocalContentColors())
+ .isEqualTo(
+ IconButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = localContentColor,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor =
+ localContentColor.copy(alpha = OutlinedIconButtonTokens.DisabledOpacity)
+ )
+ )
+ }
+ }
+
+ @Test
+ fun outlinedIconButton_defaultColors() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Truth.assertThat(IconButtonDefaults.outlinedIconButtonColors())
+ .isEqualTo(
+ IconButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = OutlinedIconButtonTokens.Color.value,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor =
+ OutlinedIconButtonTokens.DisabledColor.value.copy(
+ alpha = OutlinedIconButtonTokens.DisabledOpacity
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun outlinedIconToggleButton_defaultColors() {
+ rule.setMaterialContent(lightColorScheme()) {
Truth.assertThat(IconButtonDefaults.outlinedIconToggleButtonColors())
.isEqualTo(
IconToggleButtonColors(
containerColor = Color.Transparent,
+ contentColor = OutlinedIconButtonTokens.UnselectedColor.value,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor =
+ OutlinedIconButtonTokens.DisabledColor.value.copy(
+ alpha = OutlinedIconButtonTokens.DisabledOpacity
+ ),
+ checkedContainerColor =
+ OutlinedIconButtonTokens.SelectedContainerColor.value,
+ checkedContentColor = OutlinedIconButtonTokens.SelectedColor.value
+ )
+ )
+ }
+ }
+
+ @Test
+ fun outlinedIconToggleButton_defaultLocalContentColors_reuse() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val localContentColor = LocalContentColor.current
+ Truth.assertThat(MaterialTheme.colorScheme.defaultOutlinedIconToggleButtonColorsCached)
+ .isNull()
+ val colors = IconButtonDefaults.outlinedIconToggleButtonLocalContentColors()
+ Truth.assertThat(MaterialTheme.colorScheme.defaultOutlinedIconToggleButtonColorsCached)
+ .isNotNull()
+ Truth.assertThat(colors)
+ .isEqualTo(
+ IconToggleButtonColors(
+ containerColor = Color.Transparent,
contentColor = localContentColor,
disabledContainerColor = Color.Transparent,
disabledContentColor =
@@ -1251,7 +1398,7 @@
.isEqualTo(
IconToggleButtonColors(
containerColor = Color.Transparent,
- contentColor = localContentColor,
+ contentColor = OutlinedIconButtonTokens.UnselectedColor.value,
disabledContainerColor = Color.Transparent,
disabledContentColor =
localContentColor.copy(
@@ -1259,8 +1406,7 @@
),
checkedContainerColor =
OutlinedIconButtonTokens.SelectedContainerColor.value,
- checkedContentColor =
- contentColorFor(OutlinedIconButtonTokens.SelectedContainerColor.value)
+ checkedContentColor = OutlinedIconButtonTokens.SelectedColor.value
)
)
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialShapesScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialShapesScreenshotTest.kt
index 5a2855b..bc28a4f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialShapesScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialShapesScreenshotTest.kt
@@ -26,15 +26,24 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -48,7 +57,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-class MaterialShapesScreenshotTest() {
+class MaterialShapesScreenshotTest {
@get:Rule val rule = createComposeRule()
@get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
@@ -57,6 +66,54 @@
private val wrapperTestTag = "materialShapesWrapper"
@Test
+ fun morphShape_start() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Button(
+ onClick = {},
+ modifier = Modifier.requiredSize(56.dp),
+ shape = morphShape(progress = 0f)
+ ) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ assertIndicatorAgainstGolden("morphShape_start")
+ }
+
+ @Test
+ fun morphShape_mid() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Button(
+ onClick = {},
+ modifier = Modifier.requiredSize(56.dp),
+ shape = morphShape(progress = 0.5f)
+ ) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ assertIndicatorAgainstGolden("morphShape_mid")
+ }
+
+ @Test
+ fun morphShape_end() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Button(
+ onClick = {},
+ modifier = Modifier.requiredSize(56.dp),
+ shape = morphShape(progress = 1f)
+ ) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+ }
+ }
+ assertIndicatorAgainstGolden("morphShape_end")
+ }
+
+ @Test
fun materialShapes_allShapes() {
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
@@ -121,6 +178,23 @@
)
}
+ private fun morphShape(progress: Float): Shape {
+ val morph = Morph(MaterialShapes.Diamond, MaterialShapes.Cookie12Sided)
+ return object : Shape {
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ val matrix = Matrix()
+ matrix.scale(size.width, size.height)
+ val path = morph.toPath(progress)
+ path.transform(matrix)
+ return Outline.Generic(path)
+ }
+ }
+ }
+
private fun assertIndicatorAgainstGolden(goldenName: String) {
rule
.onNodeWithTag(wrapperTestTag)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt
index 2dfc1a5..a3a1596 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt
@@ -41,7 +41,6 @@
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class ModalExpandedNavigationRailScreenshotTest {
- // TODO: Add screenshot tests for predictive back behavior.
@get:Rule val composeTestRule = createComposeRule()
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 343bbe2..06b13ec 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -72,7 +72,7 @@
}
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
onClick = {},
checked = false,
) {
@@ -144,18 +144,14 @@
Text("My Button")
},
trailingContent = {
- Box(
- modifier = Modifier.fillMaxHeight(),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- Icons.Outlined.KeyboardArrowDown,
- modifier =
- Modifier.size(SplitButtonDefaults.TrailingIconSize)
- .graphicsLayer { this.rotationZ = 180f },
- contentDescription = "Localized description"
- )
- }
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ modifier =
+ Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
+ this.rotationZ = 180f
+ },
+ contentDescription = "Localized description"
+ )
}
)
}
@@ -280,7 +276,7 @@
}
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
onClick = {},
checked = false,
) {
@@ -311,7 +307,7 @@
}
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(onClick = {}, checked = false) {
+ SplitButtonDefaults.TrailingButton(onClick = {}, checked = false) {
Icon(
Icons.Outlined.KeyboardArrowDown,
contentDescription = "Localized description",
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
index 021104b..bb8c0ba 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
@@ -69,8 +69,8 @@
trailingButton = {
SplitButtonDefaults.TrailingButton(
modifier = Modifier.size(34.dp).testTag("trailingButton"),
+ checked = false,
onClick = {},
- shape = SplitButtonDefaults.trailingButtonShape(),
) {
Icon(Icons.Outlined.KeyboardArrowDown, contentDescription = "Trailing Icon")
}
@@ -99,9 +99,9 @@
},
trailingButton = {
SplitButtonDefaults.TrailingButton(
- modifier = Modifier.size(34.dp).testTag("trailing button"),
onClick = {},
- shape = SplitButtonDefaults.trailingButtonShape(),
+ checked = false,
+ modifier = Modifier.size(34.dp).testTag("trailing button"),
) {
Icon(Icons.Outlined.KeyboardArrowDown, contentDescription = "Trailing Icon")
}
@@ -134,9 +134,9 @@
},
trailingButton = {
SplitButtonDefaults.TrailingButton(
- modifier = Modifier.size(34.dp).testTag("trailing button"),
onClick = {},
- shape = SplitButtonDefaults.trailingButtonShape(),
+ checked = false,
+ modifier = Modifier.size(34.dp).testTag("trailing button"),
enabled = false,
) {
Icon(Icons.Outlined.KeyboardArrowDown, contentDescription = "Trailing Icon")
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt
index 41caaaa..a4b9fc7 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.tokens.ElevatedButtonTokens
import androidx.compose.material3.tokens.FilledButtonTokens
import androidx.compose.material3.tokens.OutlinedButtonTokens
@@ -31,6 +32,7 @@
import androidx.compose.testutils.assertIsEqualTo
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
@@ -300,4 +302,30 @@
)
}
}
+
+ @Test
+ fun buttonShapes_AllRounded_hasRoundedShapesIsTrue() {
+ assertThat(
+ ButtonShapes(
+ shape = RoundedCornerShape(10.dp),
+ pressedShape = RoundedCornerShape(10.dp),
+ checkedShape = RoundedCornerShape(4.dp),
+ )
+ .hasRoundedCornerShapes
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun buttonShapes_mixedShapes_hasRoundedShapesIsFalse() {
+ assertThat(
+ ButtonShapes(
+ shape = RectangleShape,
+ pressedShape = RoundedCornerShape(10.dp),
+ checkedShape = RoundedCornerShape(4.dp),
+ )
+ .hasRoundedCornerShapes
+ )
+ .isFalse()
+ }
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index bf7443d..2ca56a9 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -697,7 +697,7 @@
initialValue = A,
defaultPositionalThreshold,
defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec
+ animationSpec = defaultAnimationSpec,
)
anchoredDraggableState.updateAnchors(
DraggableAnchors {
@@ -735,6 +735,40 @@
dragJob.cancel()
}
+ @Test
+ fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
+ val anchoredDraggableState =
+ AnchoredDraggableState(
+ initialValue = B,
+ defaultPositionalThreshold,
+ defaultVelocityThreshold,
+ animationSpec = defaultAnimationSpec,
+ confirmValueChange = { false }
+ )
+ anchoredDraggableState.updateAnchors(
+ DraggableAnchors {
+ A at 0f
+ B at 200f
+ }
+ )
+
+ assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
+
+ val unexpectedTarget = A
+ val targetUpdates = Channel<Float>()
+ val dragJob =
+ launch(Dispatchers.Unconfined) {
+ anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
+ targetUpdates.send(anchors.positionOf(latestTarget))
+ suspendIndefinitely()
+ }
+ }
+
+ val firstTarget = targetUpdates.receive()
+ assertThat(firstTarget).isEqualTo(200f)
+ dragJob.cancel()
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt
index 08b8371..94554e1 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt
@@ -127,6 +127,7 @@
properties: ModalExpandedNavigationRailProperties,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
+ predictiveBackState: RailPredictiveBackState,
content: @Composable () -> Unit
) {
val view = LocalView.current
@@ -147,6 +148,7 @@
dialogId,
onPredictiveBack,
onPredictiveBackCancelled,
+ predictiveBackState,
darkThemeEnabled,
)
.apply {
@@ -188,6 +190,8 @@
private val onDismissRequest: () -> Unit,
private val onPredictiveBack: (Float) -> Unit,
private val onPredictiveBackCancelled: () -> Unit,
+ private val predictiveBackState: RailPredictiveBackState,
+ private val layoutDirection: LayoutDirection,
) : AbstractComposeView(context), DialogWindowProvider {
private var content: @Composable () -> Unit by mutableStateOf({})
@@ -232,9 +236,11 @@
backCallback =
if (Build.VERSION.SDK_INT >= 34) {
Api34Impl.createBackCallback(
- onDismissRequest,
- onPredictiveBack,
- onPredictiveBackCancelled,
+ onDismissRequest = onDismissRequest,
+ onPredictiveBack = onPredictiveBack,
+ onPredictiveBackCancelled = onPredictiveBackCancelled,
+ predictiveBackState = predictiveBackState,
+ layoutDirection = layoutDirection
)
} else {
Api33Impl.createBackCallback(onDismissRequest)
@@ -258,13 +264,23 @@
onDismissRequest: () -> Unit,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
+ predictiveBackState: RailPredictiveBackState,
+ layoutDirection: LayoutDirection
) =
object : OnBackAnimationCallback {
override fun onBackStarted(backEvent: BackEvent) {
+ predictiveBackState.update(
+ isSwipeEdgeLeft = backEvent.swipeEdge == BackEvent.EDGE_LEFT,
+ isRtl = layoutDirection == LayoutDirection.Rtl
+ )
onPredictiveBack(PredictiveBack.transform(backEvent.progress))
}
override fun onBackProgressed(backEvent: BackEvent) {
+ predictiveBackState.update(
+ isSwipeEdgeLeft = backEvent.swipeEdge == BackEvent.EDGE_LEFT,
+ isRtl = layoutDirection == LayoutDirection.Rtl
+ )
onPredictiveBack(PredictiveBack.transform(backEvent.progress))
}
@@ -321,6 +337,7 @@
dialogId: UUID,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
+ predictiveBackState: RailPredictiveBackState,
darkThemeEnabled: Boolean,
) :
ComponentDialog(
@@ -347,12 +364,14 @@
WindowCompat.setDecorFitsSystemWindows(window, false)
dialogLayout =
ModalWideNavigationRailDialogLayout(
- context,
- window,
- properties.shouldDismissOnBackPress,
- onDismissRequest,
- onPredictiveBack,
- onPredictiveBackCancelled,
+ context = context,
+ window = window,
+ shouldDismissOnBackPress = properties.shouldDismissOnBackPress,
+ onDismissRequest = onDismissRequest,
+ onPredictiveBack = onPredictiveBack,
+ onPredictiveBackCancelled = onPredictiveBackCancelled,
+ predictiveBackState = predictiveBackState,
+ layoutDirection = layoutDirection,
)
.apply {
// Set unique id for AbstractComposeView. This allows state restoration for the
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index f712ed9..9193761 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -45,7 +45,7 @@
import androidx.compose.material3.internal.ProvideContentColorTextStyle
import androidx.compose.material3.internal.systemBarsForVisualComponents
import androidx.compose.material3.tokens.BottomAppBarTokens
-import androidx.compose.material3.tokens.FabSecondaryTokens
+import androidx.compose.material3.tokens.FabSecondaryContainerTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.material3.tokens.TopAppBarLargeTokens
import androidx.compose.material3.tokens.TopAppBarMediumTokens
@@ -1971,7 +1971,7 @@
/** The color of a [BottomAppBar]'s [FloatingActionButton] */
val bottomAppBarFabColor: Color
- @Composable get() = FabSecondaryTokens.ContainerColor.value
+ @Composable get() = FabSecondaryContainerTokens.ContainerColor.value
val HorizontalArrangement =
Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally) // TODO tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 8ca5c0f..1473429 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -35,6 +35,7 @@
import androidx.compose.material3.internal.animateElevation
import androidx.compose.material3.tokens.BaselineButtonTokens
import androidx.compose.material3.tokens.ButtonSmallTokens
+import androidx.compose.material3.tokens.ColorSchemeKeyTokens
import androidx.compose.material3.tokens.ElevatedButtonTokens
import androidx.compose.material3.tokens.FilledButtonTokens
import androidx.compose.material3.tokens.FilledTonalButtonTokens
@@ -805,7 +806,8 @@
return defaultTextButtonColorsCached
?: ButtonColors(
containerColor = Color.Transparent,
- contentColor = fromToken(TextButtonTokens.LabelColor),
+ // TODO replace with the token value once it's corrected
+ contentColor = fromToken(ColorSchemeKeyTokens.Primary),
disabledContainerColor = Color.Transparent,
disabledContentColor =
fromToken(TextButtonTokens.DisabledLabelColor)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
index c81dc3e..17606fb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -511,7 +511,7 @@
internal var defaultShortNavigationBarItemColorsCached: NavigationItemColors? = null
internal var defaultNavigationRailItemColorsCached: NavigationRailItemColors? = null
- internal var mDefaultWideWideNavigationRailColorsCached: WideNavigationRailColors? = null
+ internal var defaultWideWideNavigationRailColorsCached: WideNavigationRailColors? = null
internal var defaultWideNavigationRailItemColorsCached: NavigationItemColors? = null
internal var defaultRadioButtonColorsCached: RadioButtonColors? = null
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index e3f0cc8..e4208ec 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -43,10 +43,16 @@
import androidx.compose.foundation.layout.width
import androidx.compose.material3.internal.ProvideContentColorTextStyle
import androidx.compose.material3.internal.animateElevation
+import androidx.compose.material3.tokens.ElevationTokens
+import androidx.compose.material3.tokens.ExtendedFabLargeTokens
+import androidx.compose.material3.tokens.ExtendedFabMediumTokens
import androidx.compose.material3.tokens.ExtendedFabPrimaryTokens
-import androidx.compose.material3.tokens.FabPrimaryLargeTokens
-import androidx.compose.material3.tokens.FabPrimarySmallTokens
-import androidx.compose.material3.tokens.FabPrimaryTokens
+import androidx.compose.material3.tokens.ExtendedFabSmallTokens
+import androidx.compose.material3.tokens.FabBaselineTokens
+import androidx.compose.material3.tokens.FabLargeTokens
+import androidx.compose.material3.tokens.FabMediumTokens
+import androidx.compose.material3.tokens.FabPrimaryContainerTokens
+import androidx.compose.material3.tokens.FabSmallTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.material3.tokens.TypographyKeyTokens
import androidx.compose.runtime.Composable
@@ -130,8 +136,8 @@
FloatingActionButton(
onClick,
ExtendedFabPrimaryTokens.LabelTextFont.value,
- FabPrimaryTokens.ContainerWidth,
- FabPrimaryTokens.ContainerHeight,
+ FabBaselineTokens.ContainerWidth,
+ FabBaselineTokens.ContainerHeight,
modifier,
shape,
containerColor,
@@ -229,8 +235,8 @@
onClick = onClick,
modifier =
modifier.sizeIn(
- minWidth = FabPrimarySmallTokens.ContainerWidth,
- minHeight = FabPrimarySmallTokens.ContainerHeight,
+ minWidth = FabSmallTokens.ContainerWidth,
+ minHeight = FabSmallTokens.ContainerHeight,
),
shape = shape,
containerColor = containerColor,
@@ -285,10 +291,9 @@
FloatingActionButton(
onClick = onClick,
modifier =
- // TODO: update sizes to use tokens
modifier.sizeIn(
- minWidth = 80.dp,
- minHeight = 80.dp,
+ minWidth = FabMediumTokens.ContainerWidth,
+ minHeight = FabMediumTokens.ContainerHeight,
),
shape = shape,
containerColor = containerColor,
@@ -346,8 +351,8 @@
onClick = onClick,
modifier =
modifier.sizeIn(
- minWidth = FabPrimaryLargeTokens.ContainerWidth,
- minHeight = FabPrimaryLargeTokens.ContainerHeight,
+ minWidth = FabLargeTokens.ContainerWidth,
+ minHeight = FabLargeTokens.ContainerHeight,
),
shape = shape,
containerColor = containerColor,
@@ -412,7 +417,11 @@
interactionSource = interactionSource,
) {
Row(
- modifier = Modifier.padding(horizontal = SmallExtendedFabHorizontalPadding),
+ modifier =
+ Modifier.padding(
+ start = SmallExtendedFabPaddingStart,
+ end = SmallExtendedFabPaddingEnd
+ ),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content,
@@ -474,7 +483,11 @@
interactionSource = interactionSource,
) {
Row(
- modifier = Modifier.padding(horizontal = MediumExtendedFabHorizontalPadding),
+ modifier =
+ Modifier.padding(
+ start = MediumExtendedFabPaddingStart,
+ end = MediumExtendedFabPaddingEnd
+ ),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content,
@@ -536,7 +549,11 @@
interactionSource = interactionSource,
) {
Row(
- modifier = Modifier.padding(horizontal = LargeExtendedFabHorizontalPadding),
+ modifier =
+ Modifier.padding(
+ start = LargeExtendedFabPaddingStart,
+ end = LargeExtendedFabPaddingEnd
+ ),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content,
@@ -662,7 +679,8 @@
textStyle = SmallExtendedFabTextStyle.value,
minWidth = SmallExtendedFabMinimumWidth,
minHeight = SmallExtendedFabMinimumHeight,
- horizontalPadding = SmallExtendedFabHorizontalPadding,
+ startPadding = SmallExtendedFabPaddingStart,
+ endPadding = SmallExtendedFabPaddingEnd,
iconPadding = SmallExtendedFabIconPadding,
modifier = modifier,
expanded = expanded,
@@ -729,7 +747,8 @@
textStyle = MediumExtendedFabTextStyle.value,
minWidth = MediumExtendedFabMinimumWidth,
minHeight = MediumExtendedFabMinimumHeight,
- horizontalPadding = MediumExtendedFabHorizontalPadding,
+ startPadding = MediumExtendedFabPaddingStart,
+ endPadding = MediumExtendedFabPaddingEnd,
iconPadding = MediumExtendedFabIconPadding,
modifier = modifier,
expanded = expanded,
@@ -796,7 +815,8 @@
textStyle = LargeExtendedFabTextStyle.value,
minWidth = LargeExtendedFabMinimumWidth,
minHeight = LargeExtendedFabMinimumHeight,
- horizontalPadding = LargeExtendedFabHorizontalPadding,
+ startPadding = LargeExtendedFabPaddingStart,
+ endPadding = LargeExtendedFabPaddingEnd,
iconPadding = LargeExtendedFabIconPadding,
modifier = modifier,
expanded = expanded,
@@ -877,7 +897,7 @@
if (expanded) {
ExtendedFabMinimumWidth
} else {
- FabPrimaryTokens.ContainerWidth
+ FabBaselineTokens.ContainerWidth
}
)
.padding(start = startPadding, end = endPadding),
@@ -907,7 +927,8 @@
textStyle: TextStyle,
minWidth: Dp,
minHeight: Dp,
- horizontalPadding: Dp,
+ startPadding: Dp,
+ endPadding: Dp,
iconPadding: Dp,
modifier: Modifier = Modifier,
expanded: Boolean = true,
@@ -947,7 +968,7 @@
layout(width, placeable.height) { placeable.place(0, 0) }
}
.sizeIn(minWidth = minWidth, minHeight = minHeight)
- .padding(horizontal = horizontalPadding),
+ .padding(start = startPadding, end = endPadding),
verticalAlignment = Alignment.CenterVertically,
) {
icon()
@@ -978,18 +999,18 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
- val MediumIconSize = 28.dp // TODO: update to use token
+ val MediumIconSize = FabMediumTokens.IconSize
/** The recommended size of the icon inside a [LargeFloatingActionButton]. */
- val LargeIconSize = FabPrimaryLargeTokens.IconSize
+ val LargeIconSize = 36.dp // TODO: FabLargeTokens.IconSize is incorrect
/** Default shape for a floating action button. */
val shape: Shape
- @Composable get() = FabPrimaryTokens.ContainerShape.value
+ @Composable get() = FabBaselineTokens.ContainerShape.value
/** Default shape for a small floating action button. */
val smallShape: Shape
- @Composable get() = FabPrimarySmallTokens.ContainerShape.value
+ @Composable get() = FabSmallTokens.ContainerShape.value
/** Default shape for a medium floating action button. */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@@ -1000,7 +1021,7 @@
/** Default shape for a large floating action button. */
val largeShape: Shape
- @Composable get() = FabPrimaryLargeTokens.ContainerShape.value
+ @Composable get() = FabLargeTokens.ContainerShape.value
/** Default shape for an extended floating action button. */
val extendedFabShape: Shape
@@ -1011,7 +1032,7 @@
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
val smallExtendedFabShape: Shape
- @Composable get() = ShapeDefaults.Large // TODO: update to use token
+ @Composable get() = ExtendedFabSmallTokens.ContainerShape.value
/** Default shape for a medium extended floating action button. */
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@@ -1025,11 +1046,11 @@
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
val largeExtendedFabShape: Shape
- @Composable get() = ShapeDefaults.ExtraLarge // TODO: update to use token
+ @Composable get() = ExtendedFabLargeTokens.ContainerShape.value
/** Default container color for a floating action button. */
val containerColor: Color
- @Composable get() = FabPrimaryTokens.ContainerColor.value
+ @Composable get() = FabPrimaryContainerTokens.ContainerColor.value
/**
* Creates a [FloatingActionButtonElevation] that represents the elevation of a
@@ -1044,10 +1065,10 @@
*/
@Composable
fun elevation(
- defaultElevation: Dp = FabPrimaryTokens.ContainerElevation,
- pressedElevation: Dp = FabPrimaryTokens.PressedContainerElevation,
- focusedElevation: Dp = FabPrimaryTokens.FocusContainerElevation,
- hoveredElevation: Dp = FabPrimaryTokens.HoverContainerElevation,
+ defaultElevation: Dp = FabPrimaryContainerTokens.ContainerElevation,
+ pressedElevation: Dp = FabPrimaryContainerTokens.PressedContainerElevation,
+ focusedElevation: Dp = FabPrimaryContainerTokens.FocusedContainerElevation,
+ hoveredElevation: Dp = FabPrimaryContainerTokens.HoveredContainerElevation,
): FloatingActionButtonElevation =
FloatingActionButtonElevation(
defaultElevation = defaultElevation,
@@ -1068,10 +1089,10 @@
*/
@Composable
fun loweredElevation(
- defaultElevation: Dp = FabPrimaryTokens.LoweredContainerElevation,
- pressedElevation: Dp = FabPrimaryTokens.LoweredPressedContainerElevation,
- focusedElevation: Dp = FabPrimaryTokens.LoweredFocusContainerElevation,
- hoveredElevation: Dp = FabPrimaryTokens.LoweredHoverContainerElevation,
+ defaultElevation: Dp = ElevationTokens.Level1,
+ pressedElevation: Dp = ElevationTokens.Level1,
+ focusedElevation: Dp = ElevationTokens.Level1,
+ hoveredElevation: Dp = ElevationTokens.Level2,
): FloatingActionButtonElevation =
FloatingActionButtonElevation(
defaultElevation = defaultElevation,
@@ -1404,22 +1425,27 @@
fun asState(): State<Dp> = animatable.asState()
}
-private val SmallExtendedFabMinimumWidth = 56.dp
-private val SmallExtendedFabMinimumHeight = 56.dp
-private val SmallExtendedFabHorizontalPadding = 16.dp
-private val SmallExtendedFabIconPadding = 8.dp
+private val SmallExtendedFabMinimumWidth = ExtendedFabSmallTokens.ContainerHeight
+private val SmallExtendedFabMinimumHeight = ExtendedFabSmallTokens.ContainerHeight
+private val SmallExtendedFabPaddingStart = ExtendedFabSmallTokens.LeadingSpace
+private val SmallExtendedFabPaddingEnd = ExtendedFabSmallTokens.TrailingSpace
+private val SmallExtendedFabIconPadding = ExtendedFabSmallTokens.IconLabelSpace
private val SmallExtendedFabTextStyle = TypographyKeyTokens.TitleMedium
-private val MediumExtendedFabMinimumWidth = 80.dp
-private val MediumExtendedFabMinimumHeight = 80.dp
-private val MediumExtendedFabHorizontalPadding = 26.dp
-private val MediumExtendedFabIconPadding = 16.dp
+private val MediumExtendedFabMinimumWidth = ExtendedFabMediumTokens.ContainerHeight
+private val MediumExtendedFabMinimumHeight = ExtendedFabMediumTokens.ContainerHeight
+private val MediumExtendedFabPaddingStart = ExtendedFabMediumTokens.LeadingSpace
+private val MediumExtendedFabPaddingEnd = ExtendedFabMediumTokens.TrailingSpace
+// TODO: ExtendedFabMediumTokens.IconLabelSpace is incorrect
+private val MediumExtendedFabIconPadding = 12.dp
private val MediumExtendedFabTextStyle = TypographyKeyTokens.TitleLarge
-private val LargeExtendedFabMinimumWidth = 96.dp
-private val LargeExtendedFabMinimumHeight = 96.dp
-private val LargeExtendedFabHorizontalPadding = 28.dp
-private val LargeExtendedFabIconPadding = 20.dp
+private val LargeExtendedFabMinimumWidth = ExtendedFabLargeTokens.ContainerHeight
+private val LargeExtendedFabMinimumHeight = ExtendedFabLargeTokens.ContainerHeight
+private val LargeExtendedFabPaddingStart = ExtendedFabLargeTokens.LeadingSpace
+private val LargeExtendedFabPaddingEnd = ExtendedFabLargeTokens.TrailingSpace
+// TODO: ExtendedFabLargeTokens.IconLabelSpace is incorrect
+private val LargeExtendedFabIconPadding = 16.dp
private val LargeExtendedFabTextStyle = TypographyKeyTokens.HeadlineSmall
private val ExtendedFabStartIconPadding = 16.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButtonMenu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButtonMenu.kt
index 1abb428..1fb22a7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButtonMenu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButtonMenu.kt
@@ -31,9 +31,13 @@
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.tokens.FabBaselineTokens
+import androidx.compose.material3.tokens.FabLargeTokens
+import androidx.compose.material3.tokens.FabMediumTokens
+import androidx.compose.material3.tokens.FabMenuBaselineTokens
+import androidx.compose.material3.tokens.FabPrimaryContainerTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -309,7 +313,7 @@
placeable.placeWithLayer(0, 0) { alpha = tempAlphaAnim.value }
}
},
- shape = CircleShape,
+ shape = FabMenuBaselineTokens.ListItemContainerShape.value,
color = containerColor,
contentColor = contentColor,
onClick = onClick
@@ -326,7 +330,10 @@
}
}
.sizeIn(minWidth = FabMenuItemMinWidth, minHeight = FabMenuItemHeight)
- .padding(horizontal = FabMenuItemContentPaddingHorizontal),
+ .padding(
+ start = FabMenuItemContentPaddingStart,
+ end = FabMenuItemContentPaddingEnd
+ ),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement =
Arrangement.spacedBy(
@@ -335,7 +342,10 @@
)
) {
icon()
- text()
+ CompositionLocalProvider(
+ LocalTextStyle provides MaterialTheme.typography.titleMedium,
+ content = text
+ )
}
}
}
@@ -586,26 +596,27 @@
val checkedProgress: Float
}
-private val FabInitialSize = 56.dp
+private val FabInitialSize = FabBaselineTokens.ContainerHeight
private val FabInitialCornerRadius = 16.dp
-private val FabInitialIconSize = 24.dp
-private val FabMediumInitialSize = 80.dp
-private val FabMediumInitialCornerRadius = 24.dp
-private val FabMediumInitialIconSize = 28.dp
-private val FabLargeInitialSize = 96.dp
+private val FabInitialIconSize = FabBaselineTokens.IconSize
+private val FabMediumInitialSize = FabMediumTokens.ContainerHeight
+private val FabMediumInitialCornerRadius = 20.dp
+private val FabMediumInitialIconSize = FabMediumTokens.IconSize
+private val FabLargeInitialSize = FabLargeTokens.ContainerHeight
private val FabLargeInitialCornerRadius = 28.dp
-private val FabLargeInitialIconSize = 36.dp
-private val FabFinalSize = 56.dp
+private val FabLargeInitialIconSize = 36.dp // TODO: FabLargeTokens.IconSize is incorrect
+private val FabFinalSize = FabMenuBaselineTokens.CloseButtonContainerHeight
private val FabFinalCornerRadius = FabFinalSize.div(2)
-private val FabFinalIconSize = 20.dp
-private val FabShadowElevation = 6.dp
+private val FabFinalIconSize = FabMenuBaselineTokens.CloseButtonIconSize
+private val FabShadowElevation = FabPrimaryContainerTokens.ContainerElevation
private val FabMenuPaddingHorizontal = 16.dp
-private val FabMenuPaddingBottom = 8.dp
+private val FabMenuPaddingBottom = FabMenuBaselineTokens.CloseButtonBetweenSpace
private val FabMenuButtonPaddingBottom = 16.dp
-private val FabMenuItemMinWidth = 56.dp
-private val FabMenuItemHeight = 56.dp
-private val FabMenuItemSpacingVertical = 4.dp
-private val FabMenuItemContentPaddingHorizontal = 16.dp
-private val FabMenuItemContentSpacingHorizontal = 8.dp
+private val FabMenuItemMinWidth = FabMenuBaselineTokens.ListItemContainerHeight
+private val FabMenuItemHeight = FabMenuBaselineTokens.ListItemContainerHeight
+private val FabMenuItemSpacingVertical = FabMenuBaselineTokens.ListItemBetweenSpace
+private val FabMenuItemContentPaddingStart = FabMenuBaselineTokens.ListItemLeadingSpace
+private val FabMenuItemContentPaddingEnd = FabMenuBaselineTokens.ListItemTrailingSpace
+private val FabMenuItemContentSpacingHorizontal = FabMenuBaselineTokens.ListItemIconLabelSpace
private const val StaggerEnterDelayMillis = 35
private const val StaggerExitDelayMillis = 25
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index 2197c8f..6337974 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -15,20 +15,26 @@
*/
package androidx.compose.material3
+import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.internal.childSemantics
-import androidx.compose.material3.tokens.ColorSchemeKeyTokens
+import androidx.compose.material3.internal.rememberAnimatedShape
import androidx.compose.material3.tokens.FilledIconButtonTokens
import androidx.compose.material3.tokens.FilledTonalIconButtonTokens
import androidx.compose.material3.tokens.LargeIconButtonTokens
import androidx.compose.material3.tokens.MediumIconButtonTokens
+import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.material3.tokens.OutlinedIconButtonTokens
import androidx.compose.material3.tokens.SmallIconButtonTokens
import androidx.compose.material3.tokens.StandardIconButtonTokens
@@ -39,6 +45,8 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
@@ -76,14 +84,6 @@
* IconButton with a color tint
*
* @sample androidx.compose.material3.samples.TintedIconButtonSample
- *
- * Small-sized narrow round shape IconButton
- *
- * @sample androidx.compose.material3.samples.XSmallNarrowSquareIconButtonsSample
- *
- * Medium / default size round-shaped icon button
- *
- * @sample androidx.compose.material3.samples.MediumRoundWideIconButtonSample
* @param onClick called when this icon button is clicked
* @param modifier the [Modifier] to be applied to this icon button
* @param enabled controls the enabled state of this icon button. When `false`, this component will
@@ -97,7 +97,6 @@
* interactions will still happen internally.
* @param content the content of this icon button, typically an [Icon]
*/
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Deprecated(
message = "Use overload with `shape`",
replaceWith =
@@ -111,7 +110,7 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
+ colors: IconButtonColors = IconButtonDefaults.iconButtonLocalContentColors(),
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit
) {
@@ -147,13 +146,22 @@
* IconButton with a color tint
*
* @sample androidx.compose.material3.samples.TintedIconButtonSample
+ *
+ * Small-sized narrow round shape IconButton
+ *
+ * @sample androidx.compose.material3.samples.XSmallNarrowSquareIconButtonsSample
+ *
+ * Medium / default size round-shaped icon button
+ *
+ * @sample androidx.compose.material3.samples.MediumRoundWideIconButtonSample
* @param onClick called when this icon button is clicked
* @param modifier the [Modifier] to be applied to this icon button
* @param enabled controls the enabled state of this icon button. When `false`, this component will
* not respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
* @param colors [IconButtonColors] that will be used to resolve the colors used for this icon
- * button in different states. See [IconButtonDefaults.iconButtonColors].
+ * button in different states. See [IconButtonDefaults.iconButtonColors] and
+ * [IconButtonDefaults.iconButtonLocalContentColors] .
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting [Interaction]s for this icon button. You can use this to change the icon button's
* appearance or preview the icon button in different states. Note that if `null` is provided,
@@ -167,7 +175,7 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
+ colors: IconButtonColors = IconButtonDefaults.iconButtonLocalContentColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = IconButtonDefaults.standardShape,
content: @Composable () -> Unit
@@ -226,7 +234,6 @@
* interactions will still happen internally.
* @param content the content of this icon button, typically an [Icon]
*/
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Deprecated(
message = "Use overload with `shape`",
replaceWith =
@@ -242,7 +249,7 @@
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
+ colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonLocalContentColors(),
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit
) {
@@ -280,7 +287,8 @@
* not respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
* @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
- * button in different states. See [IconButtonDefaults.iconToggleButtonColors].
+ * button in different states. See [IconButtonDefaults.iconToggleButtonColors] and
+ * [IconButtonDefaults.iconToggleButtonLocalContentColors].
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting [Interaction]s for this icon button. You can use this to change the icon button's
* appearance or preview the icon button in different states. Note that if `null` is provided,
@@ -295,6 +303,86 @@
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
+ colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonLocalContentColors(),
+ interactionSource: MutableInteractionSource? = null,
+ shape: Shape = IconButtonDefaults.standardShape,
+ content: @Composable () -> Unit
+) =
+ IconToggleButtonImpl(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource,
+ shape = shape,
+ content = content
+ )
+
+/**
+ * <a href="https://m3.material.io/components/icon-button/overview" class="external"
+ * target="_blank">Material Design standard icon toggle button</a>.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * 
+ *
+ * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
+ * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
+ * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
+ *
+ * @sample androidx.compose.material3.samples.IconToggleButtonWithAnimatedShapeSample
+ * @param checked whether this icon button is toggled on or off
+ * @param onCheckedChange called when this icon button is clicked
+ * @param shapes the [IconButtonShapes] that the icon toggle button will morph between depending on
+ * the user's interaction with the icon toggle button.
+ * @param modifier the [Modifier] to be applied to this icon button
+ * @param enabled controls the enabled state of this icon button. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.iconToggleButtonColors].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this icon button. You can use this to change the icon button's
+ * appearance or preview the icon button in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ * @param content the content of this icon button, typically an [Icon]
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun IconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ shapes: IconButtonShapes,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable () -> Unit
+) {
+ @Suppress("NAME_SHADOWING")
+ val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+ IconToggleButtonImpl(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ shape = shapeForInteraction(checked, shapes, interactionSource),
+ colors = colors,
+ interactionSource = interactionSource,
+ content = content
+ )
+}
+
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+private fun IconToggleButtonImpl(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = IconButtonDefaults.standardShape,
@@ -379,6 +467,118 @@
/**
* <a href="https://m3.material.io/components/icon-button/overview" class="external"
+ * target="_blank">Material Design filled icon toggle button</a>.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * 
+ *
+ * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
+ * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
+ * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
+ *
+ * Toggleable filled icon button sample:
+ *
+ * @sample androidx.compose.material3.samples.FilledIconToggleButtonSample
+ * @param checked whether this icon button is toggled on or off
+ * @param onCheckedChange called when this icon button is clicked
+ * @param modifier the [Modifier] to be applied to this icon button
+ * @param enabled controls the enabled state of this icon button. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param shape defines the shape of this icon button's container
+ * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.filledIconToggleButtonColors].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this icon button. You can use this to change the icon button's
+ * appearance or preview the icon button in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ * @param content the content of this icon button, typically an [Icon]
+ */
+@Composable
+fun FilledIconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = IconButtonDefaults.filledShape,
+ colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable () -> Unit
+) =
+ SurfaceIconToggleButton(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier.semantics { role = Role.Checkbox },
+ enabled = enabled,
+ shape = shape,
+ colors = colors,
+ border = null,
+ interactionSource = interactionSource,
+ content = content
+ )
+
+/**
+ * <a href="https://m3.material.io/components/icon-button/overview" class="external"
+ * target="_blank">Material Design filled icon toggle button</a>.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * 
+ *
+ * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
+ * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
+ * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
+ *
+ * Toggleable filled icon button sample:
+ *
+ * @sample androidx.compose.material3.samples.FilledIconToggleButtonWithAnimatedShapeSample
+ * @param checked whether this icon button is toggled on or off
+ * @param onCheckedChange called when this icon button is clicked
+ * @param shapes the [IconButtonShapes] that the icon toggle button will morph between depending on
+ * the user's interaction with the icon toggle button.
+ * @param modifier the [Modifier] to be applied to this icon button
+ * @param enabled controls the enabled state of this icon button. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.filledIconToggleButtonColors].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this icon button. You can use this to change the icon button's
+ * appearance or preview the icon button in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ * @param content the content of this icon button, typically an [Icon]
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun FilledIconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ shapes: IconButtonShapes,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable () -> Unit
+) =
+ SurfaceIconToggleButton(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier.semantics { role = Role.Checkbox },
+ enabled = enabled,
+ shapes = shapes,
+ colors = colors,
+ border = null,
+ interactionSource = interactionSource,
+ content = content
+ )
+
+/**
+ * <a href="https://m3.material.io/components/icon-button/overview" class="external"
* target="_blank">Material Design filled tonal icon button</a>.
*
* Icon buttons help people take supplementary actions with a single tap. They’re used when a
@@ -435,61 +635,6 @@
/**
* <a href="https://m3.material.io/components/icon-button/overview" class="external"
- * target="_blank">Material Design filled icon toggle button</a>.
- *
- * Icon buttons help people take supplementary actions with a single tap. They’re used when a
- * compact button is required, such as in a toolbar or image list.
- *
- * 
- *
- * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
- * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
- * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
- *
- * Toggleable filled icon button sample:
- *
- * @sample androidx.compose.material3.samples.FilledIconToggleButtonSample
- * @param checked whether this icon button is toggled on or off
- * @param onCheckedChange called when this icon button is clicked
- * @param modifier the [Modifier] to be applied to this icon button
- * @param enabled controls the enabled state of this icon button. When `false`, this component will
- * not respond to user input, and it will appear visually disabled and disabled to accessibility
- * services.
- * @param shape defines the shape of this icon button's container
- * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
- * button in different states. See [IconButtonDefaults.filledIconToggleButtonColors].
- * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
- * emitting [Interaction]s for this icon button. You can use this to change the icon button's
- * appearance or preview the icon button in different states. Note that if `null` is provided,
- * interactions will still happen internally.
- * @param content the content of this icon button, typically an [Icon]
- */
-@Composable
-fun FilledIconToggleButton(
- checked: Boolean,
- onCheckedChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = true,
- shape: Shape = IconButtonDefaults.filledShape,
- colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
- interactionSource: MutableInteractionSource? = null,
- content: @Composable () -> Unit
-) =
- SurfaceIconToggleButton(
- checked = checked,
- onCheckedChange = onCheckedChange,
- modifier = modifier.semantics { role = Role.Checkbox },
- enabled = enabled,
- shape = shape,
- colors = colors,
- border = null,
- interactionSource = interactionSource,
- content = content
- )
-
-/**
- * <a href="https://m3.material.io/components/icon-button/overview" class="external"
* target="_blank">Material Design filled tonal icon toggle button</a>.
*
* Icon buttons help people take supplementary actions with a single tap. They’re used when a
@@ -525,7 +670,6 @@
* interactions will still happen internally.
* @param content the content of this icon button, typically an [Icon]
*/
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun FilledTonalIconToggleButton(
checked: Boolean,
@@ -551,6 +695,68 @@
/**
* <a href="https://m3.material.io/components/icon-button/overview" class="external"
+ * target="_blank">Material Design filled tonal icon toggle button</a>.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * 
+ *
+ * A filled tonal toggle icon button is a medium-emphasis icon button that is an alternative middle
+ * ground between the default [FilledIconToggleButton] and [OutlinedIconToggleButton]. They can be
+ * used in contexts where the lower-priority icon button requires slightly more emphasis than an
+ * outline would give.
+ *
+ * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
+ * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
+ * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
+ *
+ * Toggleable filled tonal icon button with animatable shape sample:
+ *
+ * @sample androidx.compose.material3.samples.FilledTonalIconToggleButtonWithAnimatedShapeSample
+ * @param checked whether this icon button is toggled on or off
+ * @param onCheckedChange called when this icon button is clicked
+ * @param shapes the [IconButtonShapes] that the icon toggle button will morph between depending on
+ * the user's interaction with the icon toggle button.
+ * @param modifier the [Modifier] to be applied to this icon button
+ * @param enabled controls the enabled state of this icon button. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.filledIconToggleButtonColors].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this icon button. You can use this to change the icon button's
+ * appearance or preview the icon button in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ * @param content the content of this icon button, typically an [Icon]
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun FilledTonalIconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ shapes: IconButtonShapes,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: IconToggleButtonColors = IconButtonDefaults.filledTonalIconToggleButtonColors(),
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable () -> Unit
+) =
+ SurfaceIconToggleButton(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier.semantics { role = Role.Checkbox },
+ enabled = enabled,
+ shapes = shapes,
+ colors = colors,
+ border = null,
+ interactionSource = interactionSource,
+ content = content
+ )
+
+/**
+ * <a href="https://m3.material.io/components/icon-button/overview" class="external"
* target="_blank">Material Design outlined icon button</a>.
*
* Icon buttons help people take supplementary actions with a single tap. They’re used when a
@@ -582,9 +788,11 @@
* @param shape defines the shape of this icon button's container and border (when [border] is not
* null)
* @param colors [IconButtonColors] that will be used to resolve the colors used for this icon
- * button in different states. See [IconButtonDefaults.outlinedIconButtonColors].
+ * button in different states. See [IconButtonDefaults.outlinedIconButtonColors] and
+ * [IconButtonDefaults.outlinedIconButtonLocalContentColors].
* @param border the border to draw around the container of this icon button. Pass `null` for no
- * border. See [IconButtonDefaults.outlinedIconButtonBorder].
+ * border. See [IconButtonDefaults.outlinedIconButtonBorder] and
+ * [IconButtonDefaults.outlinedIconButtonLocalContentColorBorder].
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting [Interaction]s for this icon button. You can use this to change the icon button's
* appearance or preview the icon button in different states. Note that if `null` is provided,
@@ -597,8 +805,8 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = IconButtonDefaults.outlinedShape,
- colors: IconButtonColors = IconButtonDefaults.outlinedIconButtonColors(),
- border: BorderStroke? = IconButtonDefaults.outlinedIconButtonBorder(enabled),
+ colors: IconButtonColors = IconButtonDefaults.outlinedIconButtonLocalContentColors(),
+ border: BorderStroke? = IconButtonDefaults.outlinedIconButtonLocalContentColorBorder(enabled),
interactionSource: MutableInteractionSource? = null,
content: @Composable () -> Unit
) =
@@ -613,6 +821,125 @@
content = content
)
+/**
+ * <a href="https://m3.material.io/components/icon-button/overview" class="external"
+ * target="_blank">Material Design outlined icon toggle button</a>.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * 
+ *
+ * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
+ * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
+ * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
+ *
+ * @sample androidx.compose.material3.samples.OutlinedIconToggleButtonSample
+ * @param checked whether this icon button is toggled on or off
+ * @param onCheckedChange called when this icon button is clicked
+ * @param modifier the [Modifier] to be applied to this icon button
+ * @param enabled controls the enabled state of this icon button. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param shape defines the shape of this icon button's container and border (when [border] is not
+ * null)
+ * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.outlinedIconToggleButtonColors] and
+ * [IconButtonDefaults.outlinedIconToggleButtonLocalContentColors].
+ * @param border the border to draw around the container of this icon button. Pass `null` for no
+ * border. See [IconButtonDefaults.outlinedIconToggleButtonBorder] and
+ * [IconButtonDefaults.outlinedIconToggleButtonLocalContentColorBorder].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this icon button. You can use this to change the icon button's
+ * appearance or preview the icon button in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ * @param content the content of this icon button, typically an [Icon]
+ */
+@Composable
+fun OutlinedIconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = IconButtonDefaults.outlinedShape,
+ colors: IconToggleButtonColors =
+ IconButtonDefaults.outlinedIconToggleButtonLocalContentColors(),
+ border: BorderStroke? =
+ IconButtonDefaults.outlinedIconToggleButtonLocalContentColorBorder(enabled, checked),
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable () -> Unit
+) =
+ SurfaceIconToggleButton(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier.semantics { role = Role.Checkbox },
+ enabled = enabled,
+ shape = shape,
+ colors = colors,
+ border = border,
+ interactionSource = interactionSource,
+ content = content
+ )
+
+/**
+ * <a href="https://m3.material.io/components/icon-button/overview" class="external"
+ * target="_blank">Material Design outlined icon toggle button</a>.
+ *
+ * Icon buttons help people take supplementary actions with a single tap. They’re used when a
+ * compact button is required, such as in a toolbar or image list.
+ *
+ * 
+ *
+ * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
+ * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
+ * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
+ *
+ * @sample androidx.compose.material3.samples.OutlinedIconToggleButtonWithAnimatedShapeSample
+ * @param checked whether this icon button is toggled on or off
+ * @param onCheckedChange called when this icon button is clicked
+ * @param shapes the [IconButtonShapes] that the icon toggle button will morph between depending on
+ * the user's interaction with the icon toggle button.
+ * @param modifier the [Modifier] to be applied to this icon button
+ * @param enabled controls the enabled state of this icon button. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.outlinedIconToggleButtonColors].
+ * @param border the border to draw around the container of this icon button. Pass `null` for no
+ * border. See [IconButtonDefaults.outlinedIconToggleButtonBorder].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this icon button. You can use this to change the icon button's
+ * appearance or preview the icon button in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ * @param content the content of this icon button, typically an [Icon]
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun OutlinedIconToggleButton(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ shapes: IconButtonShapes,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: IconToggleButtonColors = IconButtonDefaults.outlinedIconToggleButtonColors(),
+ border: BorderStroke? = IconButtonDefaults.outlinedIconToggleButtonBorder(enabled, checked),
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable () -> Unit
+) =
+ SurfaceIconToggleButton(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier.semantics { role = Role.Checkbox },
+ enabled = enabled,
+ shapes = shapes,
+ colors = colors,
+ border = border,
+ interactionSource = interactionSource,
+ content = content
+ )
+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun SurfaceIconButton(
@@ -670,8 +997,27 @@
Box(
modifier =
Modifier.size(
- IconButtonDefaults.smallContainerSize(),
- ),
+ IconButtonDefaults.smallContainerSize(),
+ )
+ .then(
+ when (shape) {
+ is ShapeWithOpticalCentering -> {
+ Modifier.opticalCentering(
+ shape = shape,
+ basePadding = PaddingValues()
+ )
+ }
+ is CornerBasedShape -> {
+ Modifier.opticalCentering(
+ shape = shape,
+ basePadding = PaddingValues()
+ )
+ }
+ else -> {
+ Modifier
+ }
+ }
+ ),
contentAlignment = Alignment.Center
) {
content()
@@ -679,74 +1025,66 @@
}
}
-/**
- * <a href="https://m3.material.io/components/icon-button/overview" class="external"
- * target="_blank">Material Design outlined icon toggle button</a>.
- *
- * Icon buttons help people take supplementary actions with a single tap. They’re used when a
- * compact button is required, such as in a toolbar or image list.
- *
- * 
- *
- * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a
- * custom icon, note that the typical size for the internal icon is 24 x 24 dp. This icon button has
- * an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines.
- *
- * @sample androidx.compose.material3.samples.OutlinedIconToggleButtonSample
- * @param checked whether this icon button is toggled on or off
- * @param onCheckedChange called when this icon button is clicked
- * @param modifier the [Modifier] to be applied to this icon button
- * @param enabled controls the enabled state of this icon button. When `false`, this component will
- * not respond to user input, and it will appear visually disabled and disabled to accessibility
- * services.
- * @param shape defines the shape of this icon button's container and border (when [border] is not
- * null)
- * @param colors [IconToggleButtonColors] that will be used to resolve the colors used for this icon
- * button in different states. See [IconButtonDefaults.outlinedIconToggleButtonColors].
- * @param border the border to draw around the container of this icon button. Pass `null` for no
- * border. See [IconButtonDefaults.outlinedIconToggleButtonBorder].
- * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
- * emitting [Interaction]s for this icon button. You can use this to change the icon button's
- * appearance or preview the icon button in different states. Note that if `null` is provided,
- * interactions will still happen internally.
- * @param content the content of this icon button, typically an [Icon]
- */
+@ExperimentalMaterial3ExpressiveApi
@Composable
-fun OutlinedIconToggleButton(
+private fun SurfaceIconToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = true,
- shape: Shape = IconButtonDefaults.outlinedShape,
- colors: IconToggleButtonColors = IconButtonDefaults.outlinedIconToggleButtonColors(),
- border: BorderStroke? = IconButtonDefaults.outlinedIconToggleButtonBorder(enabled, checked),
- interactionSource: MutableInteractionSource? = null,
+ modifier: Modifier,
+ enabled: Boolean,
+ shapes: IconButtonShapes,
+ colors: IconToggleButtonColors,
+ border: BorderStroke?,
+ interactionSource: MutableInteractionSource?,
content: @Composable () -> Unit
-) =
+) {
+
+ @Suppress("NAME_SHADOWING")
+ val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+
SurfaceIconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
- modifier = modifier.semantics { role = Role.Checkbox },
+ modifier = modifier,
enabled = enabled,
- shape = shape,
+ shape = shapeForInteraction(checked, shapes, interactionSource),
colors = colors,
border = border,
interactionSource = interactionSource,
content = content
)
+}
-/** Contains the default values used by all icon button types. */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+private fun shapeForInteraction(
+ checked: Boolean,
+ shapes: IconButtonShapes,
+ interactionSource: MutableInteractionSource,
+): Shape {
+ // TODO Load the motionScheme tokens from the component tokens file
+ // MotionSchemeKeyTokens.DefaultEffects is intentional here to prevent
+ // any bounce in this component.
+ val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
+ val pressed by interactionSource.collectIsPressedAsState()
+
+ return shapeByInteraction(shapes, pressed, checked, defaultAnimationSpec)
+}
+
+/** Contains the default values for all four icon and icon toggle button types. */
object IconButtonDefaults {
- /** Creates a [IconButtonColors] that represents the default colors used in a [IconButton]. */
+ /**
+ * Contains the default values used by [IconButton]. [LocalContentColor] will be applied to the
+ * icon and down the UI tree.
+ */
@Composable
- fun iconButtonColors(): IconButtonColors {
+ fun iconButtonLocalContentColors(): IconButtonColors {
val contentColor = LocalContentColor.current
val colors = MaterialTheme.colorScheme.defaultIconButtonColors(contentColor)
- if (colors.contentColor == contentColor) {
- return colors
+ return if (colors.contentColor == contentColor) {
+ colors
} else {
- return colors.copy(
+ colors.copy(
contentColor = contentColor,
disabledContentColor =
contentColor.copy(alpha = StandardIconButtonTokens.DisabledOpacity)
@@ -755,6 +1093,14 @@
}
/**
+ * Creates a [IconButtonColors] that represents the default colors used in a [IconButton]. See
+ * [iconButtonLocalContentColors] for default values that applies [LocalContentColor] to the
+ * icon and down the UI tree.
+ */
+ @Composable
+ fun iconButtonColors(): IconButtonColors = MaterialTheme.colorScheme.defaultIconButtonColors()
+
+ /**
* Creates a [IconButtonColors] that represents the default colors used in a [IconButton].
*
* @param containerColor the container color of this icon button when enabled.
@@ -765,13 +1111,13 @@
@Composable
fun iconButtonColors(
containerColor: Color = Color.Unspecified,
- contentColor: Color = LocalContentColor.current,
+ contentColor: Color = Color.Unspecified,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color =
contentColor.copy(alpha = StandardIconButtonTokens.DisabledOpacity)
): IconButtonColors =
MaterialTheme.colorScheme
- .defaultIconButtonColors(LocalContentColor.current)
+ .defaultIconButtonColors()
.copy(
containerColor = containerColor,
contentColor = contentColor,
@@ -779,15 +1125,22 @@
disabledContentColor = disabledContentColor,
)
- internal fun ColorScheme.defaultIconButtonColors(localContentColor: Color): IconButtonColors {
+ internal fun ColorScheme.defaultIconButtonColors(
+ localContentColor: Color? = null,
+ ): IconButtonColors {
return defaultIconButtonColorsCached
?: run {
IconButtonColors(
containerColor = Color.Transparent,
- contentColor = localContentColor,
+ contentColor =
+ localContentColor ?: fromToken(StandardIconButtonTokens.Color),
disabledContainerColor = Color.Transparent,
disabledContentColor =
- localContentColor.copy(alpha = StandardIconButtonTokens.DisabledOpacity)
+ localContentColor?.copy(
+ alpha = StandardIconButtonTokens.DisabledOpacity
+ )
+ ?: fromToken(StandardIconButtonTokens.DisabledColor)
+ .copy(alpha = StandardIconButtonTokens.DisabledOpacity)
)
.also { defaultIconButtonColorsCached = it }
}
@@ -795,10 +1148,10 @@
/**
* Creates a [IconToggleButtonColors] that represents the default colors used in a
- * [IconToggleButton].
+ * [IconToggleButton]. [LocalContentColor] will be applied to the icon and down the UI tree.
*/
@Composable
- fun iconToggleButtonColors(): IconToggleButtonColors {
+ fun iconToggleButtonLocalContentColors(): IconToggleButtonColors {
val contentColor = LocalContentColor.current
val colors = MaterialTheme.colorScheme.defaultIconToggleButtonColors(contentColor)
if (colors.contentColor == contentColor) {
@@ -814,6 +1167,15 @@
/**
* Creates a [IconToggleButtonColors] that represents the default colors used in a
+ * [IconToggleButton]. See [iconToggleButtonLocalContentColors] for default values that applies
+ * [LocalContentColor] to the icon and down the UI tree.
+ */
+ @Composable
+ fun iconToggleButtonColors(): IconToggleButtonColors =
+ MaterialTheme.colorScheme.defaultIconToggleButtonColors()
+
+ /**
+ * Creates a [IconToggleButtonColors] that represents the default colors used in a
* [IconToggleButton].
*
* @param containerColor the container color of this icon button when enabled.
@@ -826,7 +1188,7 @@
@Composable
fun iconToggleButtonColors(
containerColor: Color = Color.Unspecified,
- contentColor: Color = LocalContentColor.current,
+ contentColor: Color = Color.Unspecified,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color =
contentColor.copy(alpha = StandardIconButtonTokens.DisabledOpacity),
@@ -834,7 +1196,7 @@
checkedContentColor: Color = Color.Unspecified
): IconToggleButtonColors =
MaterialTheme.colorScheme
- .defaultIconToggleButtonColors(LocalContentColor.current)
+ .defaultIconToggleButtonColors()
.copy(
containerColor = containerColor,
contentColor = contentColor,
@@ -845,18 +1207,22 @@
)
internal fun ColorScheme.defaultIconToggleButtonColors(
- localContentColor: Color
+ localContentColor: Color? = null,
): IconToggleButtonColors {
return defaultIconToggleButtonColorsCached
?: run {
IconToggleButtonColors(
containerColor = Color.Transparent,
- contentColor = localContentColor,
+ contentColor =
+ localContentColor
+ ?: fromToken(StandardIconButtonTokens.UnselectedColor),
disabledContainerColor = Color.Transparent,
disabledContentColor =
- localContentColor.copy(
+ localContentColor?.copy(
alpha = StandardIconButtonTokens.DisabledOpacity
- ),
+ )
+ ?: fromToken(StandardIconButtonTokens.DisabledColor)
+ .copy(alpha = StandardIconButtonTokens.DisabledOpacity),
checkedContainerColor = Color.Transparent,
checkedContentColor = fromToken(StandardIconButtonTokens.SelectedColor)
)
@@ -898,8 +1264,7 @@
return defaultFilledIconButtonColorsCached
?: IconButtonColors(
containerColor = fromToken(FilledIconButtonTokens.ContainerColor),
- contentColor =
- contentColorFor(fromToken(FilledIconButtonTokens.ContainerColor)),
+ contentColor = fromToken(FilledIconButtonTokens.Color),
disabledContainerColor =
fromToken(FilledIconButtonTokens.DisabledContainerColor)
.copy(alpha = FilledIconButtonTokens.DisabledContainerOpacity),
@@ -966,10 +1331,7 @@
.copy(alpha = FilledIconButtonTokens.DisabledOpacity),
checkedContainerColor =
fromToken(FilledIconButtonTokens.SelectedContainerColor),
- checkedContentColor =
- contentColorFor(
- fromToken(FilledIconButtonTokens.SelectedContainerColor)
- )
+ checkedContentColor = fromToken(FilledIconButtonTokens.SelectedColor)
)
.also { defaultFilledIconToggleButtonColorsCached = it }
}
@@ -1010,8 +1372,7 @@
return defaultFilledTonalIconButtonColorsCached
?: IconButtonColors(
containerColor = fromToken(FilledTonalIconButtonTokens.ContainerColor),
- contentColor =
- contentColorFor(fromToken(FilledTonalIconButtonTokens.ContainerColor)),
+ contentColor = fromToken(FilledTonalIconButtonTokens.Color),
disabledContainerColor =
fromToken(FilledTonalIconButtonTokens.DisabledContainerColor)
.copy(alpha = FilledTonalIconButtonTokens.DisabledContainerOpacity),
@@ -1065,10 +1426,7 @@
?: IconToggleButtonColors(
containerColor =
fromToken(FilledTonalIconButtonTokens.UnselectedContainerColor),
- contentColor =
- contentColorFor(
- fromToken(FilledTonalIconButtonTokens.UnselectedContainerColor)
- ),
+ contentColor = fromToken(FilledTonalIconButtonTokens.UnselectedColor),
disabledContainerColor =
fromToken(FilledTonalIconButtonTokens.DisabledContainerColor)
.copy(alpha = FilledTonalIconButtonTokens.DisabledContainerOpacity),
@@ -1077,23 +1435,19 @@
.copy(alpha = FilledTonalIconButtonTokens.DisabledOpacity),
checkedContainerColor =
fromToken(FilledTonalIconButtonTokens.SelectedContainerColor),
- checkedContentColor =
- contentColorFor(
- fromToken(FilledTonalIconButtonTokens.SelectedContainerColor)
- )
+ checkedContentColor = fromToken(FilledTonalIconButtonTokens.SelectedColor)
)
.also { defaultFilledTonalIconToggleButtonColorsCached = it }
}
/**
* Creates a [IconButtonColors] that represents the default colors used in a
- * [OutlinedIconButton].
+ * [OutlinedIconButton]. [LocalContentColor] will be applied to the icon and down the UI tree.
*/
@Composable
- fun outlinedIconButtonColors(): IconButtonColors {
- val colors =
- MaterialTheme.colorScheme.defaultOutlinedIconButtonColors(LocalContentColor.current)
+ fun outlinedIconButtonLocalContentColors(): IconButtonColors {
val contentColor = LocalContentColor.current
+ val colors = MaterialTheme.colorScheme.defaultOutlinedIconButtonColors(contentColor)
if (colors.contentColor == contentColor) {
return colors
} else {
@@ -1109,6 +1463,17 @@
* Creates a [IconButtonColors] that represents the default colors used in a
* [OutlinedIconButton].
*
+ * See [outlinedIconButtonLocalContentColors] for default values that applies
+ * [LocalContentColor] to the icon and down the UI tree.
+ */
+ @Composable
+ fun outlinedIconButtonColors(): IconButtonColors =
+ MaterialTheme.colorScheme.defaultOutlinedIconButtonColors()
+
+ /**
+ * Creates a [IconButtonColors] that represents the default colors used in a
+ * [OutlinedIconButton].
+ *
* @param containerColor the container color of this icon button when enabled.
* @param contentColor the content color of this icon button when enabled.
* @param disabledContainerColor the container color of this icon button when not enabled.
@@ -1117,13 +1482,13 @@
@Composable
fun outlinedIconButtonColors(
containerColor: Color = Color.Unspecified,
- contentColor: Color = LocalContentColor.current,
+ contentColor: Color = Color.Unspecified,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color =
contentColor.copy(alpha = OutlinedIconButtonTokens.DisabledOpacity)
): IconButtonColors =
MaterialTheme.colorScheme
- .defaultOutlinedIconButtonColors(LocalContentColor.current)
+ .defaultOutlinedIconButtonColors()
.copy(
containerColor = containerColor,
contentColor = contentColor,
@@ -1132,16 +1497,21 @@
)
internal fun ColorScheme.defaultOutlinedIconButtonColors(
- localContentColor: Color
+ localContentColor: Color? = null
): IconButtonColors {
return defaultOutlinedIconButtonColorsCached
?: run {
IconButtonColors(
containerColor = Color.Transparent,
- contentColor = localContentColor,
+ contentColor =
+ localContentColor ?: fromToken(OutlinedIconButtonTokens.Color),
disabledContainerColor = Color.Transparent,
disabledContentColor =
- localContentColor.copy(alpha = OutlinedIconButtonTokens.DisabledOpacity)
+ localContentColor?.copy(
+ alpha = OutlinedIconButtonTokens.DisabledOpacity
+ )
+ ?: fromToken(OutlinedIconButtonTokens.DisabledColor)
+ .copy(alpha = OutlinedIconButtonTokens.DisabledOpacity)
)
.also { defaultOutlinedIconButtonColorsCached = it }
}
@@ -1149,10 +1519,11 @@
/**
* Creates a [IconToggleButtonColors] that represents the default colors used in a
- * [OutlinedIconToggleButton].
+ * [OutlinedIconToggleButton]. [LocalContentColor] will be applied to the icon and down the UI
+ * tree.
*/
@Composable
- fun outlinedIconToggleButtonColors(): IconToggleButtonColors {
+ fun outlinedIconToggleButtonLocalContentColors(): IconToggleButtonColors {
val contentColor = LocalContentColor.current
val colors = MaterialTheme.colorScheme.defaultOutlinedIconToggleButtonColors(contentColor)
if (colors.contentColor == contentColor) {
@@ -1170,6 +1541,17 @@
* Creates a [IconToggleButtonColors] that represents the default colors used in a
* [OutlinedIconToggleButton].
*
+ * See [outlinedIconToggleButtonLocalContentColors] for default values that applies
+ * [LocalContentColor] to the icon and down the UI tree.
+ */
+ @Composable
+ fun outlinedIconToggleButtonColors(): IconToggleButtonColors =
+ MaterialTheme.colorScheme.defaultOutlinedIconToggleButtonColors()
+
+ /**
+ * Creates a [IconToggleButtonColors] that represents the default colors used in a
+ * [OutlinedIconToggleButton].
+ *
* @param containerColor the container color of this icon button when enabled.
* @param contentColor the content color of this icon button when enabled.
* @param disabledContainerColor the container color of this icon button when not enabled.
@@ -1180,7 +1562,7 @@
@Composable
fun outlinedIconToggleButtonColors(
containerColor: Color = Color.Unspecified,
- contentColor: Color = LocalContentColor.current,
+ contentColor: Color = Color.Unspecified,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color =
contentColor.copy(alpha = OutlinedIconButtonTokens.DisabledOpacity),
@@ -1188,7 +1570,7 @@
checkedContentColor: Color = contentColorFor(checkedContainerColor)
): IconToggleButtonColors =
MaterialTheme.colorScheme
- .defaultOutlinedIconToggleButtonColors(LocalContentColor.current)
+ .defaultOutlinedIconToggleButtonColors()
.copy(
containerColor = containerColor,
contentColor = contentColor,
@@ -1199,23 +1581,25 @@
)
internal fun ColorScheme.defaultOutlinedIconToggleButtonColors(
- localContentColor: Color
+ localContentColor: Color? = null
): IconToggleButtonColors {
return defaultOutlinedIconToggleButtonColorsCached
?: run {
IconToggleButtonColors(
containerColor = Color.Transparent,
- contentColor = localContentColor,
+ contentColor =
+ localContentColor
+ ?: fromToken(OutlinedIconButtonTokens.UnselectedColor),
disabledContainerColor = Color.Transparent,
disabledContentColor =
- localContentColor.copy(
+ localContentColor?.copy(
alpha = OutlinedIconButtonTokens.DisabledOpacity
- ),
- checkedContainerColor = fromToken(ColorSchemeKeyTokens.InverseSurface),
- checkedContentColor =
- contentColorFor(
- fromToken(OutlinedIconButtonTokens.SelectedContainerColor)
)
+ ?: fromToken(OutlinedIconButtonTokens.DisabledColor)
+ .copy(alpha = OutlinedIconButtonTokens.DisabledOpacity),
+ checkedContainerColor =
+ fromToken(OutlinedIconButtonTokens.SelectedContainerColor),
+ checkedContentColor = fromToken(OutlinedIconButtonTokens.SelectedColor)
)
.also { defaultOutlinedIconToggleButtonColorsCached = it }
}
@@ -1229,6 +1613,24 @@
* @param checked whether the icon button is checked
*/
@Composable
+ fun outlinedIconToggleButtonLocalContentColorBorder(
+ enabled: Boolean,
+ checked: Boolean
+ ): BorderStroke? {
+ if (checked) {
+ return null
+ }
+ return outlinedIconButtonLocalContentColorBorder(enabled)
+ }
+
+ /**
+ * Represents the [BorderStroke] for an [OutlinedIconButton], depending on its [enabled] and
+ * [checked] state.
+ *
+ * @param enabled whether the icon button is enabled
+ * @param checked whether the icon button is checked
+ */
+ @Composable
fun outlinedIconToggleButtonBorder(enabled: Boolean, checked: Boolean): BorderStroke? {
if (checked) {
return null
@@ -1236,6 +1638,18 @@
return outlinedIconButtonBorder(enabled)
}
+ @Composable
+ fun outlinedIconButtonLocalContentColorBorder(enabled: Boolean): BorderStroke? {
+ val outlineColor = LocalContentColor.current
+ val color: Color =
+ if (enabled) {
+ outlineColor
+ } else {
+ outlineColor.copy(alpha = OutlinedIconButtonTokens.DisabledContainerOpacity)
+ }
+ return remember(color) { BorderStroke(SmallIconButtonTokens.OutlinedOutlineWidth, color) }
+ }
+
/**
* Represents the [BorderStroke] for an [OutlinedIconButton], depending on its [enabled] state.
*
@@ -1243,13 +1657,12 @@
*/
@Composable
fun outlinedIconButtonBorder(enabled: Boolean): BorderStroke {
+ val outlineColor = OutlinedIconButtonTokens.OutlineColor.value
val color: Color =
if (enabled) {
- LocalContentColor.current
+ outlineColor
} else {
- LocalContentColor.current.copy(
- alpha = OutlinedIconButtonTokens.DisabledContainerOpacity
- )
+ outlineColor.copy(alpha = OutlinedIconButtonTokens.DisabledContainerOpacity)
}
return remember(color) { BorderStroke(SmallIconButtonTokens.OutlinedOutlineWidth, color) }
}
@@ -1283,6 +1696,13 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
+ /** Default pressed shape for any extra small icon button. */
+ val xSmallPressedShape: Shape
+ @Composable get() = XSmallIconButtonTokens.PressedContainerShape.value
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
/** Default shape for any small icon button. */
val smallRoundShape: Shape
@Composable get() = SmallIconButtonTokens.ContainerShapeRound.value
@@ -1297,6 +1717,13 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
+ /** Default pressed shape for any small icon button. */
+ val smallPressedShape: Shape
+ @Composable get() = SmallIconButtonTokens.PressedContainerShape.value
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
/** Default shape for any medium icon button. */
val mediumRoundShape: Shape
@Composable get() = MediumIconButtonTokens.ContainerShapeRound.value
@@ -1311,6 +1738,13 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
+ /** Default pressed shape for any medium icon button. */
+ val mediumPressedShape: Shape
+ @Composable get() = MediumIconButtonTokens.PressedContainerShape.value
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
/** Default shape for any large icon button. */
val largeRoundShape: Shape
@Composable get() = LargeIconButtonTokens.ContainerShapeRound.value
@@ -1325,6 +1759,13 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
+ /** Default pressed shape for any large icon button. */
+ val largePressedShape: Shape
+ @Composable get() = LargeIconButtonTokens.PressedContainerShape.value
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
/** Default shape for any xlarge icon button. */
val xLargeRoundShape: Shape
@Composable get() = XLargeIconButtonTokens.ContainerShapeRound.value
@@ -1339,6 +1780,29 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalMaterial3ExpressiveApi
@ExperimentalMaterial3ExpressiveApi
+ /** Default pressed shape for any extra large icon button. */
+ val xLargePressedShape: Shape
+ @Composable get() = XLargeIconButtonTokens.PressedContainerShape.value
+
+ /**
+ * Creates a [ButtonShapes] that correspond to the shapes in the default, pressed, and checked
+ * states. Toggle button will morph between these shapes as long as the shapes are all
+ * [CornerBasedShape]s.
+ *
+ * @param shape the unchecked shape for [ButtonShapes]
+ * @param pressedShape the unchecked shape for [ButtonShapes]
+ * @param checkedShape the unchecked shape for [ButtonShapes]
+ */
+ @ExperimentalMaterial3ExpressiveApi
+ @Composable
+ fun shapes(shape: Shape, pressedShape: Shape, checkedShape: Shape): IconButtonShapes =
+ remember(shape, pressedShape, checkedShape) {
+ IconButtonShapes(shape, pressedShape, checkedShape)
+ }
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
/** Default container for any extra small icon button. */
val xSmallIconSize: Dp = XSmallIconButtonTokens.IconSize
@@ -1720,3 +2184,63 @@
return result
}
}
+
+/**
+ * The shapes that will be used in toggle buttons. Toggle button will morph between these three
+ * shapes depending on the interaction of the toggle button, assuming all of the shapes are
+ * [CornerBasedShape]s.
+ *
+ * @property shape is the unchecked shape.
+ * @property pressedShape is the pressed shape.
+ * @property checkedShape is the checked shape.
+ */
+@ExperimentalMaterial3ExpressiveApi
+class IconButtonShapes(val shape: Shape, val pressedShape: Shape, val checkedShape: Shape) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is IconButtonShapes) return false
+
+ if (shape != other.shape) return false
+ if (pressedShape != other.pressedShape) return false
+ if (checkedShape != other.checkedShape) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = shape.hashCode()
+ result = 31 * result + pressedShape.hashCode()
+ result = 31 * result + checkedShape.hashCode()
+
+ return result
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal val IconButtonShapes.isCornerBasedShape: Boolean
+ get() =
+ shape is RoundedCornerShape &&
+ pressedShape is CornerBasedShape &&
+ checkedShape is CornerBasedShape
+
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+private fun shapeByInteraction(
+ shapes: IconButtonShapes,
+ pressed: Boolean,
+ checked: Boolean,
+ animationSpec: FiniteAnimationSpec<Float>
+): Shape {
+ val shape =
+ if (pressed) {
+ shapes.pressedShape
+ } else if (checked) {
+ shapes.checkedShape
+ } else shapes.shape
+
+ if (shapes.isCornerBasedShape) {
+ return key(shapes) { rememberAnimatedShape(shape as RoundedCornerShape, animationSpec) }
+ }
+ return shape
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
index ec24bc8..b0ac90c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
import androidx.graphics.shapes.CornerRounding
+import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.TransformResult
import androidx.graphics.shapes.circle
@@ -46,11 +47,23 @@
import kotlin.math.sin
/**
- * Returns a normalized [Path] that is remembered across compositions for this [RoundedPolygon].
+ * Returns a [Path] for this [Morph].
+ *
+ * @param progress the [Morph]'s progress
+ * @param path a [Path] to rewind and set with the new path data. In case provided, this Path would
+ * be the returned one.
+ * @param startAngle an angle to rotate the [Path] to start drawing from
+ */
+@ExperimentalMaterial3ExpressiveApi
+fun Morph.toPath(progress: Float, path: Path = Path(), startAngle: Int = 0): Path {
+ return this.toPath(path = path, progress = progress, startAngle = startAngle)
+}
+
+/**
+ * Returns a [Path] that is remembered across compositions for this [RoundedPolygon].
*
* @param startAngle an angle to rotate the Material shape's path to start drawing from. The
* rotation pivot is set to be the shape's centerX and centerY coordinates.
- * @see RoundedPolygon.normalized
*/
@ExperimentalMaterial3ExpressiveApi
@Composable
@@ -72,19 +85,25 @@
fun RoundedPolygon.toShape(startAngle: Int = 0): Shape {
return remember(this, startAngle) {
object : Shape {
- private val path: Path = toPath(startAngle = startAngle)
+ // Store the Path we convert from the RoundedPolygon here. The path we will be
+ // manipulating and using on the createOutline would be a copy of this to ensure we
+ // don't mutate the original.
+ private val shapePath: Path = toPath(startAngle = startAngle)
+ private val workPath: Path = Path()
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
+ workPath.rewind()
+ workPath.addPath(shapePath)
val scaleMatrix = Matrix().apply { scale(x = size.width, y = size.height) }
// Scale and translate the path to align its center with the available size
// center.
- path.transform(scaleMatrix)
- path.translate(size.center - path.getBounds().center)
- return Outline.Generic(path)
+ workPath.transform(scaleMatrix)
+ workPath.translate(size.center - workPath.getBounds().center)
+ return Outline.Generic(workPath)
}
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt
index c39448b..b19a579 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt
@@ -18,10 +18,11 @@
import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.spring
+import androidx.compose.material3.tokens.ExpressiveMotionTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
+import androidx.compose.material3.tokens.StandardMotionTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@@ -225,27 +226,45 @@
fun standardMotionScheme(): MotionScheme =
object : MotionScheme {
override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = StandardSpatialDampingRatio, stiffness = 700f)
+ return spring(
+ dampingRatio = StandardMotionTokens.SpringDefaultSpatialDamping,
+ stiffness = StandardMotionTokens.SpringDefaultSpatialStiffness
+ )
}
override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = StandardSpatialDampingRatio, stiffness = 1400f)
+ return spring(
+ dampingRatio = StandardMotionTokens.SpringFastSpatialDamping,
+ stiffness = StandardMotionTokens.SpringFastSpatialStiffness
+ )
}
override fun <T> slowSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = StandardSpatialDampingRatio, stiffness = 300f)
+ return spring(
+ dampingRatio = StandardMotionTokens.SpringSlowSpatialDamping,
+ stiffness = StandardMotionTokens.SpringSlowSpatialStiffness
+ )
}
override fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = EffectsDampingRatio, stiffness = EffectsDefaultStiffness)
+ return spring(
+ dampingRatio = StandardMotionTokens.SpringDefaultEffectsDamping,
+ stiffness = StandardMotionTokens.SpringDefaultEffectsStiffness
+ )
}
override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = EffectsDampingRatio, stiffness = EffectsFastStiffness)
+ return spring(
+ dampingRatio = StandardMotionTokens.SpringFastEffectsDamping,
+ stiffness = StandardMotionTokens.SpringFastEffectsStiffness
+ )
}
override fun <T> slowEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = EffectsDampingRatio, stiffness = EffectsSlowStiffness)
+ return spring(
+ dampingRatio = StandardMotionTokens.SpringSlowEffectsDamping,
+ stiffness = StandardMotionTokens.SpringSlowEffectsStiffness
+ )
}
}
@@ -254,27 +273,45 @@
fun expressiveMotionScheme(): MotionScheme =
object : MotionScheme {
override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = 0.8f, stiffness = 380f)
+ return spring(
+ dampingRatio = ExpressiveMotionTokens.SpringDefaultSpatialDamping,
+ stiffness = ExpressiveMotionTokens.SpringDefaultSpatialStiffness
+ )
}
override fun <T> fastSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = 0.6f, stiffness = 800f)
+ return spring(
+ dampingRatio = ExpressiveMotionTokens.SpringFastSpatialDamping,
+ stiffness = ExpressiveMotionTokens.SpringFastSpatialStiffness
+ )
}
override fun <T> slowSpatialSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = 0.8f, stiffness = 200f)
+ return spring(
+ dampingRatio = ExpressiveMotionTokens.SpringSlowSpatialDamping,
+ stiffness = ExpressiveMotionTokens.SpringSlowSpatialStiffness
+ )
}
override fun <T> defaultEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = EffectsDampingRatio, stiffness = EffectsDefaultStiffness)
+ return spring(
+ dampingRatio = ExpressiveMotionTokens.SpringDefaultEffectsDamping,
+ stiffness = ExpressiveMotionTokens.SpringDefaultEffectsStiffness
+ )
}
override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = EffectsDampingRatio, stiffness = EffectsFastStiffness)
+ return spring(
+ dampingRatio = ExpressiveMotionTokens.SpringFastEffectsDamping,
+ stiffness = ExpressiveMotionTokens.SpringFastEffectsStiffness
+ )
}
override fun <T> slowEffectsSpec(): FiniteAnimationSpec<T> {
- return spring(dampingRatio = EffectsDampingRatio, stiffness = EffectsSlowStiffness)
+ return spring(
+ dampingRatio = ExpressiveMotionTokens.SpringSlowEffectsDamping,
+ stiffness = ExpressiveMotionTokens.SpringSlowEffectsStiffness
+ )
}
}
@@ -323,12 +360,3 @@
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal inline fun <reified T> MotionSchemeKeyTokens.value(): FiniteAnimationSpec<T> =
MaterialTheme.motionScheme.fromToken(this)
-
-// Common effects damping and stiffness values for both Standard and Expressive
-private const val EffectsDampingRatio = Spring.DampingRatioNoBouncy
-private const val EffectsDefaultStiffness = 1600f
-private const val EffectsFastStiffness = 3800f
-private const val EffectsSlowStiffness = 800f
-
-// Common damping for Standard spatial specs
-private const val StandardSpatialDampingRatio = 0.9f
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index e44e841..4cf6819 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -16,10 +16,11 @@
package androidx.compose.material3
-import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -27,10 +28,14 @@
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.CornerSize
-import androidx.compose.foundation.shape.GenericShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.internal.ProvideContentColorTextStyle
+import androidx.compose.material3.internal.rememberAnimatedShape
+import androidx.compose.material3.tokens.BaselineButtonTokens
+import androidx.compose.material3.tokens.MotionSchemeKeyTokens
import androidx.compose.material3.tokens.SplitButtonSmallTokens
import androidx.compose.material3.tokens.StateTokens
import androidx.compose.runtime.Composable
@@ -40,11 +45,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.RoundRect
-import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.layout.Layout
@@ -53,9 +53,7 @@
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
@@ -158,17 +156,18 @@
SplitButtonDefaults.LeadingButton(
onClick = onLeadingButtonClick,
enabled = enabled,
- shape = SplitButtonDefaults.leadingButtonShape(innerCornerSize),
+ shapes = SplitButtonDefaults.leadingButtonShapes(innerCornerSize),
content = leadingContent
)
},
trailingButton = {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
onClick = onTrailingButtonClick,
modifier = Modifier,
enabled = enabled,
checked = checked,
- startCornerSize = innerCornerSize,
+ shapes =
+ SplitButtonDefaults.trailingButtonShapes(startCornerSize = innerCornerSize),
content = trailingContent,
)
},
@@ -229,7 +228,7 @@
onClick = onLeadingButtonClick,
modifier = Modifier,
enabled = enabled,
- endCornerSize = innerCornerSize,
+ shapes = SplitButtonDefaults.leadingButtonShapes(endCornerSize = innerCornerSize),
content = leadingContent,
)
},
@@ -238,8 +237,8 @@
onClick = onTrailingButtonClick,
modifier = Modifier,
enabled = enabled,
- startCornerSize = innerCornerSize,
checked = checked,
+ shapes = SplitButtonDefaults.trailingButtonShapes(innerCornerSize),
content = trailingContent,
)
},
@@ -303,7 +302,7 @@
onClick = onLeadingButtonClick,
modifier = Modifier,
enabled = enabled,
- endCornerSize = innerCornerSize,
+ shapes = SplitButtonDefaults.leadingButtonShapes(endCornerSize = innerCornerSize),
content = leadingContent,
)
},
@@ -312,8 +311,9 @@
onClick = onTrailingButtonClick,
modifier = Modifier,
enabled = enabled,
- startCornerSize = innerCornerSize,
checked = checked,
+ shapes =
+ SplitButtonDefaults.trailingButtonShapes(startCornerSize = innerCornerSize),
content = trailingContent
)
},
@@ -377,7 +377,7 @@
onClick = onLeadingButtonClick,
modifier = Modifier,
enabled = enabled,
- endCornerSize = innerCornerSize,
+ shapes = SplitButtonDefaults.leadingButtonShapes(innerCornerSize),
content = leadingContent,
)
},
@@ -386,7 +386,7 @@
onClick = onTrailingButtonClick,
modifier = Modifier,
enabled = enabled,
- startCornerSize = innerCornerSize,
+ shapes = SplitButtonDefaults.trailingButtonShapes(innerCornerSize),
checked = checked,
content = trailingContent
)
@@ -462,32 +462,41 @@
)
}
-// TODO Replace default value with tokens
/** Contains default values used by [SplitButton] and its style variants. */
@ExperimentalMaterial3ExpressiveApi
object SplitButtonDefaults {
/** Default icon size for the leading button */
- val LeadingIconSize = 20.dp
+ val LeadingIconSize = BaselineButtonTokens.IconSize
/** Default icon size for the trailing button */
- val TrailingIconSize = 22.dp
+ val TrailingIconSize = SplitButtonSmallTokens.TrailingIconSize
/** Default spacing between the `leading` and `trailing` button */
val Spacing = SplitButtonSmallTokens.BetweenSpace
/** Default size for the leading button end corners and trailing button start corners */
- val InnerCornerSize = CornerSize(4.dp)
+ // TODO update token to dp size and use it here
+ val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
+ private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
/**
* Default percentage size for the leading button start corners and trailing button end corners
*/
- val OuterCornerSize = CornerSize(50)
+ val OuterCornerSize = ShapeDefaults.CornerFull
/** Default content padding of the leading button */
- val LeadingButtonContentPadding = PaddingValues(16.dp, 10.dp, 12.dp, 10.dp)
+ val LeadingButtonContentPadding =
+ PaddingValues(
+ start = SplitButtonSmallTokens.LeadingButtonLeadingSpace,
+ end = SplitButtonSmallTokens.LeadingButtonTrailingSpace
+ )
/** Default content padding of the trailing button */
- val TrailingButtonContentPadding = PaddingValues(horizontal = 13.dp)
+ val TrailingButtonContentPadding =
+ PaddingValues(
+ start = SplitButtonSmallTokens.TrailingButtonLeadingSpace,
+ end = SplitButtonSmallTokens.TrailingButtonTrailingSpace
+ )
/**
* Default minimum width of the [LeadingButton], applies to all 4 variants of the split button
@@ -503,24 +512,57 @@
/** Default minimum width of the [TrailingButton]. */
private val TrailingButtonMinWidth = LeadingButtonMinWidth
- /** Trailng button state layer alpha when in checked state */
+ /** Trailing button state layer alpha when in checked state */
private const val TrailingButtonStateLayerAlpha = StateTokens.PressedStateLayerOpacity
+ /** Default shape of the leading button. */
+ private fun leadingButtonShape(endCornerSize: CornerSize = InnerCornerSize) =
+ RoundedCornerShape(OuterCornerSize, endCornerSize, endCornerSize, OuterCornerSize)
+
+ private val LeadingPressedShape =
+ RoundedCornerShape(
+ topStart = OuterCornerSize,
+ bottomStart = OuterCornerSize,
+ topEnd = InnerCornerSizePressed,
+ bottomEnd = InnerCornerSizePressed
+ )
+ private val TrailingPressedShape =
+ RoundedCornerShape(
+ topStart = InnerCornerSizePressed,
+ bottomStart = InnerCornerSizePressed,
+ topEnd = OuterCornerSize,
+ bottomEnd = OuterCornerSize
+ )
+ private val TrailingCheckedShape = CircleShape
+
/**
- * Default shape of the leading button.
+ * Default shapes for the leading button. This defines the shapes the leading button should
+ * morph to when enabled, pressed etc.
*
* @param endCornerSize the size for top end corner and bottom end corner
*/
- fun leadingButtonShape(endCornerSize: CornerSize = InnerCornerSize) =
- RoundedCornerShape(OuterCornerSize, endCornerSize, endCornerSize, OuterCornerSize)
+ fun leadingButtonShapes(endCornerSize: CornerSize = InnerCornerSize) =
+ SplitButtonShapes(
+ shape = leadingButtonShape(endCornerSize),
+ pressedShape = LeadingPressedShape,
+ checkedShape = null,
+ )
+
+ /** Default shape of the trailing button */
+ private fun trailingButtonShape(startCornerSize: CornerSize = InnerCornerSize) =
+ RoundedCornerShape(startCornerSize, OuterCornerSize, OuterCornerSize, startCornerSize)
/**
- * Default shape of the trailing button
+ * Default shapes for the trailing button
*
* @param startCornerSize the size for top start corner and bottom start corner
*/
- fun trailingButtonShape(startCornerSize: CornerSize = InnerCornerSize) =
- RoundedCornerShape(startCornerSize, OuterCornerSize, OuterCornerSize, startCornerSize)
+ fun trailingButtonShapes(startCornerSize: CornerSize = InnerCornerSize) =
+ SplitButtonShapes(
+ shape = trailingButtonShape(startCornerSize),
+ pressedShape = TrailingPressedShape,
+ checkedShape = TrailingCheckedShape
+ )
/**
* Create a default `leading` button that has the same visual as a Filled[Button]. To create a
@@ -532,9 +574,10 @@
* @param onClick called when the button is clicked
* @param modifier the [Modifier] to be applied to this button.
* @param enabled controls the enabled state of the split button. When `false`, this component
- * will
- * @param shape defines the shape of this button's container, border (when [border] is not
- * null), and shadow (when using [elevation])
+ * will not respond to user input, and it will appear visually disabled and disabled to
+ * accessibility services.
+ * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
+ * on the user's interaction with the button.
* @param colors [ButtonColors] that will be used to resolve the colors for this button in
* different states. See [ButtonDefaults.buttonColors].
* @param elevation [ButtonElevation] used to resolve the elevation for this button in different
@@ -556,7 +599,7 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- shape: Shape = leadingButtonShape(),
+ shapes: SplitButtonShapes = leadingButtonShapes(),
colors: ButtonColors = ButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
border: BorderStroke? = null,
@@ -566,11 +609,16 @@
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+
+ // TODO Load the motionScheme tokens from the component tokens file
+ val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
+ val pressed by interactionSource.collectIsPressedAsState()
+
Surface(
onClick = onClick,
modifier = modifier.semantics { role = Role.Button },
enabled = enabled,
- shape = shape,
+ shape = shapeByInteraction(shapes, pressed, checked = false, defaultAnimationSpec),
color = colors.containerColor,
contentColor = colors.contentColor,
shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
@@ -593,80 +641,9 @@
}
/**
- * Create a default `trailing` button that has the same visual as a Filled[Button]. For a
- * `trailing` button that offers corner morphing animation, see [AnimatedTrailingButton].
- *
- * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
- * can be passed in. For example, [ElevatedButton].
- *
- * The default text style for internal [Text] components will be set to [Typography.labelLarge].
- *
- * @param onClick called when the button is clicked
- * @param modifier the [Modifier] to be applied to this button.
- * @param shape defines the shape of this button's container, border (when [border] is not
- * null), and shadow (when using [elevation]). [TrailingButton]
- * @param enabled controls the enabled state of the split button. When `false`, this component
- * will
- * @param colors [ButtonColors] that will be used to resolve the colors for this button in
- * different states. See [ButtonDefaults.buttonColors].
- * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
- * states. This controls the size of the shadow below the button. See
- * [ButtonElevation.shadowElevation].
- * @param border the border to draw around the container of this button contentPadding the
- * spacing values to apply internally between the container and the content
- * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
- * emitting [Interaction]s for this button. You can use this to change the button's appearance
- * or preview the button in different states. Note that if `null` is provided, interactions
- * will still happen internally.
- * @param content the content to be placed inside a button
- */
- @ExperimentalMaterial3ExpressiveApi
- @Composable
- fun TrailingButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- shape: Shape = trailingButtonShape(),
- enabled: Boolean = true,
- colors: ButtonColors = ButtonDefaults.buttonColors(),
- elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
- border: BorderStroke? = null,
- interactionSource: MutableInteractionSource? = null,
- content: @Composable RowScope.() -> Unit
- ) {
- @Suppress("NAME_SHADOWING")
- val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
-
- Surface(
- onClick = onClick,
- modifier = modifier.semantics { role = Role.Button },
- enabled = enabled,
- shape = shape,
- color = colors.containerColor,
- contentColor = colors.contentColor,
- shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
- border = border,
- interactionSource = interactionSource
- ) {
- ProvideContentColorTextStyle(
- contentColor = colors.contentColor,
- textStyle = MaterialTheme.typography.labelLarge
- ) {
- Row(
- Modifier.defaultMinSize(
- minWidth = TrailingButtonMinWidth,
- minHeight = MinHeight
- ),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- content = content
- )
- }
- }
- }
-
- /**
- * Create a animated `trailing` button that has the same visual as a Filled[Button]. When
- * [checked] is updated from `false` to `true`, the buttons corners will morph to `full`.
+ * Creates a `trailing` button that has the same visual as a Filled[Button]. When [checked] is
+ * updated from `false` to `true`, the buttons corners will morph to `full` by default. Pressed
+ * shape and checked shape can be customized via [shapes] param.
*
* To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
* can be passed in. For example, [ElevatedButton].
@@ -678,8 +655,10 @@
* trigger the corner morphing animation to reflect the updated state.
* @param modifier the [Modifier] to be applied to this button.
* @param enabled controls the enabled state of the split button. When `false`, this component
- * will
- * @param startCornerSize The size for top start corner and bottom start corner
+ * will not respond to user input, and it will appear visually disabled and disabled to
+ * accessibility services.
+ * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
+ * on the user's interaction with the button.
* @param colors [ButtonColors] that will be used to resolve the colors for this button in
* different states. See [ButtonDefaults.buttonColors].
* @param elevation [ButtonElevation] used to resolve the elevation for this button in different
@@ -697,12 +676,12 @@
*/
@Composable
@ExperimentalMaterial3ExpressiveApi
- fun AnimatedTrailingButton(
+ fun TrailingButton(
onClick: () -> Unit,
checked: Boolean,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- startCornerSize: CornerSize = InnerCornerSize,
+ shapes: SplitButtonShapes = trailingButtonShapes(),
colors: ButtonColors = ButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
border: BorderStroke? = null,
@@ -710,102 +689,90 @@
interactionSource: MutableInteractionSource? = null,
content: @Composable RowScope.() -> Unit
) {
- val cornerMorphProgress: Float by animateFloatAsState(if (checked) 1f else 0f)
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
- val density = LocalDensity.current
- val shape = rememberTrailingButtonShape(density, startCornerSize) { cornerMorphProgress }
- TrailingButton(
+ // TODO Load the motionScheme tokens from the component tokens file
+ val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
+ val pressed by interactionSource.collectIsPressedAsState()
+
+ val density = LocalDensity.current
+ val shape = shapeByInteraction(shapes, pressed, checked, defaultAnimationSpec)
+
+ Surface(
onClick = onClick,
modifier =
- modifier.drawWithContent {
- drawContent()
- if (checked) {
- drawOutline(
- outline = shape.createOutline(size, layoutDirection, density),
- color = colors.contentColor,
- alpha = TrailingButtonStateLayerAlpha
- )
+ modifier
+ .drawWithContent {
+ drawContent()
+ if (checked) {
+ drawOutline(
+ outline = shape.createOutline(size, layoutDirection, density),
+ color = colors.contentColor,
+ alpha = TrailingButtonStateLayerAlpha
+ )
+ }
}
- },
+ .semantics { role = Role.Button },
enabled = enabled,
- colors = colors,
- elevation = elevation,
- border = border,
- interactionSource = interactionSource,
shape = shape,
+ color = colors.containerColor,
+ contentColor = colors.contentColor,
+ shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
+ border = border,
+ interactionSource = interactionSource
) {
- Row(
- modifier =
- modifier.opticalCentering(
- trailingButtonShape(if (checked) OuterCornerSize else startCornerSize),
- contentPadding
- ),
- content = content
- )
+ ProvideContentColorTextStyle(
+ contentColor = colors.contentColor,
+ textStyle = MaterialTheme.typography.labelLarge
+ ) {
+ Row(
+ Modifier.defaultMinSize(
+ minWidth = TrailingButtonMinWidth,
+ minHeight = MinHeight
+ )
+ .then(
+ when (shape) {
+ is ShapeWithOpticalCentering -> {
+ Modifier.opticalCentering(
+ shape = shape,
+ basePadding = contentPadding
+ )
+ }
+ is CornerBasedShape -> {
+ Modifier.opticalCentering(
+ shape = shape,
+ basePadding = contentPadding
+ )
+ }
+ else -> {
+ Modifier.padding(contentPadding)
+ }
+ }
+ ),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ content = content
+ )
+ }
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-private fun rememberTrailingButtonShape(
- density: Density,
- startCornerSize: CornerSize,
- progress: () -> Float
-) =
- remember(density, progress) {
- GenericShape { size, layoutDirection ->
- val rect = Rect(Offset.Zero, size)
- val startCornerSizePx = startCornerSize.toPx(size, density)
- val originalStartCornerRadius = CornerRadius(startCornerSizePx)
- val endCornerRadius =
- CornerRadius(SplitButtonDefaults.OuterCornerSize.toPx(size, density))
- val originalRoundRect =
- if (layoutDirection == LayoutDirection.Rtl) {
- RoundRect(
- rect,
- endCornerRadius,
- originalStartCornerRadius,
- originalStartCornerRadius,
- endCornerRadius
- )
- } else {
- RoundRect(
- rect,
- originalStartCornerRadius,
- endCornerRadius,
- endCornerRadius,
- originalStartCornerRadius
- )
- }
-
- val endRoundRect =
- RoundRect(
- rect,
- CornerRadius(SplitButtonDefaults.OuterCornerSize.toPx(size, density))
- )
-
- val roundRect = lerp(originalRoundRect, endRoundRect, progress.invoke())
- addRoundRect(roundRect)
- }
- }
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
private fun TonalLeadingButton(
onClick: () -> Unit,
modifier: Modifier,
enabled: Boolean,
- endCornerSize: CornerSize,
+ shapes: SplitButtonShapes,
content: @Composable RowScope.() -> Unit
) {
SplitButtonDefaults.LeadingButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- shape = SplitButtonDefaults.leadingButtonShape(endCornerSize),
+ shapes = shapes,
colors = ButtonDefaults.filledTonalButtonColors(),
elevation = ButtonDefaults.filledTonalButtonElevation(),
border = null,
@@ -820,14 +787,14 @@
checked: Boolean,
modifier: Modifier,
enabled: Boolean,
- startCornerSize: CornerSize,
+ shapes: SplitButtonShapes,
content: @Composable RowScope.() -> Unit
) {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- startCornerSize = startCornerSize,
+ shapes = shapes,
checked = checked,
colors = ButtonDefaults.filledTonalButtonColors(),
elevation = ButtonDefaults.filledTonalButtonElevation(),
@@ -842,14 +809,14 @@
onClick: () -> Unit,
modifier: Modifier,
enabled: Boolean,
- endCornerSize: CornerSize,
+ shapes: SplitButtonShapes,
content: @Composable RowScope.() -> Unit
) {
SplitButtonDefaults.LeadingButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- shape = SplitButtonDefaults.leadingButtonShape(endCornerSize),
+ shapes = shapes,
colors = ButtonDefaults.outlinedButtonColors(),
elevation = null,
border = ButtonDefaults.outlinedButtonBorder(enabled),
@@ -864,14 +831,14 @@
checked: Boolean,
modifier: Modifier,
enabled: Boolean,
- startCornerSize: CornerSize,
+ shapes: SplitButtonShapes,
content: @Composable RowScope.() -> Unit
) {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- startCornerSize = startCornerSize,
+ shapes = shapes,
checked = checked,
colors = ButtonDefaults.outlinedButtonColors(),
elevation = null,
@@ -886,14 +853,14 @@
onClick: () -> Unit,
modifier: Modifier,
enabled: Boolean,
- endCornerSize: CornerSize,
+ shapes: SplitButtonShapes,
content: @Composable RowScope.() -> Unit
) {
SplitButtonDefaults.LeadingButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- shape = SplitButtonDefaults.leadingButtonShape(endCornerSize),
+ shapes = shapes,
colors = ButtonDefaults.elevatedButtonColors(),
elevation = ButtonDefaults.elevatedButtonElevation(),
border = null,
@@ -908,14 +875,14 @@
checked: Boolean,
modifier: Modifier,
enabled: Boolean,
- startCornerSize: CornerSize,
+ shapes: SplitButtonShapes,
content: @Composable RowScope.() -> Unit
) {
- SplitButtonDefaults.AnimatedTrailingButton(
+ SplitButtonDefaults.TrailingButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
- startCornerSize = startCornerSize,
+ shapes = shapes,
checked = checked,
colors = ButtonDefaults.elevatedButtonColors(),
elevation = ButtonDefaults.elevatedButtonElevation(),
@@ -924,5 +891,43 @@
)
}
+@Composable
+private fun shapeByInteraction(
+ shapes: SplitButtonShapes,
+ pressed: Boolean,
+ checked: Boolean,
+ animationSpec: FiniteAnimationSpec<Float>
+): Shape {
+ val shape =
+ if (pressed) {
+ shapes.pressedShape ?: shapes.shape
+ } else if (checked) {
+ shapes.checkedShape ?: shapes.shape
+ } else shapes.shape
+
+ if (shapes.hasRoundedCornerShapes) {
+ return rememberAnimatedShape(shape as RoundedCornerShape, animationSpec)
+ }
+ return shape
+}
+
+/**
+ * The shapes that will be used in [SplitButton]. Split button will morph between these shapes
+ * depending on the interaction of the buttons, assuming all of the shapes are [CornerBasedShape]s.
+ *
+ * @property shape is the default shape.
+ * @property pressedShape is the pressed shape.
+ * @property checkedShape is the checked shape.
+ */
+data class SplitButtonShapes(val shape: Shape, val pressedShape: Shape?, val checkedShape: Shape?)
+
+internal val SplitButtonShapes.hasRoundedCornerShapes: Boolean
+ get() {
+ // Ignore null shapes and only check default shape for RoundedCorner
+ if (pressedShape != null && pressedShape !is RoundedCornerShape) return false
+ if (checkedShape != null && checkedShape !is RoundedCornerShape) return false
+ return shape is RoundedCornerShape
+ }
+
private const val LeadingButtonLayoutId = "LeadingButton"
private const val TrailingButtonLayoutId = "TrailingButton"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt
index 4cdbe44..d75437c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ToggleButton.kt
@@ -16,8 +16,6 @@
package androidx.compose.material3
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
@@ -32,6 +30,7 @@
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.internal.ProvideContentColorTextStyle
+import androidx.compose.material3.internal.rememberAnimatedShape
import androidx.compose.material3.tokens.ButtonSmallTokens
import androidx.compose.material3.tokens.ElevatedButtonTokens
import androidx.compose.material3.tokens.FilledButtonTokens
@@ -40,31 +39,19 @@
import androidx.compose.material3.tokens.TonalButtonTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.key
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import kotlin.jvm.JvmInline
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
/**
* TODO link to mio page when available.
@@ -459,8 +446,11 @@
* @param pressedShape the unchecked shape for [ButtonShapes]
* @param checkedShape the unchecked shape for [ButtonShapes]
*/
+ @Composable
fun shapes(shape: Shape, pressedShape: Shape, checkedShape: Shape): ButtonShapes =
- ButtonShapes(shape, pressedShape, checkedShape)
+ remember(shape, pressedShape, checkedShape) {
+ ButtonShapes(shape, pressedShape, checkedShape)
+ }
/** A round shape that can be used for all [ToggleButton]s and its variants */
val roundShape: Shape
@@ -856,13 +846,32 @@
* @property pressedShape is the pressed shape.
* @property checkedShape is the checked shape.
*/
-data class ButtonShapes(val shape: Shape, val pressedShape: Shape, val checkedShape: Shape)
+class ButtonShapes(val shape: Shape, val pressedShape: Shape, val checkedShape: Shape) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is ButtonShapes) return false
-internal val ButtonShapes.isCornerBasedShape: Boolean
+ if (shape != other.shape) return false
+ if (pressedShape != other.pressedShape) return false
+ if (checkedShape != other.checkedShape) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = shape.hashCode()
+ result = 31 * result + pressedShape.hashCode()
+ result = 31 * result + checkedShape.hashCode()
+
+ return result
+ }
+}
+
+internal val ButtonShapes.hasRoundedCornerShapes: Boolean
get() =
shape is RoundedCornerShape &&
- pressedShape is CornerBasedShape &&
- checkedShape is CornerBasedShape
+ pressedShape is RoundedCornerShape &&
+ checkedShape is RoundedCornerShape
@Composable
private fun shapeByInteraction(
@@ -871,163 +880,20 @@
checked: Boolean,
animationSpec: FiniteAnimationSpec<Float>
): Shape {
- if (shapes.isCornerBasedShape) {
- return rememberAnimatedShape(shapes, checked, animationSpec, pressed)
- }
+ val shape =
+ if (pressed) {
+ shapes.pressedShape
+ } else if (checked) {
+ shapes.checkedShape
+ } else shapes.shape
- if (pressed) {
- return shapes.pressedShape
- }
-
- if (checked) {
- return shapes.checkedShape
- }
-
- return shapes.shape
-}
-
-@Composable
-private fun rememberAnimatedShape(
- shapes: ButtonShapes,
- checked: Boolean,
- animationSpec: FiniteAnimationSpec<Float>,
- pressed: Boolean
-): Shape {
- val defaultShape = shapes.shape as CornerBasedShape
- val pressedShape = shapes.pressedShape as CornerBasedShape
- val checkedShape = shapes.checkedShape as CornerBasedShape
- val state =
- remember(shapes, animationSpec) {
- AnimatedShapeState(
- startShape = if (checked) checkedShape else defaultShape,
- defaultShape = defaultShape,
- pressedShape = pressedShape,
- checkedShape = checkedShape,
- spec = animationSpec,
+ if (shapes.hasRoundedCornerShapes)
+ return key(shapes) {
+ rememberAnimatedShape(
+ shape as RoundedCornerShape,
+ animationSpec,
)
}
- val channel = remember { Channel<AnimatedShapeValue>(Channel.CONFLATED) }
- val targetValue =
- when {
- pressed -> AnimatedShapeValue.Pressed
- checked -> AnimatedShapeValue.Checked
- else -> AnimatedShapeValue.Default
- }
- SideEffect { channel.trySend(targetValue) }
- LaunchedEffect(state, channel) {
- for (target in channel) {
- val newTarget = channel.tryReceive().getOrNull() ?: target
- launch {
- with(state) {
- when (newTarget) {
- AnimatedShapeValue.Pressed -> animateToPressed()
- AnimatedShapeValue.Checked -> animateToChecked()
- else -> animateToDefault()
- }
- }
- }
- }
- }
-
- return rememberAnimatedShape(state)
-}
-
-@Composable
-private fun rememberAnimatedShape(state: AnimatedShapeState): Shape {
- val density = LocalDensity.current
- state.density = density
-
- return remember(density, state) {
- object : ShapeWithOpticalCentering {
- var clampedRange by mutableStateOf(0f..1f)
-
- override fun offset(): Float {
- val topStart = state.topStart?.value?.coerceIn(clampedRange) ?: 0f
- val topEnd = state.topEnd?.value?.coerceIn(clampedRange) ?: 0f
- val bottomStart = state.bottomStart?.value?.coerceIn(clampedRange) ?: 0f
- val bottomEnd = state.bottomEnd?.value?.coerceIn(clampedRange) ?: 0f
- val avgStart = (topStart + bottomStart) / 2
- val avgEnd = (topEnd + bottomEnd) / 2
- return OpticalCenteringCoefficient * (avgStart - avgEnd)
- }
-
- override fun createOutline(
- size: Size,
- layoutDirection: LayoutDirection,
- density: Density
- ): Outline {
- state.size = size
- if (!state.didInit) {
- state.init()
- }
-
- clampedRange = 0f..size.height / 2
- return RoundedCornerShape(
- topStart = state.topStart?.value?.coerceIn(clampedRange) ?: 0f,
- topEnd = state.topEnd?.value?.coerceIn(clampedRange) ?: 0f,
- bottomStart = state.bottomStart?.value?.coerceIn(clampedRange) ?: 0f,
- bottomEnd = state.bottomEnd?.value?.coerceIn(clampedRange) ?: 0f,
- )
- .createOutline(size, layoutDirection, density)
- }
- }
- }
-}
-
-@Stable
-private class AnimatedShapeState(
- val startShape: CornerBasedShape,
- val defaultShape: CornerBasedShape,
- val pressedShape: CornerBasedShape,
- val checkedShape: CornerBasedShape,
- val spec: FiniteAnimationSpec<Float>,
-) {
- var size: Size = Size.Zero
- var density: Density = Density(0f, 0f)
- var didInit = false
-
- var topStart: Animatable<Float, AnimationVector1D>? = null
- private set
-
- var topEnd: Animatable<Float, AnimationVector1D>? = null
- private set
-
- var bottomStart: Animatable<Float, AnimationVector1D>? = null
- private set
-
- var bottomEnd: Animatable<Float, AnimationVector1D>? = null
- private set
-
- fun init() {
- topStart = Animatable(startShape.topStart.toPx(size, density))
- topEnd = Animatable(startShape.topEnd.toPx(size, density))
- bottomStart = Animatable(startShape.bottomStart.toPx(size, density))
- bottomEnd = Animatable(startShape.bottomEnd.toPx(size, density))
- didInit = true
- }
-
- suspend fun animateToPressed() = animateToShape(pressedShape)
-
- suspend fun animateToChecked() = animateToShape(checkedShape)
-
- suspend fun animateToDefault() = animateToShape(defaultShape)
-
- private suspend fun animateToShape(shape: CornerBasedShape) = coroutineScope {
- launch { topStart?.animateTo(shape.topStart.toPx(size, density), spec) }
- launch { topEnd?.animateTo(shape.topEnd.toPx(size, density), spec) }
- launch { bottomStart?.animateTo(shape.bottomStart.toPx(size, density), spec) }
- launch { bottomEnd?.animateTo(shape.bottomEnd.toPx(size, density), spec) }
- }
-}
-
-@Immutable
-@JvmInline
-internal value class AnimatedShapeValue internal constructor(internal val type: Int) {
-
- companion object {
- val Default = AnimatedShapeValue(0)
- val Pressed = AnimatedShapeValue(1)
- val Checked = AnimatedShapeValue(2)
- }
+ return shape
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Typography.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Typography.kt
index 0bfc8d5..a9f9262 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Typography.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Typography.kt
@@ -34,6 +34,11 @@
* The type scale is a combination of thirteen styles that are supported by the type system. It
* contains reusable categories of text, each with an intended application and meaning.
*
+ * The emphasized versions of the baseline styles add dynamism and personality to the baseline
+ * styles. It can be used to further stylize select pieces of text. The emphasized states have
+ * pragmatic uses, such as creating clearer division of content and drawing users' eyes to relevant
+ * material.
+ *
* To learn more about typography, see
* [Material Design typography](https://m3.material.io/styles/typography/overview).
*
@@ -77,9 +82,26 @@
* annotate imagery or to introduce a headline.
* @property labelSmall labelSmall is one of the smallest font sizes. It is used sparingly to
* annotate imagery or to introduce a headline.
+ * @property displayLargeEmphasized an emphasized version of [displayLarge].
+ * @property displayMediumEmphasized an emphasized version of [displayMedium].
+ * @property displaySmallEmphasized an emphasized version of [displaySmall].
+ * @property headlineLargeEmphasized an emphasized version of [headlineLarge].
+ * @property headlineMediumEmphasized an emphasized version of [headlineMedium].
+ * @property headlineSmallEmphasized an emphasized version of [headlineSmall].
+ * @property titleLargeEmphasized an emphasized version of [titleLarge].
+ * @property titleMediumEmphasized an emphasized version of [titleMedium].
+ * @property titleSmallEmphasized an emphasized version of [titleSmall].
+ * @property bodyLargeEmphasized an emphasized version of [bodyLarge].
+ * @property bodyMediumEmphasized an emphasized version of [bodyMedium].
+ * @property bodySmallEmphasized an emphasized version of [bodySmall].
+ * @property labelLargeEmphasized an emphasized version of [labelLarge].
+ * @property labelMediumEmphasized an emphasized version of [labelMedium].
+ * @property labelSmallEmphasized an emphasized version of [labelSmall].
*/
@Immutable
-class Typography(
+class Typography
+@ExperimentalMaterial3ExpressiveApi
+constructor(
val displayLarge: TextStyle = TypographyTokens.DisplayLarge,
val displayMedium: TextStyle = TypographyTokens.DisplayMedium,
val displaySmall: TextStyle = TypographyTokens.DisplaySmall,
@@ -95,9 +117,285 @@
val labelLarge: TextStyle = TypographyTokens.LabelLarge,
val labelMedium: TextStyle = TypographyTokens.LabelMedium,
val labelSmall: TextStyle = TypographyTokens.LabelSmall,
+ displayLargeEmphasized: TextStyle = TypographyTokens.DisplayLargeEmphasized,
+ displayMediumEmphasized: TextStyle = TypographyTokens.DisplayMediumEmphasized,
+ displaySmallEmphasized: TextStyle = TypographyTokens.DisplaySmallEmphasized,
+ headlineLargeEmphasized: TextStyle = TypographyTokens.HeadlineLargeEmphasized,
+ headlineMediumEmphasized: TextStyle = TypographyTokens.HeadlineMediumEmphasized,
+ headlineSmallEmphasized: TextStyle = TypographyTokens.HeadlineSmallEmphasized,
+ titleLargeEmphasized: TextStyle = TypographyTokens.TitleLargeEmphasized,
+ titleMediumEmphasized: TextStyle = TypographyTokens.TitleMediumEmphasized,
+ titleSmallEmphasized: TextStyle = TypographyTokens.TitleSmallEmphasized,
+ bodyLargeEmphasized: TextStyle = TypographyTokens.BodyLargeEmphasized,
+ bodyMediumEmphasized: TextStyle = TypographyTokens.BodyMediumEmphasized,
+ bodySmallEmphasized: TextStyle = TypographyTokens.BodySmallEmphasized,
+ labelLargeEmphasized: TextStyle = TypographyTokens.LabelLargeEmphasized,
+ labelMediumEmphasized: TextStyle = TypographyTokens.LabelMediumEmphasized,
+ labelSmallEmphasized: TextStyle = TypographyTokens.LabelSmallEmphasized,
) {
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [displayLarge]. */
+ val displayLargeEmphasized = displayLargeEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [displayMedium]. */
+ val displayMediumEmphasized = displayMediumEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [displaySmall]. */
+ val displaySmallEmphasized = displaySmallEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [headlineLarge]. */
+ val headlineLargeEmphasized = headlineLargeEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [headlineMedium]. */
+ val headlineMediumEmphasized = headlineMediumEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [headlineSmall]. */
+ val headlineSmallEmphasized = headlineSmallEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [titleLarge]. */
+ val titleLargeEmphasized = titleLargeEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [titleMedium]. */
+ val titleMediumEmphasized = titleMediumEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [titleSmall]. */
+ val titleSmallEmphasized = titleSmallEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [bodyLarge]. */
+ val bodyLargeEmphasized = bodyLargeEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [bodyMedium]. */
+ val bodyMediumEmphasized = bodyMediumEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [bodySmall]. */
+ val bodySmallEmphasized = bodySmallEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [labelLarge]. */
+ val labelLargeEmphasized = labelLargeEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [labelMedium]. */
+ val labelMediumEmphasized = labelMediumEmphasized
+
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalMaterial3ExpressiveApi
+ @ExperimentalMaterial3ExpressiveApi
+ /** an emphasized version of [labelSmall]. */
+ val labelSmallEmphasized = labelSmallEmphasized
+
+ /**
+ * The Material Design type scale includes a range of contrasting styles that support the needs
+ * of your product and its content.
+ *
+ * Use typography to make writing legible and beautiful. Material's default type scale includes
+ * contrasting and flexible styles to support a wide range of use cases.
+ *
+ * The type scale is a combination of thirteen styles that are supported by the type system. It
+ * contains reusable categories of text, each with an intended application and meaning.
+ *
+ * To learn more about typography, see
+ * [Material Design typography](https://m3.material.io/styles/typography/overview).
+ *
+ * @param displayLarge displayLarge is the largest display text.
+ * @param displayMedium displayMedium is the second largest display text.
+ * @param displaySmall displaySmall is the smallest display text.
+ * @param headlineLarge headlineLarge is the largest headline, reserved for short, important
+ * text or numerals. For headlines, you can choose an expressive font, such as a display,
+ * handwritten, or script style. These unconventional font designs have details and intricacy
+ * that help attract the eye.
+ * @param headlineMedium headlineMedium is the second largest headline, reserved for short,
+ * important text or numerals. For headlines, you can choose an expressive font, such as a
+ * display, handwritten, or script style. These unconventional font designs have details and
+ * intricacy that help attract the eye.
+ * @param headlineSmall headlineSmall is the smallest headline, reserved for short, important
+ * text or numerals. For headlines, you can choose an expressive font, such as a display,
+ * handwritten, or script style. These unconventional font designs have details and intricacy
+ * that help attract the eye.
+ * @param titleLarge titleLarge is the largest title, and is typically reserved for
+ * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for
+ * subtitles.
+ * @param titleMedium titleMedium is the second largest title, and is typically reserved for
+ * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for
+ * subtitles.
+ * @param titleSmall titleSmall is the smallest title, and is typically reserved for
+ * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for
+ * subtitles.
+ * @param bodyLarge bodyLarge is the largest body, and is typically used for long-form writing
+ * as it works well for small text sizes. For longer sections of text, a serif or sans serif
+ * typeface is recommended.
+ * @param bodyMedium bodyMedium is the second largest body, and is typically used for long-form
+ * writing as it works well for small text sizes. For longer sections of text, a serif or sans
+ * serif typeface is recommended.
+ * @param bodySmall bodySmall is the smallest body, and is typically used for long-form writing
+ * as it works well for small text sizes. For longer sections of text, a serif or sans serif
+ * typeface is recommended.
+ * @param labelLarge labelLarge text is a call to action used in different types of buttons
+ * (such as text, outlined and contained buttons) and in tabs, dialogs, and cards. Button text
+ * is typically sans serif, using all caps text.
+ * @param labelMedium labelMedium is one of the smallest font sizes. It is used sparingly to
+ * annotate imagery or to introduce a headline.
+ * @param labelSmall labelSmall is one of the smallest font sizes. It is used sparingly to
+ * annotate imagery or to introduce a headline.
+ */
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+ constructor(
+ displayLarge: TextStyle = TypographyTokens.DisplayLarge,
+ displayMedium: TextStyle = TypographyTokens.DisplayMedium,
+ displaySmall: TextStyle = TypographyTokens.DisplaySmall,
+ headlineLarge: TextStyle = TypographyTokens.HeadlineLarge,
+ headlineMedium: TextStyle = TypographyTokens.HeadlineMedium,
+ headlineSmall: TextStyle = TypographyTokens.HeadlineSmall,
+ titleLarge: TextStyle = TypographyTokens.TitleLarge,
+ titleMedium: TextStyle = TypographyTokens.TitleMedium,
+ titleSmall: TextStyle = TypographyTokens.TitleSmall,
+ bodyLarge: TextStyle = TypographyTokens.BodyLarge,
+ bodyMedium: TextStyle = TypographyTokens.BodyMedium,
+ bodySmall: TextStyle = TypographyTokens.BodySmall,
+ labelLarge: TextStyle = TypographyTokens.LabelLarge,
+ labelMedium: TextStyle = TypographyTokens.LabelMedium,
+ labelSmall: TextStyle = TypographyTokens.LabelSmall,
+ ) : this(
+ displayLarge = displayLarge,
+ displayMedium = displayMedium,
+ displaySmall = displaySmall,
+ headlineLarge = headlineLarge,
+ headlineMedium = headlineMedium,
+ headlineSmall = headlineSmall,
+ titleLarge = titleLarge,
+ titleMedium = titleMedium,
+ titleSmall = titleSmall,
+ bodyLarge = bodyLarge,
+ bodyMedium = bodyMedium,
+ bodySmall = bodySmall,
+ labelLarge = labelLarge,
+ labelMedium = labelMedium,
+ labelSmall = labelSmall,
+ displayLargeEmphasized = displayLarge,
+ displayMediumEmphasized = displayMedium,
+ displaySmallEmphasized = displaySmall,
+ headlineLargeEmphasized = headlineLarge,
+ headlineMediumEmphasized = headlineMedium,
+ headlineSmallEmphasized = headlineSmall,
+ titleLargeEmphasized = titleLarge,
+ titleMediumEmphasized = titleMedium,
+ titleSmallEmphasized = titleSmall,
+ bodyLargeEmphasized = bodyLarge,
+ bodyMediumEmphasized = bodyMedium,
+ bodySmallEmphasized = bodySmall,
+ labelLargeEmphasized = labelLarge,
+ labelMediumEmphasized = labelMedium,
+ labelSmallEmphasized = labelSmall,
+ )
/** Returns a copy of this Typography, optionally overriding some of the values. */
+ @ExperimentalMaterial3ExpressiveApi
+ fun copy(
+ displayLarge: TextStyle = this.displayLarge,
+ displayMedium: TextStyle = this.displayMedium,
+ displaySmall: TextStyle = this.displaySmall,
+ headlineLarge: TextStyle = this.headlineLarge,
+ headlineMedium: TextStyle = this.headlineMedium,
+ headlineSmall: TextStyle = this.headlineSmall,
+ titleLarge: TextStyle = this.titleLarge,
+ titleMedium: TextStyle = this.titleMedium,
+ titleSmall: TextStyle = this.titleSmall,
+ bodyLarge: TextStyle = this.bodyLarge,
+ bodyMedium: TextStyle = this.bodyMedium,
+ bodySmall: TextStyle = this.bodySmall,
+ labelLarge: TextStyle = this.labelLarge,
+ labelMedium: TextStyle = this.labelMedium,
+ labelSmall: TextStyle = this.labelSmall,
+ displayLargeEmphasized: TextStyle = this.displayLargeEmphasized,
+ displayMediumEmphasized: TextStyle = this.displayMediumEmphasized,
+ displaySmallEmphasized: TextStyle = this.displaySmallEmphasized,
+ headlineLargeEmphasized: TextStyle = this.headlineLargeEmphasized,
+ headlineMediumEmphasized: TextStyle = this.headlineMediumEmphasized,
+ headlineSmallEmphasized: TextStyle = this.headlineSmallEmphasized,
+ titleLargeEmphasized: TextStyle = this.titleLargeEmphasized,
+ titleMediumEmphasized: TextStyle = this.titleMediumEmphasized,
+ titleSmallEmphasized: TextStyle = this.titleSmallEmphasized,
+ bodyLargeEmphasized: TextStyle = this.bodyLargeEmphasized,
+ bodyMediumEmphasized: TextStyle = this.bodyMediumEmphasized,
+ bodySmallEmphasized: TextStyle = this.bodySmallEmphasized,
+ labelLargeEmphasized: TextStyle = this.labelLargeEmphasized,
+ labelMediumEmphasized: TextStyle = this.labelMediumEmphasized,
+ labelSmallEmphasized: TextStyle = this.labelSmallEmphasized,
+ ): Typography =
+ Typography(
+ displayLarge = displayLarge,
+ displayMedium = displayMedium,
+ displaySmall = displaySmall,
+ headlineLarge = headlineLarge,
+ headlineMedium = headlineMedium,
+ headlineSmall = headlineSmall,
+ titleLarge = titleLarge,
+ titleMedium = titleMedium,
+ titleSmall = titleSmall,
+ bodyLarge = bodyLarge,
+ bodyMedium = bodyMedium,
+ bodySmall = bodySmall,
+ labelLarge = labelLarge,
+ labelMedium = labelMedium,
+ labelSmall = labelSmall,
+ displayLargeEmphasized = displayLargeEmphasized,
+ displayMediumEmphasized = displayMediumEmphasized,
+ displaySmallEmphasized = displaySmallEmphasized,
+ headlineLargeEmphasized = headlineLargeEmphasized,
+ headlineMediumEmphasized = headlineMediumEmphasized,
+ headlineSmallEmphasized = headlineSmallEmphasized,
+ titleLargeEmphasized = titleLargeEmphasized,
+ titleMediumEmphasized = titleMediumEmphasized,
+ titleSmallEmphasized = titleSmallEmphasized,
+ bodyLargeEmphasized = bodyLargeEmphasized,
+ bodyMediumEmphasized = bodyMediumEmphasized,
+ bodySmallEmphasized = bodySmallEmphasized,
+ labelLargeEmphasized = labelLargeEmphasized,
+ labelMediumEmphasized = labelMediumEmphasized,
+ labelSmallEmphasized = labelSmallEmphasized,
+ )
+
+ /** Returns a copy of this Typography, optionally overriding some of the values. */
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun copy(
displayLarge: TextStyle = this.displayLarge,
displayMedium: TextStyle = this.displayMedium,
@@ -115,7 +413,7 @@
labelMedium: TextStyle = this.labelMedium,
labelSmall: TextStyle = this.labelSmall,
): Typography =
- Typography(
+ copy(
displayLarge = displayLarge,
displayMedium = displayMedium,
displaySmall = displaySmall,
@@ -130,9 +428,25 @@
bodySmall = bodySmall,
labelLarge = labelLarge,
labelMedium = labelMedium,
- labelSmall = labelSmall
+ labelSmall = labelSmall,
+ displayLargeEmphasized = this.displayLargeEmphasized,
+ displayMediumEmphasized = this.displayMediumEmphasized,
+ displaySmallEmphasized = this.displaySmallEmphasized,
+ headlineLargeEmphasized = this.headlineLargeEmphasized,
+ headlineMediumEmphasized = this.headlineMediumEmphasized,
+ headlineSmallEmphasized = this.headlineSmallEmphasized,
+ titleLargeEmphasized = this.titleLargeEmphasized,
+ titleMediumEmphasized = this.titleMediumEmphasized,
+ titleSmallEmphasized = this.titleSmallEmphasized,
+ bodyLargeEmphasized = this.bodyLargeEmphasized,
+ bodyMediumEmphasized = this.bodyMediumEmphasized,
+ bodySmallEmphasized = this.bodySmallEmphasized,
+ labelLargeEmphasized = this.labelLargeEmphasized,
+ labelMediumEmphasized = this.labelMediumEmphasized,
+ labelSmallEmphasized = this.labelSmallEmphasized,
)
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Typography) return false
@@ -152,9 +466,25 @@
if (labelLarge != other.labelLarge) return false
if (labelMedium != other.labelMedium) return false
if (labelSmall != other.labelSmall) return false
+ if (displayLargeEmphasized != other.displayLargeEmphasized) return false
+ if (displayMediumEmphasized != other.displayMediumEmphasized) return false
+ if (displaySmallEmphasized != other.displaySmallEmphasized) return false
+ if (headlineLargeEmphasized != other.headlineLargeEmphasized) return false
+ if (headlineMediumEmphasized != other.headlineMediumEmphasized) return false
+ if (headlineSmallEmphasized != other.headlineSmallEmphasized) return false
+ if (titleLargeEmphasized != other.titleLargeEmphasized) return false
+ if (titleMediumEmphasized != other.titleMediumEmphasized) return false
+ if (titleSmallEmphasized != other.titleSmallEmphasized) return false
+ if (bodyLargeEmphasized != other.bodyLargeEmphasized) return false
+ if (bodyMediumEmphasized != other.bodyMediumEmphasized) return false
+ if (bodySmallEmphasized != other.bodySmallEmphasized) return false
+ if (labelLargeEmphasized != other.labelLargeEmphasized) return false
+ if (labelMediumEmphasized != other.labelMediumEmphasized) return false
+ if (labelSmallEmphasized != other.labelSmallEmphasized) return false
return true
}
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
override fun hashCode(): Int {
var result = displayLarge.hashCode()
result = 31 * result + displayMedium.hashCode()
@@ -171,9 +501,25 @@
result = 31 * result + labelLarge.hashCode()
result = 31 * result + labelMedium.hashCode()
result = 31 * result + labelSmall.hashCode()
+ result = 31 * result + displayLargeEmphasized.hashCode()
+ result = 31 * result + displayMediumEmphasized.hashCode()
+ result = 31 * result + displaySmallEmphasized.hashCode()
+ result = 31 * result + headlineLargeEmphasized.hashCode()
+ result = 31 * result + headlineMediumEmphasized.hashCode()
+ result = 31 * result + headlineSmallEmphasized.hashCode()
+ result = 31 * result + titleLargeEmphasized.hashCode()
+ result = 31 * result + titleMediumEmphasized.hashCode()
+ result = 31 * result + titleSmallEmphasized.hashCode()
+ result = 31 * result + bodyLargeEmphasized.hashCode()
+ result = 31 * result + bodyMediumEmphasized.hashCode()
+ result = 31 * result + bodySmallEmphasized.hashCode()
+ result = 31 * result + labelLargeEmphasized.hashCode()
+ result = 31 * result + labelMediumEmphasized.hashCode()
+ result = 31 * result + labelSmallEmphasized.hashCode()
return result
}
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
override fun toString(): String {
return "Typography(displayLarge=$displayLarge, displayMedium=$displayMedium," +
"displaySmall=$displaySmall, " +
@@ -181,11 +527,27 @@
" headlineSmall=$headlineSmall, " +
"titleLarge=$titleLarge, titleMedium=$titleMedium, titleSmall=$titleSmall, " +
"bodyLarge=$bodyLarge, bodyMedium=$bodyMedium, bodySmall=$bodySmall, " +
- "labelLarge=$labelLarge, labelMedium=$labelMedium, labelSmall=$labelSmall)"
+ "labelLarge=$labelLarge, labelMedium=$labelMedium, labelSmall=$labelSmall, " +
+ "displayLargeEmphasized=$displayLargeEmphasized, " +
+ "displayMediumEmphasized=$displayMediumEmphasized, " +
+ "displaySmallEmphasized=$displaySmallEmphasized, " +
+ "headlineLargeEmphasized=$headlineLargeEmphasized, " +
+ "headlineMediumEmphasized=$headlineMediumEmphasized, " +
+ "headlineSmallEmphasized=$headlineSmallEmphasized, " +
+ "titleLargeEmphasized=$titleLargeEmphasized, " +
+ "titleMediumEmphasized=$titleMediumEmphasized, " +
+ "titleSmallEmphasized=$titleSmallEmphasized, " +
+ "bodyLargeEmphasized=$bodyLargeEmphasized, " +
+ "bodyMediumEmphasized=$bodyMediumEmphasized, " +
+ "bodySmallEmphasized=$bodySmallEmphasized, " +
+ "labelLargeEmphasized=$labelLargeEmphasized, " +
+ "labelMediumEmphasized=$labelMediumEmphasized, " +
+ "labelSmallEmphasized=$labelSmallEmphasized)"
}
}
/** Helper function for component typography tokens. */
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun Typography.fromToken(value: TypographyKeyTokens): TextStyle {
return when (value) {
TypographyKeyTokens.DisplayLarge -> displayLarge
@@ -203,6 +565,21 @@
TypographyKeyTokens.LabelLarge -> labelLarge
TypographyKeyTokens.LabelMedium -> labelMedium
TypographyKeyTokens.LabelSmall -> labelSmall
+ TypographyKeyTokens.DisplayLargeEmphasized -> displayLargeEmphasized
+ TypographyKeyTokens.DisplayMediumEmphasized -> displayMediumEmphasized
+ TypographyKeyTokens.DisplaySmallEmphasized -> displaySmallEmphasized
+ TypographyKeyTokens.HeadlineLargeEmphasized -> headlineLargeEmphasized
+ TypographyKeyTokens.HeadlineMediumEmphasized -> headlineMediumEmphasized
+ TypographyKeyTokens.HeadlineSmallEmphasized -> headlineSmallEmphasized
+ TypographyKeyTokens.TitleLargeEmphasized -> titleLargeEmphasized
+ TypographyKeyTokens.TitleMediumEmphasized -> titleMediumEmphasized
+ TypographyKeyTokens.TitleSmallEmphasized -> titleSmallEmphasized
+ TypographyKeyTokens.BodyLargeEmphasized -> bodyLargeEmphasized
+ TypographyKeyTokens.BodyMediumEmphasized -> bodyMediumEmphasized
+ TypographyKeyTokens.BodySmallEmphasized -> bodySmallEmphasized
+ TypographyKeyTokens.LabelLargeEmphasized -> labelLargeEmphasized
+ TypographyKeyTokens.LabelMediumEmphasized -> labelMediumEmphasized
+ TypographyKeyTokens.LabelSmallEmphasized -> labelSmallEmphasized
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
index 717b523..65e07ad 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
@@ -17,6 +17,7 @@
package androidx.compose.material3
import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
@@ -60,7 +61,9 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.takeOrElse
@@ -87,6 +90,7 @@
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastSumBy
+import androidx.compose.ui.util.lerp
import kotlin.jvm.JvmInline
import kotlin.math.min
import kotlinx.coroutines.launch
@@ -444,15 +448,16 @@
}
val scope = rememberCoroutineScope()
val predictiveBackProgress = remember { Animatable(initialValue = 0f) }
+ val predictiveBackState = remember { RailPredictiveBackState() }
ModalWideNavigationRailDialog(
properties = properties,
- // TODO: Implement predictive back behavior.
onDismissRequest = { scope.launch { animateToDismiss() } },
onPredictiveBack = { backEvent ->
scope.launch { predictiveBackProgress.snapTo(backEvent) }
},
- onPredictiveBackCancelled = { scope.launch { predictiveBackProgress.animateTo(0f) } }
+ onPredictiveBackCancelled = { scope.launch { predictiveBackProgress.animateTo(0f) } },
+ predictiveBackState = predictiveBackState
) {
Box(modifier = Modifier.fillMaxSize().imePadding()) {
Scrim(
@@ -461,6 +466,8 @@
visible = railState.targetValue != ModalExpandedNavigationRailValue.Closed
)
ModalWideNavigationRailContent(
+ predictiveBackProgress = predictiveBackProgress,
+ predictiveBackState = predictiveBackState,
settleToDismiss = settleToDismiss,
modifier = modifier,
railState = railState,
@@ -703,7 +710,7 @@
private val ColorScheme.defaultWideWideNavigationRailColors: WideNavigationRailColors
@Composable
get() {
- return mDefaultWideWideNavigationRailColorsCached
+ return defaultWideWideNavigationRailColorsCached
?: WideNavigationRailColors(
containerColor = containerColor,
contentColor = contentColorFor(containerColor),
@@ -712,7 +719,7 @@
expandedModalScrimColor =
ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
)
- .also { mDefaultWideWideNavigationRailColorsCached = it }
+ .also { defaultWideWideNavigationRailColorsCached = it }
}
}
@@ -779,14 +786,17 @@
properties: ModalExpandedNavigationRailProperties,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
+ predictiveBackState: RailPredictiveBackState,
content: @Composable () -> Unit
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun ModalWideNavigationRailContent(
+ predictiveBackProgress: Animatable<Float, AnimationVector1D>,
+ predictiveBackState: RailPredictiveBackState,
settleToDismiss: suspend (velocity: Float) -> Unit,
- modifier: Modifier = Modifier,
+ modifier: Modifier,
railState: ModalExpandedNavigationRailState,
colors: WideNavigationRailColors,
shape: Shape,
@@ -800,14 +810,32 @@
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val railPaneTitle = getString(string = Strings.WideNavigationRailPaneTitle)
- Box(
+ Surface(
+ shape = shape,
+ color = colors.expandedModalContainerColor,
modifier =
modifier
- .fillMaxHeight()
.widthIn(max = openModalRailMaxWidth)
+ .fillMaxHeight()
.semantics { paneTitle = railPaneTitle }
.graphicsLayer {
- // TODO: Implement predictive back behavior.
+ val progress = predictiveBackProgress.value
+ if (progress <= 0f) {
+ return@graphicsLayer
+ }
+ val offset = railState.currentOffset
+ val width = size.width
+ if (!offset.isNaN() && !width.isNaN() && width != 0f) {
+ // Apply the predictive back animation.
+ scaleX =
+ calculatePredictiveBackScaleX(
+ progress,
+ predictiveBackState.swipeEdgeMatchesRail
+ )
+ scaleY = calculatePredictiveBackScaleY(progress)
+ transformOrigin =
+ TransformOrigin(if (isRtl) 1f else 0f, PredictiveBackPivotFractionY)
+ }
}
.draggableAnchors(railState.anchoredDraggableState, Orientation.Horizontal) {
railSize,
@@ -829,7 +857,26 @@
)
) {
WideNavigationRailLayout(
- modifier = modifier,
+ modifier =
+ Modifier.graphicsLayer {
+ val progress = predictiveBackProgress.value
+ if (progress <= 0) {
+ return@graphicsLayer
+ }
+ // Preserve the original aspect ratio and alignment due to the predictive back
+ // animation.
+ val predictiveBackScaleX =
+ calculatePredictiveBackScaleX(
+ progress,
+ predictiveBackState.swipeEdgeMatchesRail
+ )
+ val predictiveBackScaleY = calculatePredictiveBackScaleY(progress)
+ scaleX =
+ if (predictiveBackScaleX != 0f) predictiveBackScaleY / predictiveBackScaleX
+ else 1f
+ transformOrigin =
+ TransformOrigin(if (isRtl) 0f else 1f, PredictiveBackPivotFractionY)
+ },
expanded = true,
shape = shape,
colors = colors,
@@ -842,6 +889,32 @@
}
}
+private fun GraphicsLayerScope.calculatePredictiveBackScaleX(
+ progress: Float,
+ swipeEdgeMatchesRail: Boolean,
+): Float {
+ val width = size.width
+ return if (width.isNaN() || width == 0f) {
+ 1f
+ } else {
+ val scaleXDirection = if (swipeEdgeMatchesRail) 1f else -1f
+ 1f +
+ (scaleXDirection *
+ lerp(0f, min(PredictiveBackMaxScaleXDistance.toPx(), width), progress)) / width
+ }
+}
+
+private fun GraphicsLayerScope.calculatePredictiveBackScaleY(
+ progress: Float,
+): Float {
+ val height = size.height
+ return if (height.isNaN() || height == 0f) {
+ 1f
+ } else {
+ 1f - lerp(0f, min(PredictiveBackMaxScaleYDistance.toPx(), height), progress) / height
+ }
+}
+
@Composable
private fun Scrim(color: Color, onDismissRequest: suspend () -> Unit, visible: Boolean) {
if (color.isSpecified) {
@@ -899,5 +972,9 @@
private val ItemStartIconIndicatorVerticalPadding =
(NavigationRailHorizontalItemTokens.ActiveIndicatorHeight -
NavigationRailBaselineItemTokens.IconSize) / 2
+private val PredictiveBackMaxScaleXDistance = 24.dp
+private val PredictiveBackMaxScaleYDistance = 48.dp
+private const val PredictiveBackPivotFractionY = 0.5f
+private const val PredictiveBackPivotFractionYScaleDown = 0f
private const val HeaderLayoutIdTag: String = "header"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt
index 865f380..aa83fb2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt
@@ -22,9 +22,13 @@
import androidx.compose.material3.internal.snapTo
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
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.Saver
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
@@ -197,3 +201,15 @@
)
}
}
+
+@Stable
+internal class RailPredictiveBackState {
+ var swipeEdgeMatchesRail by mutableStateOf(true)
+
+ fun update(
+ isSwipeEdgeLeft: Boolean,
+ isRtl: Boolean,
+ ) {
+ swipeEdgeMatchesRail = (isSwipeEdgeLeft && !isRtl) || (!isSwipeEdgeLeft && isRtl)
+ }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index fa71348..09b667b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
if (anchors.hasAnchorFor(targetValue)) {
try {
dragMutex.mutate(dragPriority) {
- dragTarget = targetValue
+ dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
restartable(inputs = { anchors to [email protected] }) {
(latestAnchors, latestTarget) ->
anchoredDragScope.block(latestAnchors, latestTarget)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnimatedShape.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnimatedShape.kt
new file mode 100644
index 0000000..d70c3ac
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnimatedShape.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.internal
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.OpticalCenteringCoefficient
+import androidx.compose.material3.ShapeWithOpticalCentering
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+@Stable
+internal class AnimatedShapeState(
+ val shape: RoundedCornerShape,
+ val spec: FiniteAnimationSpec<Float>,
+) {
+ var size: Size = Size.Zero
+ var density: Density = Density(0f, 0f)
+
+ private var topStart: Animatable<Float, AnimationVector1D>? = null
+
+ private var topEnd: Animatable<Float, AnimationVector1D>? = null
+
+ private var bottomStart: Animatable<Float, AnimationVector1D>? = null
+
+ private var bottomEnd: Animatable<Float, AnimationVector1D>? = null
+
+ fun topStart(size: Size = this.size, density: Density = this.density): Float {
+ return (topStart ?: Animatable(shape.topStart.toPx(size, density)).also { topStart = it })
+ .value
+ }
+
+ fun topEnd(size: Size = this.size, density: Density = this.density): Float {
+ return (topEnd ?: Animatable(shape.topEnd.toPx(size, density)).also { topEnd = it }).value
+ }
+
+ fun bottomStart(size: Size = this.size, density: Density = this.density): Float {
+ return (bottomStart
+ ?: Animatable(shape.bottomStart.toPx(size, density)).also { bottomStart = it })
+ .value
+ }
+
+ fun bottomEnd(size: Size = this.size, density: Density = this.density): Float {
+ return (bottomEnd
+ ?: Animatable(shape.bottomEnd.toPx(size, density)).also { bottomEnd = it })
+ .value
+ }
+
+ suspend fun animateToShape(shape: CornerBasedShape) = coroutineScope {
+ launch { topStart?.animateTo(shape.topStart.toPx(size, density), spec) }
+ launch { topEnd?.animateTo(shape.topEnd.toPx(size, density), spec) }
+ launch { bottomStart?.animateTo(shape.bottomStart.toPx(size, density), spec) }
+ launch { bottomEnd?.animateTo(shape.bottomEnd.toPx(size, density), spec) }
+ }
+}
+
+@Composable
+private fun rememberAnimatedShape(
+ state: AnimatedShapeState,
+): Shape {
+ val density = LocalDensity.current
+ state.density = density
+
+ return remember(density, state) {
+ object : ShapeWithOpticalCentering {
+ var clampedRange by mutableStateOf(0f..1f)
+
+ override fun offset(): Float {
+ val topStart = state.topStart().coerceIn(clampedRange)
+ val topEnd = state.topEnd().coerceIn(clampedRange)
+ val bottomStart = state.bottomStart().coerceIn(clampedRange)
+ val bottomEnd = state.bottomEnd().coerceIn(clampedRange)
+ val avgStart = (topStart + bottomStart) / 2
+ val avgEnd = (topEnd + bottomEnd) / 2
+ return OpticalCenteringCoefficient * (avgStart - avgEnd)
+ }
+
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ state.size = size
+
+ clampedRange = 0f..size.height / 2
+ return RoundedCornerShape(
+ topStart = state.topStart().coerceIn(clampedRange),
+ topEnd = state.topEnd().coerceIn(clampedRange),
+ bottomStart = state.bottomStart().coerceIn(clampedRange),
+ bottomEnd = state.bottomEnd().coerceIn(clampedRange),
+ )
+ .createOutline(size, layoutDirection, density)
+ }
+ }
+ }
+}
+
+@Composable
+internal fun rememberAnimatedShape(
+ currentShape: RoundedCornerShape,
+ animationSpec: FiniteAnimationSpec<Float>,
+): Shape {
+ val state =
+ remember(animationSpec) {
+ AnimatedShapeState(
+ shape = currentShape,
+ spec = animationSpec,
+ )
+ }
+
+ val channel = remember { Channel<RoundedCornerShape>(Channel.CONFLATED) }
+
+ SideEffect { channel.trySend(currentShape) }
+ LaunchedEffect(state, channel) {
+ for (target in channel) {
+ val newTarget = channel.tryReceive().getOrNull() ?: target
+ launch { state.animateToShape(newTarget) }
+ }
+ }
+
+ return rememberAnimatedShape(state)
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index 73196bb..639db61 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -319,12 +319,11 @@
if (isRefreshing) return 0f // Already refreshing, do nothing
// Trigger refresh
if (adjustedDistancePulled > thresholdPx) {
- animateToThreshold()
onRefresh()
- } else {
- animateToHidden()
}
+ animateToHidden()
+
val consumed =
when {
// We are flinging without having dragged the pull refresh (for example a fling
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExpressiveMotionTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExpressiveMotionTokens.kt
new file mode 100644
index 0000000..3362be1
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExpressiveMotionTokens.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+internal object ExpressiveMotionTokens {
+ val SpringDefaultSpatialDamping = 0.8f
+ val SpringDefaultSpatialStiffness = 380.0f
+ val SpringDefaultEffectsDamping = 1.0f
+ val SpringDefaultEffectsStiffness = 1600.0f
+ val SpringFastSpatialDamping = 0.6f
+ val SpringFastSpatialStiffness = 800.0f
+ val SpringFastEffectsDamping = 1.0f
+ val SpringFastEffectsStiffness = 3800.0f
+ val SpringSlowSpatialDamping = 0.8f
+ val SpringSlowSpatialStiffness = 200.0f
+ val SpringSlowEffectsDamping = 1.0f
+ val SpringSlowEffectsStiffness = 800.0f
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabLargeTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabLargeTokens.kt
new file mode 100644
index 0000000..7b339a7
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabLargeTokens.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object ExtendedFabLargeTokens {
+ val ContainerHeight = 96.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerExtraLarge
+ val IconLabelSpace = 20.0.dp
+ val IconSize = 32.0.dp
+ val LeadingSpace = 28.0.dp
+ val TrailingSpace = 28.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabMediumTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabMediumTokens.kt
new file mode 100644
index 0000000..3f6c72ff
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabMediumTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object ExtendedFabMediumTokens {
+ val ContainerHeight = 80.0.dp
+ // TODO: uncomment when ShapeKeyTokens.CornerLargeIncreased is available
+ // val ContainerShape = ShapeKeyTokens.CornerLargeIncreased
+ val IconLabelSpace = 16.0.dp
+ val IconSize = 28.0.dp
+ val LeadingSpace = 26.0.dp
+ val TrailingSpace = 26.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabSmallTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabSmallTokens.kt
new file mode 100644
index 0000000..6a51bb1
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ExtendedFabSmallTokens.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object ExtendedFabSmallTokens {
+ val ContainerHeight = 56.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerLarge
+ val IconLabelSpace = 8.0.dp
+ val IconSize = 24.0.dp
+ val LeadingSpace = 16.0.dp
+ val TrailingSpace = 16.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabBaselineTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabBaselineTokens.kt
new file mode 100644
index 0000000..f4f3506
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabBaselineTokens.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object FabBaselineTokens {
+ val ContainerHeight = 56.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerLarge
+ val ContainerWidth = 56.0.dp
+ val IconSize = 24.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabLargeTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabLargeTokens.kt
new file mode 100644
index 0000000..57421b1
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabLargeTokens.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object FabLargeTokens {
+ val ContainerHeight = 96.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerExtraLarge
+ val ContainerWidth = 96.0.dp
+ val IconSize = 32.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabMediumTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabMediumTokens.kt
new file mode 100644
index 0000000..70853b3
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabMediumTokens.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object FabMediumTokens {
+ val ContainerHeight = 80.0.dp
+ // TODO: uncomment when ShapeKeyTokens.CornerLargeIncreased is available
+ // val ContainerShape = ShapeKeyTokens.CornerLargeIncreased
+ val ContainerWidth = 80.0.dp
+ val IconSize = 28.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabMenuBaselineTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabMenuBaselineTokens.kt
new file mode 100644
index 0000000..4c8771a
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabMenuBaselineTokens.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object FabMenuBaselineTokens {
+ val CloseButtonBetweenSpace = 8.0.dp
+ val CloseButtonContainerElevation = ElevationTokens.Level3
+ val CloseButtonContainerHeight = 56.0.dp
+ val CloseButtonContainerShape = ShapeKeyTokens.CornerFull
+ val CloseButtonContainerWidth = 56.0.dp
+ val CloseButtonIconSize = 20.0.dp
+ val ListItemBetweenSpace = 4.0.dp
+ val ListItemContainerElevation = ElevationTokens.Level3
+ val ListItemContainerHeight = 56.0.dp
+ val ListItemContainerShape = ShapeKeyTokens.CornerFull
+ val ListItemIconLabelSpace = 8.0.dp
+ val ListItemIconSize = 24.0.dp
+ val ListItemLeadingSpace = 24.0.dp
+ val ListItemTrailingSpace = 24.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryContainerTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryContainerTokens.kt
new file mode 100644
index 0000000..4f273ca
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryContainerTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+internal object FabPrimaryContainerTokens {
+ val ContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+ val ContainerElevation = ElevationTokens.Level3
+ val FocusedContainerElevation = ElevationTokens.Level3
+ val FocusedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
+ val HoveredContainerElevation = ElevationTokens.Level4
+ val HoveredIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
+ val IconColor = ColorSchemeKeyTokens.OnPrimaryContainer
+ val PressedContainerElevation = ElevationTokens.Level3
+ val PressedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryLargeTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryLargeTokens.kt
deleted file mode 100644
index 2330a5c..0000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryLargeTokens.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-// VERSION: v0_103
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-package androidx.compose.material3.tokens
-
-import androidx.compose.ui.unit.dp
-
-internal object FabPrimaryLargeTokens {
- val ContainerColor = ColorSchemeKeyTokens.PrimaryContainer
- val ContainerElevation = ElevationTokens.Level3
- val ContainerHeight = 96.0.dp
- val ContainerShape = ShapeKeyTokens.CornerExtraLarge
- val ContainerWidth = 96.0.dp
- val FocusContainerElevation = ElevationTokens.Level3
- val FocusIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val HoverContainerElevation = ElevationTokens.Level4
- val HoverIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val IconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val IconSize = 36.0.dp
- val LoweredContainerElevation = ElevationTokens.Level1
- val LoweredFocusContainerElevation = ElevationTokens.Level1
- val LoweredHoverContainerElevation = ElevationTokens.Level2
- val LoweredPressedContainerElevation = ElevationTokens.Level1
- val PressedContainerElevation = ElevationTokens.Level3
- val PressedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimarySmallTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimarySmallTokens.kt
deleted file mode 100644
index 106084a..0000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimarySmallTokens.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-// VERSION: v0_103
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-package androidx.compose.material3.tokens
-
-import androidx.compose.ui.unit.dp
-
-internal object FabPrimarySmallTokens {
- val ContainerColor = ColorSchemeKeyTokens.PrimaryContainer
- val ContainerElevation = ElevationTokens.Level3
- val ContainerHeight = 40.0.dp
- val ContainerShape = ShapeKeyTokens.CornerMedium
- val ContainerWidth = 40.0.dp
- val FocusContainerElevation = ElevationTokens.Level3
- val FocusIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val HoverContainerElevation = ElevationTokens.Level4
- val HoverIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val IconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val IconSize = 24.0.dp
- val LoweredContainerElevation = ElevationTokens.Level1
- val LoweredFocusContainerElevation = ElevationTokens.Level1
- val LoweredHoverContainerElevation = ElevationTokens.Level2
- val LoweredPressedContainerElevation = ElevationTokens.Level1
- val PressedContainerElevation = ElevationTokens.Level3
- val PressedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryTokens.kt
deleted file mode 100644
index 6ee7c71..0000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabPrimaryTokens.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-// VERSION: v0_103
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-package androidx.compose.material3.tokens
-
-import androidx.compose.ui.unit.dp
-
-internal object FabPrimaryTokens {
- val ContainerColor = ColorSchemeKeyTokens.PrimaryContainer
- val ContainerElevation = ElevationTokens.Level3
- val ContainerHeight = 56.0.dp
- val ContainerShape = ShapeKeyTokens.CornerLarge
- val ContainerWidth = 56.0.dp
- val FocusContainerElevation = ElevationTokens.Level3
- val FocusIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val HoverContainerElevation = ElevationTokens.Level4
- val HoverIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val IconColor = ColorSchemeKeyTokens.OnPrimaryContainer
- val IconSize = 24.0.dp
- val LoweredContainerElevation = ElevationTokens.Level1
- val LoweredFocusContainerElevation = ElevationTokens.Level1
- val LoweredHoverContainerElevation = ElevationTokens.Level2
- val LoweredPressedContainerElevation = ElevationTokens.Level1
- val PressedContainerElevation = ElevationTokens.Level3
- val PressedIconColor = ColorSchemeKeyTokens.OnPrimaryContainer
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSecondaryContainerTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSecondaryContainerTokens.kt
new file mode 100644
index 0000000..a419646
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSecondaryContainerTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+internal object FabSecondaryContainerTokens {
+ val ContainerColor = ColorSchemeKeyTokens.SecondaryContainer
+ val ContainerElevation = ElevationTokens.Level3
+ val FocusedContainerElevation = ElevationTokens.Level3
+ val FocusedIconColor = ColorSchemeKeyTokens.OnSecondaryContainer
+ val HoveredContainerElevation = ElevationTokens.Level4
+ val HoveredIconColor = ColorSchemeKeyTokens.OnSecondaryContainer
+ val IconColor = ColorSchemeKeyTokens.OnSecondaryContainer
+ val PressedContainerElevation = ElevationTokens.Level3
+ val PressedIconColor = ColorSchemeKeyTokens.OnSecondaryContainer
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSecondaryTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSecondaryTokens.kt
deleted file mode 100644
index ce17e1b..0000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSecondaryTokens.kt
+++ /dev/null
@@ -1,41 +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.
- */
-// VERSION: v0_103
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-package androidx.compose.material3.tokens
-
-import androidx.compose.ui.unit.dp
-
-internal object FabSecondaryTokens {
- val ContainerColor = ColorSchemeKeyTokens.SecondaryContainer
- val ContainerElevation = ElevationTokens.Level3
- val ContainerHeight = 56.0.dp
- val ContainerShape = ShapeKeyTokens.CornerLarge
- val ContainerWidth = 56.0.dp
- val FocusContainerElevation = ElevationTokens.Level3
- val FocusIconColor = ColorSchemeKeyTokens.OnSecondaryContainer
- val HoverContainerElevation = ElevationTokens.Level4
- val HoverIconColor = ColorSchemeKeyTokens.OnSecondaryContainer
- val IconColor = ColorSchemeKeyTokens.OnSecondaryContainer
- val IconSize = 24.0.dp
- val LoweredContainerElevation = ElevationTokens.Level1
- val LoweredFocusContainerElevation = ElevationTokens.Level1
- val LoweredHoverContainerElevation = ElevationTokens.Level2
- val LoweredPressedContainerElevation = ElevationTokens.Level1
- val PressedContainerElevation = ElevationTokens.Level3
- val PressedIconColor = ColorSchemeKeyTokens.OnSecondaryContainer
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/readAssetsFile.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSmallTokens.kt
similarity index 60%
rename from tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/readAssetsFile.kt
rename to compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSmallTokens.kt
index 3fa6028..da6ea8e 100644
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/readAssetsFile.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FabSmallTokens.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,10 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
-package androidx.tv.integration.presentation
+package androidx.compose.material3.tokens
-import android.content.res.AssetManager
+import androidx.compose.ui.unit.dp
-fun AssetManager.readAssetsFile(fileName: String): String =
- open(fileName).bufferedReader().use { it.readText() }
+internal object FabSmallTokens {
+ val ContainerHeight = 40.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerMedium
+ val ContainerWidth = 40.0.dp
+ val IconSize = 24.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LargeIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LargeIconButtonTokens.kt
index 040a520..cd13b6b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LargeIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LargeIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_9_0
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -24,14 +24,15 @@
val ContainerHeight = 96.0.dp
val ContainerShapeRound = ShapeKeyTokens.CornerFull
val ContainerShapeSquare = ShapeKeyTokens.CornerExtraLarge
- val UniformLeadingSpace = 32.0.dp
- val UniformTrailingSpace = 32.0.dp
val IconSize = 32.0.dp
val NarrowLeadingSpace = 16.0.dp
val NarrowTrailingSpace = 16.0.dp
val OutlinedOutlineWidth = 2.0.dp
- val PressedContainerCornerSizeMultiplierPercent = 50.0f
- val SelectedPressedContainerShape = ShapeKeyTokens.CornerExtraLarge
+ val PressedContainerShape = ShapeKeyTokens.CornerLarge
+ val SelectedContainerShapeRound = ShapeKeyTokens.CornerExtraLarge
+ val SelectedContainerShapeSquare = ShapeKeyTokens.CornerFull
+ val UniformLeadingSpace = 32.0.dp
+ val UniformTrailingSpace = 32.0.dp
val WideLeadingSpace = 48.0.dp
val WideTrailingSpace = 48.0.dp
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MediumIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MediumIconButtonTokens.kt
index 310d016..d147c78 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MediumIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MediumIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_9_0
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -25,11 +25,12 @@
val ContainerShapeRound = ShapeKeyTokens.CornerFull
val ContainerShapeSquare = ShapeKeyTokens.CornerLarge
val IconSize = 24.0.dp
- val NarrowLeadingSpace = 10.0.dp
- val NarrowTrailingSpace = 10.0.dp
+ val NarrowLeadingSpace = 12.0.dp
+ val NarrowTrailingSpace = 12.0.dp
val OutlinedOutlineWidth = 1.0.dp
- val PressedContainerCornerSizeMultiplierPercent = 50.0f
- val SelectedPressedContainerShape = ShapeKeyTokens.CornerLarge
+ val PressedContainerShape = ShapeKeyTokens.CornerMedium
+ val SelectedContainerShapeRound = ShapeKeyTokens.CornerLarge
+ val SelectedContainerShapeSquare = ShapeKeyTokens.CornerFull
val UniformLeadingSpace = 16.0.dp
val UniformTrailingSpace = 16.0.dp
val WideLeadingSpace = 24.0.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarHorizontalItemTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarHorizontalItemTokens.kt
index bdecc5f..fba7659 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarHorizontalItemTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarHorizontalItemTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarTokens.kt
index 4842560..352fe47 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarVerticalItemTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarVerticalItemTokens.kt
index 5452b98..c901af7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarVerticalItemTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationBarVerticalItemTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailBaselineItemTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailBaselineItemTokens.kt
index 144c675..7e5c0b3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailBaselineItemTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailBaselineItemTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailCollapsedTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailCollapsedTokens.kt
index 4827019..c4ad2b0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailCollapsedTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailCollapsedTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailColorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailColorTokens.kt
index 951345a..68a5be1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailColorTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailColorTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailExpandedTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailExpandedTokens.kt
index 9270c7e..34be029 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailExpandedTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailExpandedTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailHorizontalItemTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailHorizontalItemTokens.kt
index 1c8d76a..9261d3a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailHorizontalItemTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailHorizontalItemTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailVerticalItemTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailVerticalItemTokens.kt
index a87af2b..c398fc6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailVerticalItemTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/NavigationRailVerticalItemTokens.kt
@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
-// TO MODIFY THIS FILE UPDATE:
-// ux/material/dsdb/contrib/generators/tokens/src/compose/templates/component.jinja2
-//
-// Design System: sandbox
-// Version: v0_9_0
-// Audience: 1p
package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SmallIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SmallIconButtonTokens.kt
index e2bda5b..f30bff3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SmallIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SmallIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_9_0
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -28,8 +28,9 @@
val NarrowLeadingSpace = 4.0.dp
val NarrowTrailingSpace = 4.0.dp
val OutlinedOutlineWidth = 1.0.dp
- val PressedContainerCornerSizeMultiplierPercent = 50.0f
- val SelectedPressedContainerShape = ShapeKeyTokens.CornerMedium
+ val PressedContainerShape = ShapeKeyTokens.CornerSmall
+ val SelectedContainerShapeRound = ShapeKeyTokens.CornerMedium
+ val SelectedContainerShapeSquare = ShapeKeyTokens.CornerFull
val UniformLeadingSpace = 8.0.dp
val UniformTrailingSpace = 8.0.dp
val WideLeadingSpace = 14.0.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt
index 749a2aa..8b85857 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SplitButtonSmallTokens.kt
@@ -14,16 +14,22 @@
* limitations under the License.
*/
-// VERSION: v0_5_0
+// VERSION: v0_14_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
+import androidx.compose.material3.ShapeDefaults
import androidx.compose.ui.unit.dp
internal object SplitButtonSmallTokens {
val BetweenSpace = 2.0.dp
val ContainerHeight = 40.0.dp
val ContainerShape = ShapeKeyTokens.CornerFull
- val InnerCornerShape = ShapeTokens.CornerExtraSmall
+ val InnerCornerSize = ShapeDefaults.CornerExtraSmall
+ val LeadingButtonLeadingSpace = 16.0.dp
+ val LeadingButtonTrailingSpace = 12.0.dp
+ val TrailingIconSize = 22.0.dp
+ val TrailingButtonLeadingSpace = 13.0.dp
+ val TrailingButtonTrailingSpace = 13.0.dp
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/StandardMotionTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/StandardMotionTokens.kt
new file mode 100644
index 0000000..19db205
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/StandardMotionTokens.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_14_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+package androidx.compose.material3.tokens
+internal object StandardMotionTokens {
+ val SpringDefaultSpatialDamping = 0.9f
+ val SpringDefaultSpatialStiffness = 700.0f
+ val SpringDefaultEffectsDamping = 1.0f
+ val SpringDefaultEffectsStiffness = 1600.0f
+ val SpringFastSpatialDamping = 0.9f
+ val SpringFastSpatialStiffness = 1400.0f
+ val SpringFastEffectsDamping = 1.0f
+ val SpringFastEffectsStiffness = 3800.0f
+ val SpringSlowSpatialDamping = 0.9f
+ val SpringSlowSpatialStiffness = 300.0f
+ val SpringSlowEffectsDamping = 1.0f
+ val SpringSlowEffectsStiffness = 800.0f
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypeScaleTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypeScaleTokens.kt
index 0073539..997e3ed 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypeScaleTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypeScaleTokens.kt
@@ -96,4 +96,80 @@
val TitleSmallSize = 14.sp
val TitleSmallTracking = 0.1.sp
val TitleSmallWeight = TypefaceTokens.WeightMedium
+ // TODO update with the generated tokens once available
+ val BodyLargeEmphasizedFont = TypefaceTokens.Plain
+ val BodyLargeEmphasizedLineHeight = 24.0.sp
+ val BodyLargeEmphasizedSize = 16.sp
+ val BodyLargeEmphasizedTracking = 0.15.sp
+ val BodyLargeEmphasizedWeight = TypefaceTokens.WeightMedium
+ val BodyMediumEmphasizedFont = TypefaceTokens.Plain
+ val BodyMediumEmphasizedLineHeight = 20.0.sp
+ val BodyMediumEmphasizedSize = 14.sp
+ val BodyMediumEmphasizedTracking = 0.25.sp
+ val BodyMediumEmphasizedWeight = TypefaceTokens.WeightMedium
+ val BodySmallEmphasizedFont = TypefaceTokens.Plain
+ val BodySmallEmphasizedLineHeight = 16.0.sp
+ val BodySmallEmphasizedSize = 12.sp
+ val BodySmallEmphasizedTracking = 0.4.sp
+ val BodySmallEmphasizedWeight = TypefaceTokens.WeightMedium
+ val DisplayLargeEmphasizedFont = TypefaceTokens.Brand
+ val DisplayLargeEmphasizedLineHeight = 64.0.sp
+ val DisplayLargeEmphasizedSize = 57.sp
+ val DisplayLargeEmphasizedTracking = 0.sp
+ val DisplayLargeEmphasizedWeight = TypefaceTokens.WeightMedium
+ val DisplayMediumEmphasizedFont = TypefaceTokens.Brand
+ val DisplayMediumEmphasizedLineHeight = 52.0.sp
+ val DisplayMediumEmphasizedSize = 45.sp
+ val DisplayMediumEmphasizedTracking = 0.sp
+ val DisplayMediumEmphasizedWeight = TypefaceTokens.WeightMedium
+ val DisplaySmallEmphasizedFont = TypefaceTokens.Brand
+ val DisplaySmallEmphasizedLineHeight = 44.0.sp
+ val DisplaySmallEmphasizedSize = 36.sp
+ val DisplaySmallEmphasizedTracking = 0.sp
+ val DisplaySmallEmphasizedWeight = TypefaceTokens.WeightMedium
+ val HeadlineLargeEmphasizedFont = TypefaceTokens.Brand
+ val HeadlineLargeEmphasizedLineHeight = 40.0.sp
+ val HeadlineLargeEmphasizedSize = 32.sp
+ val HeadlineLargeEmphasizedTracking = 0.sp
+ val HeadlineLargeEmphasizedWeight = TypefaceTokens.WeightMedium
+ val HeadlineMediumEmphasizedFont = TypefaceTokens.Brand
+ val HeadlineMediumEmphasizedLineHeight = 36.0.sp
+ val HeadlineMediumEmphasizedSize = 28.sp
+ val HeadlineMediumEmphasizedTracking = 0.sp
+ val HeadlineMediumEmphasizedWeight = TypefaceTokens.WeightMedium
+ val HeadlineSmallEmphasizedFont = TypefaceTokens.Brand
+ val HeadlineSmallEmphasizedLineHeight = 32.0.sp
+ val HeadlineSmallEmphasizedSize = 24.sp
+ val HeadlineSmallEmphasizedTracking = 0.sp
+ val HeadlineSmallEmphasizedWeight = TypefaceTokens.WeightMedium
+ val LabelLargeEmphasizedFont = TypefaceTokens.Plain
+ val LabelLargeEmphasizedLineHeight = 20.0.sp
+ val LabelLargeEmphasizedSize = 14.sp
+ val LabelLargeEmphasizedTracking = 0.1.sp
+ val LabelLargeEmphasizedWeight = TypefaceTokens.WeightBold
+ val LabelMediumEmphasizedFont = TypefaceTokens.Plain
+ val LabelMediumEmphasizedLineHeight = 16.0.sp
+ val LabelMediumEmphasizedSize = 12.sp
+ val LabelMediumEmphasizedTracking = 0.5.sp
+ val LabelMediumEmphasizedWeight = TypefaceTokens.WeightBold
+ val LabelSmallEmphasizedFont = TypefaceTokens.Plain
+ val LabelSmallEmphasizedLineHeight = 16.0.sp
+ val LabelSmallEmphasizedSize = 11.sp
+ val LabelSmallEmphasizedTracking = 0.5.sp
+ val LabelSmallEmphasizedWeight = TypefaceTokens.WeightBold
+ val TitleLargeEmphasizedFont = TypefaceTokens.Brand
+ val TitleLargeEmphasizedLineHeight = 28.0.sp
+ val TitleLargeEmphasizedSize = 22.sp
+ val TitleLargeEmphasizedTracking = 0.sp
+ val TitleLargeEmphasizedWeight = TypefaceTokens.WeightMedium
+ val TitleMediumEmphasizedFont = TypefaceTokens.Plain
+ val TitleMediumEmphasizedLineHeight = 24.0.sp
+ val TitleMediumEmphasizedSize = 16.sp
+ val TitleMediumEmphasizedTracking = 0.15.sp
+ val TitleMediumEmphasizedWeight = TypefaceTokens.WeightBold
+ val TitleSmallEmphasizedFont = TypefaceTokens.Plain
+ val TitleSmallEmphasizedLineHeight = 20.0.sp
+ val TitleSmallEmphasizedSize = 14.sp
+ val TitleSmallEmphasizedTracking = 0.1.sp
+ val TitleSmallEmphasizedWeight = TypefaceTokens.WeightBold
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyKeyTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyKeyTokens.kt
index 0e58763..fd559d8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyKeyTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyKeyTokens.kt
@@ -34,4 +34,20 @@
TitleLarge,
TitleMedium,
TitleSmall,
+ // TODO update with the generated tokens once available
+ BodyLargeEmphasized,
+ BodyMediumEmphasized,
+ BodySmallEmphasized,
+ DisplayLargeEmphasized,
+ DisplayMediumEmphasized,
+ DisplaySmallEmphasized,
+ HeadlineLargeEmphasized,
+ HeadlineMediumEmphasized,
+ HeadlineSmallEmphasized,
+ LabelLargeEmphasized,
+ LabelMediumEmphasized,
+ LabelSmallEmphasized,
+ TitleLargeEmphasized,
+ TitleMediumEmphasized,
+ TitleSmallEmphasized,
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt
index 46d7643..3d12ae9 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt
@@ -143,6 +143,127 @@
lineHeight = TypeScaleTokens.TitleSmallLineHeight,
letterSpacing = TypeScaleTokens.TitleSmallTracking,
)
+ // TODO update with the generated tokens once available
+ val BodyLargeEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.BodyLargeEmphasizedFont,
+ fontWeight = TypeScaleTokens.BodyLargeEmphasizedWeight,
+ fontSize = TypeScaleTokens.BodyLargeEmphasizedSize,
+ lineHeight = TypeScaleTokens.BodyLargeEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.BodyLargeEmphasizedTracking,
+ )
+ val BodyMediumEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.BodyMediumEmphasizedFont,
+ fontWeight = TypeScaleTokens.BodyMediumEmphasizedWeight,
+ fontSize = TypeScaleTokens.BodyMediumEmphasizedSize,
+ lineHeight = TypeScaleTokens.BodyMediumEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.BodyMediumEmphasizedTracking,
+ )
+ val BodySmallEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.BodySmallEmphasizedFont,
+ fontWeight = TypeScaleTokens.BodySmallEmphasizedWeight,
+ fontSize = TypeScaleTokens.BodySmallEmphasizedSize,
+ lineHeight = TypeScaleTokens.BodySmallEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.BodySmallEmphasizedTracking,
+ )
+ val DisplayLargeEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.DisplayLargeEmphasizedFont,
+ fontWeight = TypeScaleTokens.DisplayLargeEmphasizedWeight,
+ fontSize = TypeScaleTokens.DisplayLargeEmphasizedSize,
+ lineHeight = TypeScaleTokens.DisplayLargeEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.DisplayLargeEmphasizedTracking,
+ )
+ val DisplayMediumEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.DisplayMediumEmphasizedFont,
+ fontWeight = TypeScaleTokens.DisplayMediumEmphasizedWeight,
+ fontSize = TypeScaleTokens.DisplayMediumEmphasizedSize,
+ lineHeight = TypeScaleTokens.DisplayMediumEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.DisplayMediumEmphasizedTracking,
+ )
+ val DisplaySmallEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.DisplaySmallEmphasizedFont,
+ fontWeight = TypeScaleTokens.DisplaySmallEmphasizedWeight,
+ fontSize = TypeScaleTokens.DisplaySmallEmphasizedSize,
+ lineHeight = TypeScaleTokens.DisplaySmallEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.DisplaySmallEmphasizedTracking,
+ )
+ val HeadlineLargeEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.HeadlineLargeEmphasizedFont,
+ fontWeight = TypeScaleTokens.HeadlineLargeEmphasizedWeight,
+ fontSize = TypeScaleTokens.HeadlineLargeEmphasizedSize,
+ lineHeight = TypeScaleTokens.HeadlineLargeEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.HeadlineLargeEmphasizedTracking,
+ )
+ val HeadlineMediumEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.HeadlineMediumEmphasizedFont,
+ fontWeight = TypeScaleTokens.HeadlineMediumEmphasizedWeight,
+ fontSize = TypeScaleTokens.HeadlineMediumEmphasizedSize,
+ lineHeight = TypeScaleTokens.HeadlineMediumEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.HeadlineMediumEmphasizedTracking,
+ )
+ val HeadlineSmallEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.HeadlineSmallEmphasizedFont,
+ fontWeight = TypeScaleTokens.HeadlineSmallEmphasizedWeight,
+ fontSize = TypeScaleTokens.HeadlineSmallEmphasizedSize,
+ lineHeight = TypeScaleTokens.HeadlineSmallEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.HeadlineSmallEmphasizedTracking,
+ )
+ val LabelLargeEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.LabelLargeEmphasizedFont,
+ fontWeight = TypeScaleTokens.LabelLargeEmphasizedWeight,
+ fontSize = TypeScaleTokens.LabelLargeEmphasizedSize,
+ lineHeight = TypeScaleTokens.LabelLargeEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.LabelLargeEmphasizedTracking,
+ )
+ val LabelMediumEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.LabelMediumEmphasizedFont,
+ fontWeight = TypeScaleTokens.LabelMediumEmphasizedWeight,
+ fontSize = TypeScaleTokens.LabelMediumEmphasizedSize,
+ lineHeight = TypeScaleTokens.LabelMediumEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.LabelMediumEmphasizedTracking,
+ )
+ val LabelSmallEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.LabelSmallEmphasizedFont,
+ fontWeight = TypeScaleTokens.LabelSmallEmphasizedWeight,
+ fontSize = TypeScaleTokens.LabelSmallEmphasizedSize,
+ lineHeight = TypeScaleTokens.LabelSmallEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.LabelSmallEmphasizedTracking,
+ )
+ val TitleLargeEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.TitleLargeEmphasizedFont,
+ fontWeight = TypeScaleTokens.TitleLargeEmphasizedWeight,
+ fontSize = TypeScaleTokens.TitleLargeEmphasizedSize,
+ lineHeight = TypeScaleTokens.TitleLargeEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.TitleLargeEmphasizedTracking,
+ )
+ val TitleMediumEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.TitleMediumEmphasizedFont,
+ fontWeight = TypeScaleTokens.TitleMediumEmphasizedWeight,
+ fontSize = TypeScaleTokens.TitleMediumEmphasizedSize,
+ lineHeight = TypeScaleTokens.TitleMediumEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.TitleMediumEmphasizedTracking,
+ )
+ val TitleSmallEmphasized =
+ DefaultTextStyle.copy(
+ fontFamily = TypeScaleTokens.TitleSmallEmphasizedFont,
+ fontWeight = TypeScaleTokens.TitleSmallEmphasizedWeight,
+ fontSize = TypeScaleTokens.TitleSmallEmphasizedSize,
+ lineHeight = TypeScaleTokens.TitleSmallEmphasizedLineHeight,
+ letterSpacing = TypeScaleTokens.TitleSmallEmphasizedTracking,
+ )
}
internal val DefaultLineHeightStyle =
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XLargeIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XLargeIconButtonTokens.kt
index 372522d..e57927c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XLargeIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XLargeIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_9_0
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -28,8 +28,9 @@
val NarrowLeadingSpace = 32.0.dp
val NarrowTrailingSpace = 32.0.dp
val OutlinedOutlineWidth = 3.0.dp
- val PressedContainerCornerSizeMultiplierPercent = 50.0f
- val SelectedPressedContainerShape = ShapeKeyTokens.CornerExtraLarge
+ val PressedContainerShape = ShapeKeyTokens.CornerLarge
+ val SelectedContainerShapeRound = ShapeKeyTokens.CornerExtraLarge
+ val SelectedContainerShapeSquare = ShapeKeyTokens.CornerFull
val UniformLeadingSpace = 48.0.dp
val UniformTrailingSpace = 48.0.dp
val WideLeadingSpace = 72.0.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XSmallIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XSmallIconButtonTokens.kt
index 75ab01b..d976555 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XSmallIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/XSmallIconButtonTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_9_0
+// VERSION: v0_11_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -28,8 +28,9 @@
val NarrowLeadingSpace = 4.0.dp
val NarrowTrailingSpace = 4.0.dp
val OutlinedOutlineWidth = 1.0.dp
- val PressedContainerCornerSizeMultiplierPercent = 50.0f
- val SelectedPressedContainerShape = ShapeKeyTokens.CornerMedium
+ val PressedContainerShape = ShapeKeyTokens.CornerSmall
+ val SelectedContainerShapeRound = ShapeKeyTokens.CornerMedium
+ val SelectedContainerShapeSquare = ShapeKeyTokens.CornerFull
val UniformLeadingSpace = 6.0.dp
val UniformTrailingSpace = 6.0.dp
val WideLeadingSpace = 10.0.dp
diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt
index 8d65b88..7c960c0 100644
--- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt
+++ b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt
@@ -39,5 +39,6 @@
properties: ModalExpandedNavigationRailProperties,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
+ predictiveBackState: RailPredictiveBackState,
content: @Composable () -> Unit
): Unit = implementedInJetBrainsFork()
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index dffb7ff..110569b 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -151,7 +151,7 @@
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void recordSideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void recordUsed(androidx.compose.runtime.RecomposeScope scope);
method @androidx.compose.runtime.ComposeCompilerApi public Object? rememberedValue();
- method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public boolean shouldExecute(boolean parametersChanged);
+ method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public boolean shouldExecute(boolean parametersChanged, int flags);
method @androidx.compose.runtime.ComposeCompilerApi public void skipCurrentGroup();
method @androidx.compose.runtime.ComposeCompilerApi public void skipToGroupEnd();
method public void sourceInformation(String sourceInformation);
@@ -1058,6 +1058,30 @@
}
+package androidx.compose.runtime.snapshots.tooling {
+
+ @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public final class SnapshotInstanceObservers {
+ ctor public SnapshotInstanceObservers();
+ ctor public SnapshotInstanceObservers(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver);
+ method public kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? getReadObserver();
+ method public kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? getWriteObserver();
+ property public final kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver;
+ property public final kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver;
+ }
+
+ @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public interface SnapshotObserver {
+ method public default void onApplied(androidx.compose.runtime.snapshots.Snapshot snapshot, java.util.Set<?> changed);
+ method public default void onCreated(androidx.compose.runtime.snapshots.Snapshot snapshot, androidx.compose.runtime.snapshots.Snapshot? parent, androidx.compose.runtime.snapshots.tooling.SnapshotInstanceObservers? observers);
+ method public default androidx.compose.runtime.snapshots.tooling.SnapshotInstanceObservers? onCreating(androidx.compose.runtime.snapshots.Snapshot? parent, boolean readonly);
+ method public default void onDisposing(androidx.compose.runtime.snapshots.Snapshot snapshot);
+ }
+
+ public final class SnapshotObserverKt {
+ method @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public static androidx.compose.runtime.snapshots.ObserverHandle observeSnapshots(androidx.compose.runtime.snapshots.Snapshot.Companion, androidx.compose.runtime.snapshots.tooling.SnapshotObserver snapshotObserver);
+ }
+
+}
+
package androidx.compose.runtime.tooling {
public interface CompositionData {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 9c343f0..186ecc4 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -156,7 +156,7 @@
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void recordSideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void recordUsed(androidx.compose.runtime.RecomposeScope scope);
method @androidx.compose.runtime.ComposeCompilerApi public Object? rememberedValue();
- method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public boolean shouldExecute(boolean parametersChanged);
+ method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public boolean shouldExecute(boolean parametersChanged, int flags);
method @androidx.compose.runtime.ComposeCompilerApi public void skipCurrentGroup();
method @androidx.compose.runtime.ComposeCompilerApi public void skipToGroupEnd();
method public void sourceInformation(String sourceInformation);
@@ -1109,6 +1109,30 @@
}
+package androidx.compose.runtime.snapshots.tooling {
+
+ @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public final class SnapshotInstanceObservers {
+ ctor public SnapshotInstanceObservers();
+ ctor public SnapshotInstanceObservers(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver);
+ method public kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? getReadObserver();
+ method public kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? getWriteObserver();
+ property public final kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver;
+ property public final kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver;
+ }
+
+ @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public interface SnapshotObserver {
+ method public default void onApplied(androidx.compose.runtime.snapshots.Snapshot snapshot, java.util.Set<?> changed);
+ method public default void onCreated(androidx.compose.runtime.snapshots.Snapshot snapshot, androidx.compose.runtime.snapshots.Snapshot? parent, androidx.compose.runtime.snapshots.tooling.SnapshotInstanceObservers? observers);
+ method public default androidx.compose.runtime.snapshots.tooling.SnapshotInstanceObservers? onCreating(androidx.compose.runtime.snapshots.Snapshot? parent, boolean readonly);
+ method public default void onDisposing(androidx.compose.runtime.snapshots.Snapshot snapshot);
+ }
+
+ public final class SnapshotObserverKt {
+ method @SuppressCompatibility @androidx.compose.runtime.ExperimentalComposeRuntimeApi public static androidx.compose.runtime.snapshots.ObserverHandle observeSnapshots(androidx.compose.runtime.snapshots.Snapshot.Companion, androidx.compose.runtime.snapshots.tooling.SnapshotObserver snapshotObserver);
+ }
+
+}
+
package androidx.compose.runtime.tooling {
public interface CompositionData {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 0e48726..28f2850 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -985,7 +985,7 @@
* execute (such as its scope was invalidated or a static composition local it was changed) or
* the composition is pausable and the composition is pausing.
*/
- @InternalComposeApi fun shouldExecute(parametersChanged: Boolean): Boolean
+ @InternalComposeApi fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean
// Internal API
@@ -3038,7 +3038,8 @@
}
@ComposeCompilerApi
- override fun shouldExecute(parametersChanged: Boolean): Boolean {
+ @Suppress("UNUSED")
+ override fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean {
return parametersChanged || !skipping
}
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 e5cea89..e593c27 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
@@ -20,6 +20,7 @@
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
+import androidx.compose.runtime.ExperimentalComposeRuntimeApi
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.SynchronizedObject
import androidx.compose.runtime.checkPrecondition
@@ -32,6 +33,9 @@
import androidx.compose.runtime.requirePrecondition
import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot
import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot
+import androidx.compose.runtime.snapshots.tooling.creatingSnapshot
+import androidx.compose.runtime.snapshots.tooling.dispatchObserverOnApplied
+import androidx.compose.runtime.snapshots.tooling.dispatchObserverOnDispose
import androidx.compose.runtime.synchronized
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
@@ -740,25 +744,30 @@
* with this snapshot can be collected. Nested active snapshots are still valid after the parent
* has been disposed but calling [apply] will fail.
*/
+ @OptIn(ExperimentalComposeRuntimeApi::class)
open fun takeNestedMutableSnapshot(
readObserver: ((Any) -> Unit)? = null,
writeObserver: ((Any) -> Unit)? = null
): MutableSnapshot {
validateNotDisposed()
validateNotAppliedOrPinned()
- return advance {
- sync {
- val newId = nextSnapshotId++
- openSnapshots = openSnapshots.set(newId)
- val currentInvalid = invalid
- this.invalid = currentInvalid.set(newId)
- NestedMutableSnapshot(
- newId,
- currentInvalid.addRange(id + 1, newId),
- mergedReadObserver(readObserver, this.readObserver),
- mergedWriteObserver(writeObserver, this.writeObserver),
- this
- )
+ return creatingSnapshot(this, readObserver, writeObserver, readonly = false) {
+ actualReadObserver,
+ actualWriteObserver ->
+ advance {
+ sync {
+ val newId = nextSnapshotId++
+ openSnapshots = openSnapshots.set(newId)
+ val currentInvalid = invalid
+ this.invalid = currentInvalid.set(newId)
+ NestedMutableSnapshot(
+ newId,
+ currentInvalid.addRange(id + 1, newId),
+ mergedReadObserver(actualReadObserver, this.readObserver),
+ mergedWriteObserver(actualWriteObserver, this.writeObserver),
+ this
+ )
+ }
}
}
}
@@ -853,6 +862,8 @@
observers.fastForEach { it(modifiedSet, this) }
}
+ dispatchObserverOnApplied(this, modified)
+
// Wait to release pinned snapshots until after running observers.
// This permits observers to safely take a nested snapshot of the one that was just applied
// before unpinning records that need to be retained in this case.
@@ -878,23 +889,32 @@
if (!disposed) {
super.dispose()
nestedDeactivated(this)
+ dispatchObserverOnDispose(this)
}
}
+ @OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot {
validateNotDisposed()
validateNotAppliedOrPinned()
val previousId = id
- return advance {
- sync {
- val readonlyId = nextSnapshotId++
- openSnapshots = openSnapshots.set(readonlyId)
- NestedReadonlySnapshot(
- id = readonlyId,
- invalid = invalid.addRange(previousId + 1, readonlyId),
- readObserver = mergedReadObserver(readObserver, this.readObserver),
- parent = this
- )
+ return creatingSnapshot(
+ if (this is GlobalSnapshot) null else this,
+ readObserver = readObserver,
+ writeObserver = null,
+ readonly = true
+ ) { actualReadObserver, _ ->
+ advance {
+ sync {
+ val readonlyId = nextSnapshotId++
+ openSnapshots = openSnapshots.set(readonlyId)
+ NestedReadonlySnapshot(
+ id = readonlyId,
+ invalid = invalid.addRange(previousId + 1, readonlyId),
+ readObserver = mergedReadObserver(actualReadObserver, this.readObserver),
+ parent = this
+ )
+ }
}
}
}
@@ -1298,14 +1318,22 @@
get() = null
@Suppress("UNUSED_PARAMETER") set(value) = unsupported()
+ @OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot {
validateOpen(this)
- return NestedReadonlySnapshot(
- id = id,
- invalid = invalid,
- readObserver = mergedReadObserver(readObserver, this.readObserver),
- parent = this
- )
+ return creatingSnapshot(
+ parent = this,
+ readObserver = readObserver,
+ writeObserver = null,
+ readonly = true,
+ ) { actualReadObserver, _ ->
+ NestedReadonlySnapshot(
+ id = id,
+ invalid = invalid,
+ readObserver = mergedReadObserver(actualReadObserver, this.readObserver),
+ parent = this
+ )
+ }
}
override fun notifyObjectsInitialized() {
@@ -1316,6 +1344,7 @@
if (!disposed) {
nestedDeactivated(this)
super.dispose()
+ dispatchObserverOnDispose(this)
}
}
@@ -1351,13 +1380,21 @@
override val root: Snapshot
get() = parent.root
+ @OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?) =
- NestedReadonlySnapshot(
- id = id,
- invalid = invalid,
- readObserver = mergedReadObserver(readObserver, this.readObserver),
- parent = parent
- )
+ creatingSnapshot(
+ parent = this,
+ readObserver = readObserver,
+ writeObserver = null,
+ readonly = true,
+ ) { actualReadObserver, _ ->
+ NestedReadonlySnapshot(
+ id = id,
+ invalid = invalid,
+ readObserver = mergedReadObserver(actualReadObserver, this.readObserver),
+ parent = parent
+ )
+ }
override fun notifyObjectsInitialized() {
// Nothing to do for read-only snapshots
@@ -1372,6 +1409,7 @@
}
parent.nestedDeactivated(this)
super.dispose()
+ dispatchObserverOnDispose(this)
}
}
@@ -1405,32 +1443,49 @@
}
) {
+ @OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot =
- takeNewSnapshot { invalid ->
- ReadonlySnapshot(
- id = sync { nextSnapshotId++ },
- invalid = invalid,
- readObserver = readObserver
- )
+ creatingSnapshot(
+ parent = null,
+ readonly = true,
+ readObserver = readObserver,
+ writeObserver = null,
+ ) { actualReadObserver, _ ->
+ takeNewSnapshot { invalid ->
+ ReadonlySnapshot(
+ id = sync { nextSnapshotId++ },
+ invalid = invalid,
+ readObserver = actualReadObserver
+ )
+ }
}
+ @OptIn(ExperimentalComposeRuntimeApi::class)
override fun takeNestedMutableSnapshot(
readObserver: ((Any) -> Unit)?,
writeObserver: ((Any) -> Unit)?
- ): MutableSnapshot = takeNewSnapshot { invalid ->
- MutableSnapshot(
- id = sync { nextSnapshotId++ },
- invalid = invalid,
-
- // It is intentional that the global read observers are not merged with mutable
- // snapshots read observers.
+ ): MutableSnapshot =
+ creatingSnapshot(
+ parent = null,
+ readonly = false,
readObserver = readObserver,
-
- // It is intentional that global write observers are not merged with mutable
- // snapshots write observers.
writeObserver = writeObserver
- )
- }
+ ) { actualReadObserver, actualWriteObserver ->
+ takeNewSnapshot { invalid ->
+ MutableSnapshot(
+ id = sync { nextSnapshotId++ },
+ invalid = invalid,
+
+ // It is intentional that the global read observers are not merged with mutable
+ // snapshots read observers.
+ readObserver = actualReadObserver,
+
+ // It is intentional that global write observers are not merged with mutable
+ // snapshots write observers.
+ writeObserver = actualWriteObserver
+ )
+ }
+ }
override fun notifyObjectsInitialized() {
advanceGlobalSnapshot()
@@ -1519,6 +1574,7 @@
applied = true
deactivate()
+ dispatchObserverOnApplied(this, modified)
return SnapshotApplyResult.Success
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/tooling/SnapshotObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/tooling/SnapshotObserver.kt
new file mode 100644
index 0000000..489e858
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/tooling/SnapshotObserver.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.snapshots.tooling
+
+import androidx.collection.ScatterSet
+import androidx.compose.runtime.ExperimentalComposeRuntimeApi
+import androidx.compose.runtime.collection.wrapIntoSet
+import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
+import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
+import androidx.compose.runtime.snapshots.ObserverHandle
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.StateObject
+import androidx.compose.runtime.snapshots.fastForEach
+import androidx.compose.runtime.snapshots.sync
+
+/**
+ * An observer for the snapshot system that notifies an observer when a snapshot is created,
+ * applied, and/or disposed.
+ *
+ * All methods are called in the thread of the snapshot so all observers must be thread safe as they
+ * may be called from any thread.
+ *
+ * Calling any of the Snapshot API (including, reading or writing mutable state objects) is not
+ * supported and may produce inconsistent result or throw an exception.
+ */
+@ExperimentalComposeRuntimeApi
+@Suppress("CallbackName")
+interface SnapshotObserver {
+ /**
+ * Called before a snapshot is created allowing reads and writes to the snapshot to be observed.
+ *
+ * This method is called in the same thread that creates the snapshot.
+ *
+ * @param parent the parent snapshot for the new snapshot if it is a nested snapshot or null
+ * otherwise.
+ * @param readonly whether the snapshot being created will be read-only.
+ * @return optional read and write observers that will be added to the snapshot created.
+ */
+ fun onCreating(parent: Snapshot?, readonly: Boolean): SnapshotInstanceObservers? = null
+
+ /**
+ * Called after snapshot is created.
+ *
+ * This is called prior to the instance being returned by [Snapshot.takeSnapshot] or
+ * [Snapshot.takeMutableSnapshot].
+ *
+ * This method is called in the same thread that creates the snapshot.
+ *
+ * @param snapshot the snapshot that was created.
+ * @param parent the parent snapshot for the new snapshot if it is a nested snapshot or null if
+ * it is a root snapshot.
+ * @param observers the read and write observers that were installed by the value returned by
+ * [onCreated]. This allows correlating which snapshot observers returned by [onCreating] to
+ * the [snapshot] that was created.
+ */
+ fun onCreated(snapshot: Snapshot, parent: Snapshot?, observers: SnapshotInstanceObservers?) {}
+
+ /**
+ * Called while a snapshot is being disposed.
+ *
+ * This method is called in the same thread that disposes the snapshot.
+ *
+ * @param snapshot information about the snapshot that was created.
+ */
+ fun onDisposing(snapshot: Snapshot) {}
+
+ /**
+ * Called after a snapshot is applied.
+ *
+ * For nested snapshots, the changes will only be visible to the parent snapshot, not globally.
+ * Snapshots do not have a parent will have changes that are visible globally and such
+ * notification are equivalent the notification sent to [Snapshot.registerApplyObserver] and
+ * will include all objects modified by any nested snapshots that have been applied to the
+ * parent snapshot.
+ *
+ * This method is called in the same thread that applies the snapshot.
+ *
+ * @param snapshot the snapshot that was applied.
+ * @param changed the set of objects that were modified during the snapshot.
+ */
+ fun onApplied(snapshot: Snapshot, changed: Set<Any>) {}
+}
+
+/**
+ * The return result of [SnapshotObserver.onCreating] allowing the reads and writes performed in the
+ * newly created snapshot to be observed
+ */
+@ExperimentalComposeRuntimeApi
+class SnapshotInstanceObservers(
+ /**
+ * Called whenever a state is read in the snapshot. This is called before the read observer
+ * passed to [Snapshot.takeSnapshot] or [Snapshot.takeMutableSnapshot].
+ *
+ * This method is called in the same thread that reads snapshot state.
+ */
+ val readObserver: ((Any) -> Unit)? = null,
+
+ /**
+ * Called just before a state object is written to the first time in the snapshot or a nested
+ * mutable snapshot. This might be called several times for the same object if nested mutable
+ * snapshots are created as the unmodified value may be needed by the nested snapshot so a new
+ * copy is created. This is not called for each write, only when the write results in the object
+ * be recorded as being modified requiring a copy to be made before the write completes. This is
+ * called before the write has been applied to the instance.
+ *
+ * This is called before the write observer passed to [Snapshot.takeMutableSnapshot].
+ *
+ * This method is called in the same thread that writes to the snapshot state.
+ */
+ val writeObserver: ((Any) -> Unit)? = null,
+)
+
+/**
+ * This is a tooling API and is not intended to be used in a production application as it will
+ * introduce global overhead to creating, applying and disposing all snapshots and, potentially, to
+ * reading and writing all state objects.
+ *
+ * Observe when snapshots are created, applied, and/or disposed. The observer can also install read
+ * and write observers on the snapshot being created.
+ *
+ * This method is thread-safe and calling [ObserverHandle.dispose] on the [ObserverHandle] returned
+ * is also thread-safe.
+ *
+ * @param snapshotObserver the snapshot observer to install.
+ * @return [ObserverHandle] an instance to unregister the [snapshotObserver].
+ */
+@ExperimentalComposeRuntimeApi
+fun Snapshot.Companion.observeSnapshots(snapshotObserver: SnapshotObserver): ObserverHandle {
+ sync { observers = (observers ?: persistentListOf()).add(snapshotObserver) }
+ return ObserverHandle {
+ sync {
+ val newObservers = observers?.remove(snapshotObserver)
+ observers = newObservers?.takeIf { it.isNotEmpty() }
+ }
+ }
+}
+
+@ExperimentalComposeRuntimeApi private var observers: PersistentList<SnapshotObserver>? = null
+
+@ExperimentalComposeRuntimeApi
+internal inline fun <R : Snapshot> creatingSnapshot(
+ parent: Snapshot?,
+ noinline readObserver: ((Any) -> Unit)?,
+ noinline writeObserver: ((Any) -> Unit)?,
+ readonly: Boolean,
+ crossinline block: (readObserver: ((Any) -> Unit)?, writeObserver: ((Any) -> Unit)?) -> R
+): R {
+ var observerMap: Map<SnapshotObserver, SnapshotInstanceObservers>? = null
+ val observers = observers
+ var actualReadObserver = readObserver
+ var actualWriteObserver = writeObserver
+ if (observers != null) {
+ val result = observers.mergeObservers(parent, readonly, readObserver, writeObserver)
+ val mappedObservers = result.first
+ actualReadObserver = mappedObservers.readObserver
+ actualWriteObserver = mappedObservers.writeObserver
+ observerMap = result.second
+ }
+ val result = block(actualReadObserver, actualWriteObserver)
+ observers?.dispatchCreatedObservers(parent, result, observerMap)
+ return result
+}
+
+@ExperimentalComposeRuntimeApi
+internal fun PersistentList<SnapshotObserver>.mergeObservers(
+ parent: Snapshot?,
+ readonly: Boolean,
+ readObserver: ((Any) -> Unit)?,
+ writeObserver: ((Any) -> Unit)?,
+): Pair<SnapshotInstanceObservers, Map<SnapshotObserver, SnapshotInstanceObservers>?> {
+ var currentReadObserver = readObserver
+ var currentWriteObserver = writeObserver
+ var observerMap: MutableMap<SnapshotObserver, SnapshotInstanceObservers>? = null
+ fastForEach { observer ->
+ val instance = observer.onCreating(parent, readonly)
+ if (instance != null) {
+ currentReadObserver = mergeObservers(instance.readObserver, currentReadObserver)
+ currentWriteObserver = mergeObservers(instance.writeObserver, currentWriteObserver)
+ (observerMap
+ ?: run {
+ val newMap = mutableMapOf<SnapshotObserver, SnapshotInstanceObservers>()
+ observerMap = newMap
+ newMap
+ })[observer] = instance
+ }
+ }
+ return SnapshotInstanceObservers(currentReadObserver, currentWriteObserver) to observerMap
+}
+
+private fun mergeObservers(a: ((Any) -> Unit)?, b: ((Any) -> Unit)?): ((Any) -> Unit)? {
+ return if (a != null && b != null) {
+ {
+ a(it)
+ b(it)
+ }
+ } else a ?: b
+}
+
+@ExperimentalComposeRuntimeApi
+internal fun PersistentList<SnapshotObserver>.dispatchCreatedObservers(
+ parent: Snapshot?,
+ result: Snapshot,
+ observerMap: Map<SnapshotObserver, SnapshotInstanceObservers>?
+) {
+ fastForEach { observer ->
+ val instance = observerMap?.get(observer)
+ observer.onCreated(result, parent, instance)
+ }
+}
+
+@OptIn(ExperimentalComposeRuntimeApi::class)
+internal fun dispatchObserverOnDispose(snapshot: Snapshot) {
+ observers?.fastForEach { observer -> observer.onDisposing(snapshot) }
+}
+
+@OptIn(ExperimentalComposeRuntimeApi::class)
+internal fun dispatchObserverOnApplied(snapshot: Snapshot, changes: ScatterSet<StateObject>?) {
+ val observers = observers
+ if (!observers.isNullOrEmpty()) {
+ val wrappedChanges = changes?.wrapIntoSet() ?: emptySet()
+ observers.fastForEach { observer -> observer.onApplied(snapshot, wrappedChanges) }
+ }
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/tooling/SnapshotObserverTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/tooling/SnapshotObserverTests.kt
new file mode 100644
index 0000000..96a947c
--- /dev/null
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/tooling/SnapshotObserverTests.kt
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalComposeRuntimeApi::class)
+
+package androidx.compose.runtime.snapshots.tooling
+
+import androidx.collection.mutableScatterSetOf
+import androidx.compose.runtime.ExperimentalComposeRuntimeApi
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.Snapshot.Companion.openSnapshotCount
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class SnapshotObserverTests {
+ @Test
+ fun canAddAndRemoveObserver() {
+ observeSnapshots(object : SnapshotObserver {}) {}
+ }
+
+ @Test
+ fun canObserverReadonlySnapshotCreation() {
+ val observed = mutableScatterSetOf<Snapshot>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onCreated(
+ snapshot: Snapshot,
+ parent: Snapshot?,
+ observers: SnapshotInstanceObservers?
+ ) {
+ observed.add(snapshot)
+ }
+ }
+ ) {
+ val created = Snapshot.takeSnapshot()
+ assertTrue(created in observed)
+ created.dispose()
+ }
+ }
+
+ @Test
+ fun canObserverMutableSnapshotCreation() {
+ val observed = mutableScatterSetOf<Snapshot>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onCreated(
+ snapshot: Snapshot,
+ parent: Snapshot?,
+ observers: SnapshotInstanceObservers?
+ ) {
+ observed.add(snapshot)
+ }
+ }
+ ) {
+ val created = Snapshot.takeMutableSnapshot()
+ assertTrue(created in observed)
+ created.dispose()
+ }
+ }
+
+ @Test
+ fun canObserveApply() {
+ val applied = mutableListOf<Pair<Snapshot, Set<Any>>>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onApplied(snapshot: Snapshot, changed: Set<Any>) {
+ applied.add(snapshot to changed)
+ }
+ }
+ ) {
+ val state = mutableIntStateOf(10)
+ val snapshot = Snapshot.takeMutableSnapshot()
+ snapshot.enter { state.value = 12 }
+ snapshot.apply().check()
+ val apply = applied.first()
+ assertEquals(snapshot, apply.first)
+ assertTrue(apply.second.contains(state))
+ snapshot.dispose()
+ }
+ }
+
+ @Test
+ fun canObserverDisposeOfReadonlySnapshot() {
+ val disposed = mutableScatterSetOf<Snapshot>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onDisposing(snapshot: Snapshot) {
+ disposed.add(snapshot)
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeSnapshot()
+ snapshot.dispose()
+ assertTrue(disposed.contains(snapshot))
+ }
+ }
+
+ @Test
+ fun canObserverDisposeOfMutableSnapshot_NotApplied() {
+ val disposed = mutableScatterSetOf<Snapshot>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onDisposing(snapshot: Snapshot) {
+ disposed.add(snapshot)
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeMutableSnapshot()
+ snapshot.dispose()
+ assertTrue(disposed.contains(snapshot))
+ }
+ }
+
+ @Test
+ fun canObserverDisposeOfMutableSnapshot_Applied() {
+ val disposed = mutableScatterSetOf<Snapshot>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onDisposing(snapshot: Snapshot) {
+ disposed.add(snapshot)
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeMutableSnapshot()
+ snapshot.apply().check()
+ snapshot.dispose()
+ assertTrue(disposed.contains(snapshot))
+ }
+ }
+
+ @Test
+ fun canObserveApplyOfNestedSnapshot() {
+ val applied = mutableListOf<Pair<Snapshot, Set<Any>>>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onApplied(snapshot: Snapshot, changed: Set<Any>) {
+ applied.add(snapshot to changed)
+ }
+ }
+ ) {
+ val state = mutableIntStateOf(10)
+ val snapshot = Snapshot.takeMutableSnapshot()
+ val nestedSnapshot = snapshot.takeNestedMutableSnapshot()
+ nestedSnapshot.enter { state.value = 12 }
+ nestedSnapshot.apply().check()
+ snapshot.apply().check()
+
+ val nestedApply = applied.first()
+ assertEquals(nestedSnapshot, nestedApply.first)
+ assertTrue(nestedApply.second.contains(state))
+ nestedSnapshot.dispose()
+ val snapshotApply = applied.last()
+ assertEquals(snapshot, snapshotApply.first)
+ assertTrue(snapshotApply.second.contains(state))
+ snapshot.dispose()
+ }
+ }
+
+ @Test
+ fun canObserveReadsInReadonlySnapshot() {
+ val state = mutableIntStateOf(10)
+ val read = mutableListOf<Pair<Any, Boolean>>()
+
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onCreating(
+ parent: Snapshot?,
+ readonly: Boolean
+ ): SnapshotInstanceObservers {
+ return SnapshotInstanceObservers(readObserver = { read.add(it to true) })
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeSnapshot(readObserver = { read.add(it to false) })
+ try {
+ val result = snapshot.enter { state.value }
+ assertEquals(10, result)
+ assertEquals(mutableListOf<Pair<Any, Boolean>>(state to true, state to false), read)
+ } finally {
+ snapshot.dispose()
+ }
+ }
+ }
+
+ @Test
+ fun canObserveReadsInMutableSnapshot() {
+ val state = mutableIntStateOf(10)
+ val read = mutableListOf<Pair<Any, Boolean>>()
+
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onCreating(
+ parent: Snapshot?,
+ readonly: Boolean
+ ): SnapshotInstanceObservers {
+ return SnapshotInstanceObservers(readObserver = { read.add(it to true) })
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeMutableSnapshot(readObserver = { read.add(it to false) })
+ try {
+ val result = snapshot.enter { state.value }
+ assertEquals(10, result)
+ assertEquals(mutableListOf<Pair<Any, Boolean>>(state to true, state to false), read)
+ } finally {
+ snapshot.dispose()
+ }
+ }
+ }
+
+ @Test
+ fun canObserveWrites() {
+ val state = mutableIntStateOf(10)
+ val writes = mutableListOf<Pair<Any, Boolean>>()
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onCreating(
+ parent: Snapshot?,
+ readonly: Boolean
+ ): SnapshotInstanceObservers {
+ return SnapshotInstanceObservers(writeObserver = { writes.add(it to true) })
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeMutableSnapshot(writeObserver = { writes.add(it to false) })
+ try {
+ val result =
+ snapshot.enter {
+ state.value = 20
+ state.value
+ }
+ assertEquals(20, result)
+ assertEquals(
+ expected = mutableListOf<Pair<Any, Boolean>>(state to true, state to false),
+ actual = writes
+ )
+ } finally {
+ snapshot.dispose()
+ }
+ }
+ }
+
+ @Test
+ fun canHaveMultipleObservers() {
+ val events = mutableListOf<Pair<Any?, String>>()
+ fun observer(prefix: String) =
+ object : SnapshotObserver {
+ override fun onCreating(
+ parent: Snapshot?,
+ readonly: Boolean
+ ): SnapshotInstanceObservers {
+ record(parent, "creating, readonly = $readonly")
+ return SnapshotInstanceObservers(
+ readObserver = { record(it, "reading") },
+ writeObserver = { record(it, "writing") }
+ )
+ }
+
+ override fun onCreated(
+ snapshot: Snapshot,
+ parent: Snapshot?,
+ observers: SnapshotInstanceObservers?
+ ) {
+ record(snapshot to parent, "created")
+ }
+
+ override fun onDisposing(snapshot: Snapshot) {
+ record(snapshot, "disposing")
+ }
+
+ override fun onApplied(snapshot: Snapshot, changed: Set<Any>) {
+ record(snapshot to changed, "applied")
+ }
+
+ fun record(value: Any?, msg: String) {
+ events.add(value to "$prefix: $msg")
+ }
+ }
+ val state1 = mutableIntStateOf(1)
+ val state2 = mutableIntStateOf(2)
+
+ observeSnapshots(observer("Outer")) {
+ observeSnapshots(observer("Inner")) {
+ val ros1 = Snapshot.takeSnapshot()
+ try {
+ ros1.enter {
+ state1.value
+ state2.value
+ }
+ } finally {
+ ros1.dispose()
+ }
+
+ val ms1 = Snapshot.takeMutableSnapshot()
+ try {
+ ms1.enter { state1.value = 11 }
+ ms1.apply().check()
+ } finally {
+ ms1.dispose()
+ }
+ assertEquals(
+ listOf(
+ null to "Outer: creating, readonly = true",
+ null to "Inner: creating, readonly = true",
+ (ros1 to null) to "Outer: created",
+ (ros1 to null) to "Inner: created",
+ state1 to "Inner: reading",
+ state1 to "Outer: reading",
+ state2 to "Inner: reading",
+ state2 to "Outer: reading",
+ ros1 to "Outer: disposing",
+ ros1 to "Inner: disposing",
+ null to "Outer: creating, readonly = false",
+ null to "Inner: creating, readonly = false",
+ (ms1 to null) to "Outer: created",
+ (ms1 to null) to "Inner: created",
+ state1 to "Inner: writing",
+ state1 to "Outer: writing",
+ (ms1 to setOf(state1)) to "Outer: applied",
+ (ms1 to setOf(state1)) to "Inner: applied",
+ ms1 to "Outer: disposing",
+ ms1 to "Inner: disposing"
+ ),
+ events as List<*>
+ )
+ }
+ }
+ }
+
+ @Test
+ fun receivesTheCorrectParent() {
+ val events = mutableListOf<Pair<Any?, String>>()
+ fun observer() =
+ object : SnapshotObserver {
+ override fun onCreating(
+ parent: Snapshot?,
+ readonly: Boolean
+ ): SnapshotInstanceObservers {
+ record(parent, "creating, readonly = $readonly")
+ return SnapshotInstanceObservers(
+ readObserver = { record(it, "reading") },
+ writeObserver = { record(it, "writing") }
+ )
+ }
+
+ override fun onCreated(
+ snapshot: Snapshot,
+ parent: Snapshot?,
+ observers: SnapshotInstanceObservers?
+ ) {
+ record(snapshot to parent, "created")
+ }
+
+ override fun onDisposing(snapshot: Snapshot) {
+ record(snapshot, "disposing")
+ }
+
+ override fun onApplied(snapshot: Snapshot, changed: Set<Any>) {
+ record(snapshot to changed, "applied")
+ }
+
+ fun record(value: Any?, msg: String) {
+ events.add(value to msg)
+ }
+ }
+
+ observeSnapshots(observer()) {
+ val ro1 = Snapshot.takeSnapshot()
+ val ro2 = ro1.takeNestedSnapshot()
+ ro2.dispose()
+ ro1.dispose()
+
+ val ms1 = Snapshot.takeMutableSnapshot()
+ val ms2 = ms1.takeNestedMutableSnapshot()
+ ms1.dispose()
+ ms2.dispose()
+
+ assertEquals(
+ listOf(
+ null to "creating, readonly = true",
+ (ro1 to null) to "created",
+ ro1 to "creating, readonly = true",
+ (ro2 to ro1) to "created",
+ ro2 to "disposing",
+ ro1 to "disposing",
+ null to "creating, readonly = false",
+ (ms1 to null) to "created",
+ ms1 to "creating, readonly = false",
+ (ms2 to ms1) to "created",
+ ms1 to "disposing",
+ ms2 to "disposing",
+ ),
+ events
+ )
+ }
+ }
+
+ @Test
+ fun canCorrelateCreatingAndCreating() {
+ var key: SnapshotInstanceObservers? = null
+ observeSnapshots(
+ object : SnapshotObserver {
+ override fun onCreating(
+ parent: Snapshot?,
+ readonly: Boolean
+ ): SnapshotInstanceObservers {
+ val result = SnapshotInstanceObservers()
+ key = result
+ return result
+ }
+
+ override fun onCreated(
+ snapshot: Snapshot,
+ parent: Snapshot?,
+ observers: SnapshotInstanceObservers?
+ ) {
+ assertEquals(observers, key)
+ }
+ }
+ ) {
+ val snapshot = Snapshot.takeMutableSnapshot()
+ snapshot.dispose()
+ }
+ }
+
+ private var count = 0
+
+ @OptIn(InternalComposeApi::class)
+ @BeforeTest
+ fun recordOpenSnapshots() {
+ count = openSnapshotCount()
+ }
+
+ // Validate that the tests do not change the number of open snapshots
+ @OptIn(InternalComposeApi::class)
+ @AfterTest
+ fun validateOpenSnapshots() {
+ assertEquals(count, openSnapshotCount(), "Snapshot not disposed?")
+ }
+}
+
+@ExperimentalComposeRuntimeApi
+private inline fun <R> observeSnapshots(observer: SnapshotObserver, block: () -> R): R {
+ val handle = Snapshot.observeSnapshots(observer)
+ try {
+ return block()
+ } finally {
+ handle.dispose()
+ }
+}
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt
index eafbbdc..6922b4e 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/ImageAssertions.android.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.abs
import kotlin.math.roundToInt
import org.junit.Assert
@@ -76,12 +77,11 @@
y: Int,
error: (Color) -> String = { color -> "Pixel($x, $y) expected to be $expected, but was $color" }
) {
- val color = this[x, y]
- val errorString = error(color)
- Assert.assertEquals(errorString, expected.red, color.red, 0.02f)
- Assert.assertEquals(errorString, expected.green, color.green, 0.02f)
- Assert.assertEquals(errorString, expected.blue, color.blue, 0.02f)
- Assert.assertEquals(errorString, expected.alpha, color.alpha, 0.02f)
+ val actual = this[x, y]
+ assert(abs(expected.red - actual.red) < 0.02f) { error(actual) }
+ assert(abs(expected.green - actual.green) < 0.02f) { error(actual) }
+ assert(abs(expected.blue - actual.blue) < 0.02f) { error(actual) }
+ assert(abs(expected.alpha - actual.alpha) < 0.02f) { error(actual) }
}
/**
diff --git a/compose/ui/ui-geometry/api/current.txt b/compose/ui/ui-geometry/api/current.txt
index c26e3c6..7cfe3bd 100644
--- a/compose/ui/ui-geometry/api/current.txt
+++ b/compose/ui/ui-geometry/api/current.txt
@@ -30,41 +30,78 @@
public final class MutableRect {
ctor public MutableRect(float left, float top, float right, float bottom);
method public operator boolean contains(long offset);
+ method public void deflate(float delta);
method public float getBottom();
+ method public long getBottomCenter();
+ method public long getBottomLeft();
+ method public long getBottomRight();
+ method public long getCenter();
+ method public long getCenterLeft();
+ method public long getCenterRight();
method public inline float getHeight();
method public float getLeft();
+ method public float getMaxDimension();
+ method public float getMinDimension();
method public float getRight();
method public long getSize();
method public float getTop();
+ method public long getTopCenter();
+ method public long getTopLeft();
+ method public long getTopRight();
method public inline float getWidth();
- method @androidx.compose.runtime.Stable public void intersect(float left, float top, float right, float bottom);
+ method public void inflate(float delta);
+ method public void intersect(float left, float top, float right, float bottom);
method public boolean isEmpty();
+ method public boolean isFinite();
+ method public boolean isInfinite();
+ method public boolean overlaps(androidx.compose.ui.geometry.MutableRect other);
+ method public boolean overlaps(androidx.compose.ui.geometry.Rect other);
method public void set(float left, float top, float right, float bottom);
method public void setBottom(float);
method public void setLeft(float);
method public void setRight(float);
method public void setTop(float);
+ method public void translate(float translateX, float translateY);
+ method public void translate(long offset);
property public final float bottom;
+ property public final long bottomCenter;
+ property public final long bottomLeft;
+ property public final long bottomRight;
+ property public final long center;
+ property public final long centerLeft;
+ property public final long centerRight;
property public final inline float height;
property public final boolean isEmpty;
+ property public final boolean isFinite;
+ property public final boolean isInfinite;
property public final float left;
+ property public final float maxDimension;
+ property public final float minDimension;
property public final float right;
property public final long size;
property public final float top;
+ property public final long topCenter;
+ property public final long topLeft;
+ property public final long topRight;
property public final inline float width;
}
public final class MutableRectKt {
+ method public static androidx.compose.ui.geometry.MutableRect MutableRect(long center, float radius);
+ method public static androidx.compose.ui.geometry.MutableRect MutableRect(long offset, long size);
+ method public static androidx.compose.ui.geometry.MutableRect MutableRect(long topLeft, long bottomRight);
method public static androidx.compose.ui.geometry.Rect toRect(androidx.compose.ui.geometry.MutableRect);
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Offset {
+ ctor public Offset(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float x, optional float y);
method @androidx.compose.runtime.Stable public operator long div(float operand);
method @androidx.compose.runtime.Stable public float getDistance();
method @androidx.compose.runtime.Stable public float getDistanceSquared();
+ method public long getPackedValue();
method public inline float getX();
method public inline float getY();
method @androidx.compose.runtime.Stable public inline boolean isValid();
@@ -73,6 +110,7 @@
method @androidx.compose.runtime.Stable public operator long rem(float operand);
method @androidx.compose.runtime.Stable public operator long times(float operand);
method @androidx.compose.runtime.Stable public inline operator long unaryMinus();
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final inline float x;
property @androidx.compose.runtime.Stable public final inline float y;
field public static final androidx.compose.ui.geometry.Offset.Companion Companion;
@@ -88,7 +126,7 @@
}
public final class OffsetKt {
- method @androidx.compose.runtime.Stable public static long Offset(float x, float y);
+ method @androidx.compose.runtime.Stable public static inline long Offset(float x, float y);
method public static inline boolean isFinite(long);
method public static inline boolean isSpecified(long);
method public static inline boolean isUnspecified(long);
@@ -232,6 +270,7 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Size {
+ ctor public Size(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float width, optional float height);
@@ -239,12 +278,14 @@
method public inline float getHeight();
method public float getMaxDimension();
method public float getMinDimension();
+ method public long getPackedValue();
method public inline float getWidth();
method @androidx.compose.runtime.Stable public boolean isEmpty();
method @androidx.compose.runtime.Stable public operator long times(float operand);
property @androidx.compose.runtime.Stable public final inline float height;
property @androidx.compose.runtime.Stable public final float maxDimension;
property @androidx.compose.runtime.Stable public final float minDimension;
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final inline float width;
field public static final androidx.compose.ui.geometry.Size.Companion Companion;
}
diff --git a/compose/ui/ui-geometry/api/restricted_current.txt b/compose/ui/ui-geometry/api/restricted_current.txt
index 8616a99..940e4ba 100644
--- a/compose/ui/ui-geometry/api/restricted_current.txt
+++ b/compose/ui/ui-geometry/api/restricted_current.txt
@@ -41,42 +41,78 @@
public final class MutableRect {
ctor public MutableRect(float left, float top, float right, float bottom);
method public operator boolean contains(long offset);
+ method public void deflate(float delta);
method public float getBottom();
+ method public long getBottomCenter();
+ method public long getBottomLeft();
+ method public long getBottomRight();
+ method public long getCenter();
+ method public long getCenterLeft();
+ method public long getCenterRight();
method public inline float getHeight();
method public float getLeft();
+ method public float getMaxDimension();
+ method public float getMinDimension();
method public float getRight();
method public long getSize();
method public float getTop();
+ method public long getTopCenter();
+ method public long getTopLeft();
+ method public long getTopRight();
method public inline float getWidth();
- method @androidx.compose.runtime.Stable public void intersect(float left, float top, float right, float bottom);
+ method public void inflate(float delta);
+ method public void intersect(float left, float top, float right, float bottom);
method public boolean isEmpty();
+ method public boolean isFinite();
+ method public boolean isInfinite();
+ method public boolean overlaps(androidx.compose.ui.geometry.MutableRect other);
+ method public boolean overlaps(androidx.compose.ui.geometry.Rect other);
method public void set(float left, float top, float right, float bottom);
method public void setBottom(float);
method public void setLeft(float);
method public void setRight(float);
method public void setTop(float);
+ method public void translate(float translateX, float translateY);
+ method public void translate(long offset);
property public final float bottom;
+ property public final long bottomCenter;
+ property public final long bottomLeft;
+ property public final long bottomRight;
+ property public final long center;
+ property public final long centerLeft;
+ property public final long centerRight;
property public final inline float height;
property public final boolean isEmpty;
+ property public final boolean isFinite;
+ property public final boolean isInfinite;
property public final float left;
+ property public final float maxDimension;
+ property public final float minDimension;
property public final float right;
property public final long size;
property public final float top;
+ property public final long topCenter;
+ property public final long topLeft;
+ property public final long topRight;
property public final inline float width;
}
public final class MutableRectKt {
+ method public static androidx.compose.ui.geometry.MutableRect MutableRect(long center, float radius);
+ method public static androidx.compose.ui.geometry.MutableRect MutableRect(long offset, long size);
+ method public static androidx.compose.ui.geometry.MutableRect MutableRect(long topLeft, long bottomRight);
method public static androidx.compose.ui.geometry.Rect toRect(androidx.compose.ui.geometry.MutableRect);
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Offset {
- ctor @kotlin.PublishedApi internal Offset(@kotlin.PublishedApi long packedValue);
+ ctor public Offset(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float x, optional float y);
method @androidx.compose.runtime.Stable public operator long div(float operand);
method @androidx.compose.runtime.Stable public float getDistance();
method @androidx.compose.runtime.Stable public float getDistanceSquared();
+ method public long getPackedValue();
method public inline float getX();
method public inline float getY();
method @androidx.compose.runtime.Stable public inline boolean isValid();
@@ -85,6 +121,7 @@
method @androidx.compose.runtime.Stable public operator long rem(float operand);
method @androidx.compose.runtime.Stable public operator long times(float operand);
method @androidx.compose.runtime.Stable public inline operator long unaryMinus();
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final inline float x;
property @androidx.compose.runtime.Stable public final inline float y;
field public static final androidx.compose.ui.geometry.Offset.Companion Companion;
@@ -100,7 +137,7 @@
}
public final class OffsetKt {
- method @androidx.compose.runtime.Stable public static long Offset(float x, float y);
+ method @androidx.compose.runtime.Stable public static inline long Offset(float x, float y);
method public static inline boolean isFinite(long);
method public static inline boolean isSpecified(long);
method public static inline boolean isUnspecified(long);
@@ -244,7 +281,7 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class Size {
- ctor @kotlin.PublishedApi internal Size(@kotlin.PublishedApi long packedValue);
+ ctor public Size(long packedValue);
method @androidx.compose.runtime.Stable public inline operator float component1();
method @androidx.compose.runtime.Stable public inline operator float component2();
method public long copy(optional float width, optional float height);
@@ -252,12 +289,14 @@
method public inline float getHeight();
method public float getMaxDimension();
method public float getMinDimension();
+ method public long getPackedValue();
method public inline float getWidth();
method @androidx.compose.runtime.Stable public boolean isEmpty();
method @androidx.compose.runtime.Stable public operator long times(float operand);
property @androidx.compose.runtime.Stable public final inline float height;
property @androidx.compose.runtime.Stable public final float maxDimension;
property @androidx.compose.runtime.Stable public final float minDimension;
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final inline float width;
field public static final androidx.compose.ui.geometry.Size.Companion Companion;
}
diff --git a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
index e4531a7..8111a01 100644
--- a/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
+++ b/compose/ui/ui-geometry/src/androidUnitTest/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
@@ -25,6 +25,11 @@
@RunWith(JUnit4::class)
class MutableRectTest {
+
+ companion object {
+ private const val DELTA = 0.01f
+ }
+
@Test
fun accessors() {
val r = MutableRect(1f, 3f, 5f, 9f)
@@ -38,6 +43,46 @@
}
@Test
+ fun `rect created by width and height`() {
+ val r = MutableRect(Offset(1.0f, 3.0f), Size(5.0f, 7.0f))
+ assertEquals(1.0f, r.left, DELTA)
+ assertEquals(3.0f, r.top, DELTA)
+ assertEquals(6.0f, r.right, DELTA)
+ assertEquals(10.0f, r.bottom, DELTA)
+ }
+
+ @Test
+ fun `rect width`() {
+ assertEquals(210f, MutableRect(70f, 10f, 280f, 300f).width)
+ }
+
+ @Test
+ fun `rect height`() {
+ assertEquals(290f, MutableRect(70f, 10f, 280f, 300f).height)
+ }
+
+ @Test
+ fun `rect size`() {
+ assertEquals(Size(210f, 290f), MutableRect(70f, 10f, 280f, 300f).size)
+ }
+
+ @Test
+ fun `rect infinite`() {
+ assertTrue(MutableRect(Float.POSITIVE_INFINITY, 10f, 200f, 500f).isInfinite)
+ assertTrue(MutableRect(10f, Float.POSITIVE_INFINITY, 200f, 500f).isInfinite)
+ assertTrue(MutableRect(10f, 200f, Float.POSITIVE_INFINITY, 500f).isInfinite)
+ assertTrue(MutableRect(10f, 200f, 500f, Float.POSITIVE_INFINITY).isInfinite)
+
+ assertFalse(MutableRect(0f, 1f, 2f, 3f).isInfinite)
+ }
+
+ @Test
+ fun `rect finite`() {
+ assertTrue(MutableRect(0f, 1f, 2f, 3f).isFinite)
+ assertFalse(MutableRect(0f, 1f, 2f, Float.POSITIVE_INFINITY).isFinite)
+ }
+
+ @Test
fun empty() {
val r = MutableRect(1f, 3f, 5f, 9f)
assertFalse(r.isEmpty)
@@ -49,6 +94,123 @@
}
@Test
+ fun `rect translate offset`() {
+ val shifted = MutableRect(0f, 5f, 10f, 15f)
+ shifted.translate(Offset(10f, 15f))
+ assertEquals(MutableRect(10f, 20f, 20f, 30f).toRect(), shifted.toRect())
+ }
+
+ @Test
+ fun `rect translate`() {
+ val translated = MutableRect(0f, 5f, 10f, 15f)
+ translated.translate(10f, 15f)
+ assertEquals(MutableRect(10f, 20f, 20f, 30f).toRect(), translated.toRect())
+ }
+
+ @Test
+ fun `rect inflate`() {
+ val inflated = MutableRect(5f, 10f, 10f, 20f)
+ inflated.inflate(5f)
+ assertEquals(MutableRect(0f, 5f, 15f, 25f).toRect(), inflated.toRect())
+ }
+
+ @Test
+ fun `rect deflate`() {
+ val deflated = MutableRect(0f, 5f, 15f, 25f)
+ deflated.deflate(5f)
+ assertEquals(MutableRect(5f, 10f, 10f, 20f).toRect(), deflated.toRect())
+ }
+
+ @Test
+ fun `rect intersect`() {
+ val intersected = MutableRect(0f, 0f, 20f, 20f)
+ intersected.intersect(10f, 10f, 30f, 30f)
+ assertEquals(MutableRect(10f, 10f, 20f, 20f).toRect(), intersected.toRect())
+ }
+
+ @Test
+ fun `rect overlap`() {
+ val rect1 = MutableRect(0f, 5f, 10f, 15f)
+ val rect2 = MutableRect(5f, 10f, 15f, 20f)
+ kotlin.test.assertTrue(rect1.overlaps(rect2))
+ kotlin.test.assertTrue(rect2.overlaps(rect1))
+ }
+
+ @Test
+ fun `rect does not overlap`() {
+ val rect1 = MutableRect(0f, 5f, 10f, 15f)
+ val rect2 = MutableRect(10f, 5f, 20f, 15f)
+ assertFalse(rect1.overlaps(rect2))
+ assertFalse(rect2.overlaps(rect1))
+ }
+
+ @Test
+ fun `rect minDimension`() {
+ val rect = MutableRect(0f, 5f, 100f, 25f)
+ assertEquals(20f, rect.minDimension)
+ }
+
+ @Test
+ fun `rect maxDimension`() {
+ val rect = MutableRect(0f, 5f, 100f, 25f)
+ assertEquals(100f, rect.maxDimension)
+ }
+
+ @Test
+ fun `rect topLeft`() {
+ val rect = MutableRect(27f, 38f, 100f, 200f)
+ assertEquals(Offset(27f, 38f), rect.topLeft)
+ }
+
+ @Test
+ fun `rect topCenter`() {
+ val rect = MutableRect(100f, 15f, 200f, 300f)
+ assertEquals(Offset(150f, 15f), rect.topCenter)
+ }
+
+ @Test
+ fun `rect topRight`() {
+ val rect = MutableRect(100f, 15f, 200f, 300f)
+ assertEquals(Offset(200f, 15f), rect.topRight)
+ }
+
+ @Test
+ fun `rect centerLeft`() {
+ val rect = MutableRect(100f, 10f, 200f, 300f)
+ assertEquals(Offset(100f, 155f), rect.centerLeft)
+ }
+
+ @Test
+ fun `rect center`() {
+ val rect = MutableRect(100f, 10f, 200f, 300f)
+ assertEquals(Offset(150f, 155f), rect.center)
+ }
+
+ @Test
+ fun `rect centerRight`() {
+ val rect = MutableRect(100f, 10f, 200f, 300f)
+ assertEquals(Offset(200f, 155f), rect.centerRight)
+ }
+
+ @Test
+ fun `rect bottomLeft`() {
+ val rect = MutableRect(100f, 10f, 200f, 300f)
+ assertEquals(Offset(100f, 300f), rect.bottomLeft)
+ }
+
+ @Test
+ fun `rect bottomCenter`() {
+ val rect = MutableRect(100f, 10f, 200f, 300f)
+ assertEquals(Offset(150f, 300f), rect.bottomCenter)
+ }
+
+ @Test
+ fun `rect bottomRight`() {
+ val rect = MutableRect(100f, 10f, 200f, 300f)
+ assertEquals(Offset(200f, 300f), rect.bottomRight)
+ }
+
+ @Test
fun contains() {
val r = MutableRect(1f, 3f, 5f, 9f)
assertTrue(Offset(1f, 3f) in r)
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt
index de47854..63e9254 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt
@@ -18,7 +18,7 @@
package androidx.compose.ui.geometry
-import androidx.compose.runtime.Stable
+import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
@@ -44,15 +44,56 @@
val size: Size
get() = Size(width, height)
+ /** Whether any of the coordinates of this rectangle are equal to positive infinity. */
+ // included for consistency with Offset and Size
+ val isInfinite: Boolean
+ get() =
+ (left == Float.POSITIVE_INFINITY) or
+ (top == Float.POSITIVE_INFINITY) or
+ (right == Float.POSITIVE_INFINITY) or
+ (bottom == Float.POSITIVE_INFINITY)
+
+ /** Whether all coordinates of this rectangle are finite. */
+ val isFinite: Boolean
+ get() =
+ ((left.toRawBits() and 0x7fffffff) < FloatInfinityBase) and
+ ((top.toRawBits() and 0x7fffffff) < FloatInfinityBase) and
+ ((right.toRawBits() and 0x7fffffff) < FloatInfinityBase) and
+ ((bottom.toRawBits() and 0x7fffffff) < FloatInfinityBase)
+
/** Whether this rectangle encloses a non-zero area. Negative areas are considered empty. */
val isEmpty: Boolean
get() = (left >= right) or (top >= bottom)
+ /** Translates the rect by the provided [Offset]. */
+ fun translate(offset: Offset) = translate(offset.x, offset.y)
+
+ /**
+ * Updates this rectangle with translateX added to the x components and translateY added to the
+ * y components.
+ */
+ fun translate(translateX: Float, translateY: Float) {
+ left += translateX
+ top += translateY
+ right += translateX
+ bottom += translateY
+ }
+
+ /** Moves edges outwards by the given delta. */
+ fun inflate(delta: Float) {
+ left -= delta
+ top -= delta
+ right += delta
+ bottom += delta
+ }
+
+ /** Moves edges inwards by the given delta. */
+ fun deflate(delta: Float) = inflate(-delta)
+
/**
* Modifies `this` to be the intersection of this and the rect formed by [left], [top], [right],
* and [bottom].
*/
- @Stable
fun intersect(left: Float, top: Float, right: Float, bottom: Float) {
this.left = max(left, this.left)
this.top = max(top, this.top)
@@ -60,6 +101,74 @@
this.bottom = min(bottom, this.bottom)
}
+ /** Whether `other` has a nonzero area of overlap with this rectangle. */
+ fun overlaps(other: Rect): Boolean {
+ return (left < other.right) and
+ (other.left < right) and
+ (top < other.bottom) and
+ (other.top < bottom)
+ }
+
+ /** Whether `other` has a nonzero area of overlap with this rectangle. */
+ fun overlaps(other: MutableRect): Boolean {
+ if (right <= other.left || other.right <= left) return false
+ if (bottom <= other.top || other.bottom <= top) return false
+ return true
+ }
+
+ /** The lesser of the magnitudes of the [width] and the [height] of this rectangle. */
+ val minDimension: Float
+ get() = min(width.absoluteValue, height.absoluteValue)
+
+ /** The greater of the magnitudes of the [width] and the [height] of this rectangle. */
+ val maxDimension: Float
+ get() = max(width.absoluteValue, height.absoluteValue)
+
+ /** The offset to the intersection of the top and left edges of this rectangle. */
+ val topLeft: Offset
+ get() = Offset(left, top)
+
+ /** The offset to the center of the top edge of this rectangle. */
+ val topCenter: Offset
+ get() = Offset(left + width / 2.0f, top)
+
+ /** The offset to the intersection of the top and right edges of this rectangle. */
+ val topRight: Offset
+ get() = Offset(right, top)
+
+ /** The offset to the center of the left edge of this rectangle. */
+ val centerLeft: Offset
+ get() = Offset(left, top + height / 2.0f)
+
+ /**
+ * The offset to the point halfway between the left and right and the top and bottom edges of
+ * this rectangle.
+ *
+ * See also [Size.center].
+ */
+ val center: Offset
+ get() = Offset(left + width / 2.0f, top + height / 2.0f)
+
+ /** The offset to the center of the right edge of this rectangle. */
+ val centerRight: Offset
+ get() = Offset(right, top + height / 2.0f)
+
+ /** The offset to the intersection of the bottom and left edges of this rectangle. */
+ val bottomLeft: Offset
+ get() = Offset(left, bottom)
+
+ /** The offset to the center of the bottom edge of this rectangle. */
+ val bottomCenter: Offset
+ get() {
+ return Offset(left + width / 2.0f, bottom)
+ }
+
+ /** The offset to the intersection of the bottom and right edges of this rectangle. */
+ val bottomRight: Offset
+ get() {
+ return Offset(right, bottom)
+ }
+
/**
* Whether the point specified by the given offset (which is assumed to be relative to the
* origin) lies between the left and right and the top and bottom edges of this rectangle.
@@ -89,3 +198,34 @@
}
fun MutableRect.toRect(): Rect = Rect(left, top, right, bottom)
+
+/**
+ * Construct a rectangle from its left and top edges as well as its width and height.
+ *
+ * @param offset Offset to represent the top and left parameters of the Rect
+ * @param size Size to determine the width and height of this [Rect].
+ * @return Rect with [Rect.left] and [Rect.top] configured to [Offset.x] and [Offset.y] as
+ * [Rect.right] and [Rect.bottom] to [Offset.x] + [Size.width] and [Offset.y] + [Size.height]
+ * respectively
+ */
+fun MutableRect(offset: Offset, size: Size): MutableRect =
+ MutableRect(offset.x, offset.y, offset.x + size.width, offset.y + size.height)
+
+/**
+ * Construct the smallest rectangle that encloses the given offsets, treating them as vectors from
+ * the origin.
+ *
+ * @param topLeft Offset representing the left and top edges of the rectangle
+ * @param bottomRight Offset representing the bottom and right edges of the rectangle
+ */
+fun MutableRect(topLeft: Offset, bottomRight: Offset): MutableRect =
+ MutableRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
+
+/**
+ * Construct a rectangle that bounds the given circle
+ *
+ * @param center Offset that represents the center of the circle
+ * @param radius Radius of the circle to enclose
+ */
+fun MutableRect(center: Offset, radius: Float): MutableRect =
+ MutableRect(center.x - radius, center.y - radius, center.x + radius, center.y + radius)
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
index 92ed64e..100212d 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Offset.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.geometry
import androidx.compose.runtime.Immutable
@@ -24,8 +26,8 @@
import androidx.compose.ui.util.unpackFloat2
import kotlin.math.sqrt
-/** Constructs an Offset from the given relative x and y offsets */
-@Stable fun Offset(x: Float, y: Float) = Offset(packFloats(x, y))
+/** Constructs an Offset from the given relative [x] and [y] offsets */
+@Stable inline fun Offset(x: Float, y: Float) = Offset(packFloats(x, y))
/**
* An immutable 2D floating-point offset.
@@ -44,15 +46,20 @@
* See also:
* * [Size], which represents a vector describing the size of a rectangle.
*
- * Creates an offset. The first argument sets [x], the horizontal component, and the second sets
- * [y], the vertical component.
+ * To create an [Offset], call the top-level function that accepts an x/y pair of coordinates:
+ * ```
+ * val offset = Offset(x, y)
+ * ```
+ *
+ * The primary constructor of [Offset] is intended to be used with the [packedValue] property to
+ * allow storing offsets in arrays or collections of primitives without boxing.
+ *
+ * @param packedValue [Long] value encoding the [x] and [y] components of the [Offset]. Encoded
+ * values can be obtained by using the [packedValue] property of existing [Offset] instances.
*/
-@Suppress("NOTHING_TO_INLINE")
@Immutable
@kotlin.jvm.JvmInline
-value class Offset
-@PublishedApi
-internal constructor(@PublishedApi internal val packedValue: Long) {
+value class Offset(val packedValue: Long) {
@Stable
inline val x: Float
get() = unpackFloat1(packedValue)
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
index d47e8e4..e288fde 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Size.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:Suppress("NOTHING_TO_INLINE")
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
package androidx.compose.ui.geometry
@@ -36,10 +36,22 @@
* Holds a 2D floating-point size.
*
* You can think of this as an [Offset] from the origin.
+ *
+ * To create a [Size], call the top-level function that accepts a width/height pair of dimensions:
+ * ```
+ * val size = Size(width, height)
+ * ```
+ *
+ * The primary constructor of [Size] is intended to be used with the [packedValue] property to allow
+ * storing sizes in arrays or collections of primitives without boxing.
+ *
+ * @param packedValue [Long] value encoding the [width] and [height] components of the [Size].
+ * Encoded values can be obtained by using the [packedValue] property of existing [Size]
+ * instances.
*/
@Immutable
@kotlin.jvm.JvmInline
-value class Size @PublishedApi internal constructor(@PublishedApi internal val packedValue: Long) {
+value class Size(val packedValue: Long) {
@Stable
inline val width: Float
get() = unpackFloat1(packedValue)
@@ -57,7 +69,6 @@
Size(packFloats(width, height))
companion object {
-
/** An empty size, one with a zero width and a zero height. */
@Stable val Zero = Size(0x0L)
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidRenderEffect.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidRenderEffect.android.kt
index e6c7790..443525f 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidRenderEffect.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidRenderEffect.android.kt
@@ -129,7 +129,14 @@
radiusY: Float,
edgeTreatment: TileMode
): android.graphics.RenderEffect =
- if (inputRenderEffect == null) {
+ if (radiusX == 0f && radiusY == 0f) {
+ // Workaround for preventing exceptions to be thrown if apps animate blur radii values
+ // through 0f. In which case the visual effect should be a no-op.
+ // The return value for each of the RenderEffect API is an opaque RenderEffect instance
+ // that wraps a native pointer, so return a no-op offset effect instead
+ // See b/241546169
+ android.graphics.RenderEffect.createOffsetEffect(0f, 0f)
+ } else if (inputRenderEffect == null) {
android.graphics.RenderEffect.createBlurEffect(
radiusX,
radiusY,
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
index ac0523c..1ddf106 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
@@ -607,7 +607,7 @@
private fun updatePathOutline(path: Path): AndroidOutline {
val resultOutline = obtainAndroidOutline()
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || path.isConvex) {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
OutlineVerificationHelper.setPath(resultOutline, path)
} else {
resultOutline.setConvexPath(path.asAndroidPath())
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
index 2ebddd2..45f1c50 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
@@ -34,6 +34,8 @@
import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -83,6 +85,11 @@
* with your own launcher.
*
* If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
*/
@ExperimentalTestApi
inline fun <reified A : ComponentActivity> createAndroidComposeRule(
@@ -132,6 +139,12 @@
* with your own launcher.
*
* If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ *
+ * @param activityClass The activity type to use in the activity scenario
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
*/
@ExperimentalTestApi
fun <A : ComponentActivity> createAndroidComposeRule(
@@ -180,7 +193,9 @@
* after one or more dependencies have been injected.
*
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
*/
@ExperimentalTestApi
fun createEmptyComposeRule(
@@ -249,7 +264,9 @@
*
* @param activityRule Test rule to use to launch the Activity.
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
* @param activityProvider Function to retrieve the Activity from the given [activityRule].
*/
@ExperimentalTestApi
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
index a0b52ec..7589a1d 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
@@ -25,6 +25,8 @@
import androidx.compose.ui.unit.Density
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
import org.junit.rules.TestRule
/**
@@ -305,7 +307,9 @@
* launched, see [createAndroidComposeRule].
*
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
*/
@ExperimentalTestApi
expect fun createComposeRule(
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/CustomEffectContextTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/CustomEffectContextTest.kt
index b4ba8f20..d7ddbb0 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/CustomEffectContextTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/CustomEffectContextTest.kt
@@ -24,10 +24,14 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
import org.junit.Test
import org.junit.runner.RunWith
@@ -136,6 +140,21 @@
}
}
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun scheduler_usedWhenPresent() {
+ val scheduler = TestCoroutineScheduler()
+ val startTime = scheduler.currentTime
+
+ // We don't need any content, we only need to trigger the scheduler
+ runComposeUiTest(scheduler) {
+ setContent { rememberCoroutineScope().launch { withFrameNanos {} } }
+ }
+
+ // Only if it is used will the scheduler's time be changed
+ assertThat(scheduler.currentTime).isNotEqualTo(startTime)
+ }
+
private class TestCoroutineContextElement : CoroutineContext.Element {
override val key: CoroutineContext.Key<*>
get() = Key
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index 5eea1ae1..23ba0f4 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -40,6 +40,7 @@
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -60,7 +61,9 @@
* @param A The Activity type to be launched, which typically (but not necessarily) hosts the
* Compose content
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
* @param block The test function.
*/
@ExperimentalTestApi
@@ -81,7 +84,9 @@
* Compose content
* @param activityClass The [Class] of the Activity type to be launched, corresponding to [A].
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
* @param block The test function.
*/
@ExperimentalTestApi
@@ -196,7 +201,9 @@
* @param A The Activity type to be interacted with, which typically (but not necessarily) is the
* activity that was launched and hosts the Compose content.
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
*/
@ExperimentalTestApi
inline fun <A : ComponentActivity> AndroidComposeUiTestEnvironment(
@@ -214,10 +221,17 @@
* some of the properties and methods on [test] will only work during the call to [runTest], as they
* require that the environment has been set up.
*
+ * If the [effectContext] contains a [TestDispatcher], that dispatcher will be used to run
+ * composition on and its [TestCoroutineScheduler] will be used to construct the [MainTestClock]. If
+ * the `effectContext` does not contain a `TestDispatcher`, an [UnconfinedTestDispatcher] will be
+ * created, using the `TestCoroutineScheduler` from the `effectContext` if present.
+ *
* @param A The Activity type to be interacted with, which typically (but not necessarily) is the
* activity that was launched and hosts the Compose content.
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
*/
@ExperimentalTestApi
@OptIn(ExperimentalCoroutinesApi::class)
@@ -235,7 +249,11 @@
private lateinit var recomposer: Recomposer
// We can only accept a TestDispatcher here because we need to access its scheduler.
private val testCoroutineDispatcher =
- effectContext[ContinuationInterceptor] as? TestDispatcher ?: UnconfinedTestDispatcher()
+ // Use the TestDispatcher if it is provided in the effectContext
+ effectContext[ContinuationInterceptor] as? TestDispatcher
+ ?:
+ // Otherwise, use the TestCoroutineScheduler if it is provided
+ UnconfinedTestDispatcher(effectContext[TestCoroutineScheduler])
private val testCoroutineScope = TestScope(testCoroutineDispatcher)
private lateinit var recomposerCoroutineScope: CoroutineScope
private val coroutineExceptionHandler = UncaughtExceptionHandler()
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 8e4aca4..d08438a 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -20,6 +20,8 @@
import androidx.compose.ui.unit.Density
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
/**
* Sets up the test environment, runs the given [test][block] and then tears down the test
@@ -38,7 +40,9 @@
* Keeping a reference to the [ComposeUiTest] outside of this function is an error.
*
* @param effectContext The [CoroutineContext] used to run the composition. The context for
- * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context. If this
+ * context contains a [TestDispatcher] or [TestCoroutineScheduler] (in that order), it will be
+ * used for composition and the [MainTestClock].
* @param block The test function.
*/
@ExperimentalTestApi
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index 08d8d65..c372803 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -94,7 +94,7 @@
}
public final class DpKt {
- method @androidx.compose.runtime.Stable public static long DpOffset(float x, float y);
+ method @androidx.compose.runtime.Stable public static inline long DpOffset(float x, float y);
method @androidx.compose.runtime.Stable public static long DpSize(float width, float height);
method @androidx.compose.runtime.Stable public static inline float coerceAtLeast(float, float minimumValue);
method @androidx.compose.runtime.Stable public static inline float coerceAtMost(float, float maximumValue);
@@ -129,11 +129,14 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DpOffset {
+ ctor public DpOffset(long packedValue);
method public long copy(optional float x, optional float y);
+ method public long getPackedValue();
method public float getX();
method public float getY();
method @androidx.compose.runtime.Stable public operator long minus(long other);
method @androidx.compose.runtime.Stable public operator long plus(long other);
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final float x;
property @androidx.compose.runtime.Stable public final float y;
field public static final androidx.compose.ui.unit.DpOffset.Companion Companion;
@@ -203,10 +206,12 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntOffset {
+ ctor public IntOffset(long packedValue);
method @androidx.compose.runtime.Stable public inline operator int component1();
method @androidx.compose.runtime.Stable public inline operator int component2();
method public long copy(optional int x, optional int y);
method @androidx.compose.runtime.Stable public operator long div(float operand);
+ method public long getPackedValue();
method public int getX();
method public int getY();
method @androidx.compose.runtime.Stable public operator long minus(long other);
@@ -214,6 +219,7 @@
method @androidx.compose.runtime.Stable public operator long rem(int operand);
method @androidx.compose.runtime.Stable public operator long times(float operand);
method @androidx.compose.runtime.Stable public operator long unaryMinus();
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final int x;
property @androidx.compose.runtime.Stable public final int y;
field public static final androidx.compose.ui.unit.IntOffset.Companion Companion;
@@ -225,7 +231,7 @@
}
public final class IntOffsetKt {
- method @androidx.compose.runtime.Stable public static long IntOffset(int x, int y);
+ method @androidx.compose.runtime.Stable public static inline long IntOffset(int x, int y);
method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, float fraction);
method @androidx.compose.runtime.Stable public static operator long minus(long, long offset);
method @androidx.compose.runtime.Stable public static operator long minus(long, long offset);
@@ -309,9 +315,11 @@
method @androidx.compose.runtime.Stable public inline operator int component2();
method @androidx.compose.runtime.Stable public operator long div(int other);
method public inline int getHeight();
+ method public long getPackedValue();
method public inline int getWidth();
method @androidx.compose.runtime.Stable public operator long times(int other);
property @androidx.compose.runtime.Stable public final inline int height;
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final inline int width;
field public static final androidx.compose.ui.unit.IntSize.Companion Companion;
}
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index 8732633..6f13e9f 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -94,7 +94,7 @@
}
public final class DpKt {
- method @androidx.compose.runtime.Stable public static long DpOffset(float x, float y);
+ method @androidx.compose.runtime.Stable public static inline long DpOffset(float x, float y);
method @androidx.compose.runtime.Stable public static long DpSize(float width, float height);
method @androidx.compose.runtime.Stable public static inline float coerceAtLeast(float, float minimumValue);
method @androidx.compose.runtime.Stable public static inline float coerceAtMost(float, float maximumValue);
@@ -129,11 +129,14 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DpOffset {
+ ctor public DpOffset(long packedValue);
method public long copy(optional float x, optional float y);
+ method public long getPackedValue();
method public float getX();
method public float getY();
method @androidx.compose.runtime.Stable public operator long minus(long other);
method @androidx.compose.runtime.Stable public operator long plus(long other);
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final float x;
property @androidx.compose.runtime.Stable public final float y;
field public static final androidx.compose.ui.unit.DpOffset.Companion Companion;
@@ -203,10 +206,12 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntOffset {
+ ctor public IntOffset(long packedValue);
method @androidx.compose.runtime.Stable public inline operator int component1();
method @androidx.compose.runtime.Stable public inline operator int component2();
method public long copy(optional int x, optional int y);
method @androidx.compose.runtime.Stable public operator long div(float operand);
+ method public long getPackedValue();
method public int getX();
method public int getY();
method @androidx.compose.runtime.Stable public operator long minus(long other);
@@ -214,6 +219,7 @@
method @androidx.compose.runtime.Stable public operator long rem(int operand);
method @androidx.compose.runtime.Stable public operator long times(float operand);
method @androidx.compose.runtime.Stable public operator long unaryMinus();
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final int x;
property @androidx.compose.runtime.Stable public final int y;
field public static final androidx.compose.ui.unit.IntOffset.Companion Companion;
@@ -225,7 +231,7 @@
}
public final class IntOffsetKt {
- method @androidx.compose.runtime.Stable public static long IntOffset(int x, int y);
+ method @androidx.compose.runtime.Stable public static inline long IntOffset(int x, int y);
method @androidx.compose.runtime.Stable public static long lerp(long start, long stop, float fraction);
method @androidx.compose.runtime.Stable public static operator long minus(long, long offset);
method @androidx.compose.runtime.Stable public static operator long minus(long, long offset);
@@ -305,14 +311,16 @@
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntSize {
- ctor @kotlin.PublishedApi internal IntSize(@kotlin.PublishedApi long packedValue);
+ ctor @kotlin.PublishedApi internal IntSize(long packedValue);
method @androidx.compose.runtime.Stable public inline operator int component1();
method @androidx.compose.runtime.Stable public inline operator int component2();
method @androidx.compose.runtime.Stable public operator long div(int other);
method public inline int getHeight();
+ method public long getPackedValue();
method public inline int getWidth();
method @androidx.compose.runtime.Stable public operator long times(int other);
property @androidx.compose.runtime.Stable public final inline int height;
+ property public final long packedValue;
property @androidx.compose.runtime.Stable public final inline int width;
field public static final androidx.compose.ui.unit.IntSize.Companion Companion;
}
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
index 4cc9af8..45a4b6d 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("NOTHING_TO_INLINE")
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
package androidx.compose.ui.unit
@@ -181,12 +181,25 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
/** Constructs a [DpOffset] from [x] and [y] position [Dp] values. */
-@Stable fun DpOffset(x: Dp, y: Dp): DpOffset = DpOffset(packFloats(x.value, y.value))
+@Stable inline fun DpOffset(x: Dp, y: Dp): DpOffset = DpOffset(packFloats(x.value, y.value))
-/** A two-dimensional offset using [Dp] for units */
+/**
+ * A two-dimensional offset using [Dp] for units.
+ *
+ * To create a [DpOffset], call the top-level function that accepts an x/y pair of coordinates:
+ * ```
+ * val offset = DpOffset(x, y)
+ * ```
+ *
+ * The primary constructor of [DpOffset] is intended to be used with the [packedValue] property to
+ * allow storing offsets in arrays or collections of primitives without boxing.
+ *
+ * @param packedValue [Long] value encoding the [x] and [y] components of the [DpOffset]. Encoded
+ * values can be obtained by using the [packedValue] property of existing [DpOffset] instances.
+ */
@Immutable
@JvmInline
-value class DpOffset internal constructor(@PublishedApi internal val packedValue: Long) {
+value class DpOffset(val packedValue: Long) {
/** The horizontal aspect of the offset in [Dp] */
@Stable
val x: Dp
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
index 9bafe44..ca4caa3 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:Suppress("NOTHING_TO_INLINE")
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
package androidx.compose.ui.unit
@@ -29,12 +29,25 @@
import kotlin.jvm.JvmInline
/** Constructs a [IntOffset] from [x] and [y] position [Int] values. */
-@Stable fun IntOffset(x: Int, y: Int): IntOffset = IntOffset(packInts(x, y))
+@Stable inline fun IntOffset(x: Int, y: Int): IntOffset = IntOffset(packInts(x, y))
-/** A two-dimensional position using [Int] pixels for units */
+/**
+ * A two-dimensional position using [Int] pixels for units.
+ *
+ * To create an [IntOffset], call the top-level function that accepts an x/y pair of coordinates:
+ * ```
+ * val offset = IntOffset(x, y)
+ * ```
+ *
+ * The primary constructor of [IntOffset] is intended to be used with the [packedValue] property to
+ * allow storing offsets in arrays or collections of primitives without boxing.
+ *
+ * @param packedValue [Long] value encoding the [x] and [y] components of the [IntOffset]. Encoded
+ * values can be obtained by using the [packedValue] property of existing [IntOffset] instances.
+ */
@Immutable
@JvmInline
-value class IntOffset internal constructor(@PublishedApi internal val packedValue: Long) {
+value class IntOffset(val packedValue: Long) {
/** The horizontal aspect of the position in [Int] pixels. */
@Stable
val x: Int
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntSize.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntSize.kt
index 48c1d7d..021b42d 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntSize.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntSize.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:Suppress("NOTHING_TO_INLINE")
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
package androidx.compose.ui.unit
@@ -29,12 +29,25 @@
/** Constructs an [IntSize] from width and height [Int] values. */
@Stable inline fun IntSize(width: Int, height: Int): IntSize = IntSize(packInts(width, height))
-/** A two-dimensional size class used for measuring in [Int] pixels. */
+/**
+ * A two-dimensional size class used for measuring in [Int] pixels.
+ *
+ * To create an [IntSize], call the top-level function that accepts a width/height pair of
+ * dimensions:
+ * ```
+ * val size = IntSize(width, height)
+ * ```
+ *
+ * The primary constructor of [IntSize] is intended to be used with the [packedValue] property to
+ * allow storing sizes in arrays or collections of primitives without boxing.
+ *
+ * @param packedValue [Long] value encoding the [width] and [height] components of the [IntSize].
+ * Encoded values can be obtained by using the [packedValue] property of existing [IntSize]
+ * instances.
+ */
@Immutable
@kotlin.jvm.JvmInline
-value class IntSize
-@PublishedApi
-internal constructor(@PublishedApi internal val packedValue: Long) {
+value class IntSize @PublishedApi internal constructor(val packedValue: Long) {
/** The horizontal aspect of the size in [Int] pixels. */
@Stable
inline val width: Int
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index 216790ac..57823b5 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -66,10 +66,16 @@
method public static float fastCbrt(float x);
method public static inline double fastCoerceAtLeast(double, double minimumValue);
method public static inline float fastCoerceAtLeast(float, float minimumValue);
+ method public static inline int fastCoerceAtLeast(int, int minimumValue);
+ method public static inline long fastCoerceAtLeast(long, long minimumValue);
method public static inline double fastCoerceAtMost(double, double maximumValue);
method public static inline float fastCoerceAtMost(float, float maximumValue);
+ method public static inline int fastCoerceAtMost(int, int maximumValue);
+ method public static inline long fastCoerceAtMost(long, long maximumValue);
method public static inline double fastCoerceIn(double, double minimumValue, double maximumValue);
method public static inline float fastCoerceIn(float, float minimumValue, float maximumValue);
+ method public static inline int fastCoerceIn(int, int minimumValue, int maximumValue);
+ method public static inline long fastCoerceIn(long, long minimumValue, long maximumValue);
method public static inline boolean fastIsFinite(double);
method public static inline boolean fastIsFinite(float);
method public static inline float fastMaxOf(float a, float b, float c, float d);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index 216790ac..57823b5 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -66,10 +66,16 @@
method public static float fastCbrt(float x);
method public static inline double fastCoerceAtLeast(double, double minimumValue);
method public static inline float fastCoerceAtLeast(float, float minimumValue);
+ method public static inline int fastCoerceAtLeast(int, int minimumValue);
+ method public static inline long fastCoerceAtLeast(long, long minimumValue);
method public static inline double fastCoerceAtMost(double, double maximumValue);
method public static inline float fastCoerceAtMost(float, float maximumValue);
+ method public static inline int fastCoerceAtMost(int, int maximumValue);
+ method public static inline long fastCoerceAtMost(long, long maximumValue);
method public static inline double fastCoerceIn(double, double minimumValue, double maximumValue);
method public static inline float fastCoerceIn(float, float minimumValue, float maximumValue);
+ method public static inline int fastCoerceIn(int, int minimumValue, int maximumValue);
+ method public static inline long fastCoerceIn(long, long minimumValue, long maximumValue);
method public static inline boolean fastIsFinite(double);
method public static inline boolean fastIsFinite(float);
method public static inline float fastMaxOf(float a, float b, float c, float d);
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
index fa82086..ace3e1b 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
@@ -93,6 +93,42 @@
}
/**
+ * Returns this integer value clamped in the inclusive range defined by [minimumValue] and
+ * [maximumValue]. Unlike [Int.coerceIn], the range is not validated: the caller must ensure that
+ * [minimumValue] is less than [maximumValue].
+ */
+inline fun Int.fastCoerceIn(minimumValue: Int, maximumValue: Int) =
+ this.fastCoerceAtLeast(minimumValue).fastCoerceAtMost(maximumValue)
+
+/** Ensures that this value is not less than the specified [minimumValue]. */
+inline fun Int.fastCoerceAtLeast(minimumValue: Int): Int {
+ return if (this < minimumValue) minimumValue else this
+}
+
+/** Ensures that this value is not greater than the specified [maximumValue]. */
+inline fun Int.fastCoerceAtMost(maximumValue: Int): Int {
+ return if (this > maximumValue) maximumValue else this
+}
+
+/**
+ * Returns this long value clamped in the inclusive range defined by [minimumValue] and
+ * [maximumValue]. Unlike [Long.coerceIn], the range is not validated: the caller must ensure that
+ * [minimumValue] is less than [maximumValue].
+ */
+inline fun Long.fastCoerceIn(minimumValue: Long, maximumValue: Long) =
+ this.fastCoerceAtLeast(minimumValue).fastCoerceAtMost(maximumValue)
+
+/** Ensures that this value is not less than the specified [minimumValue]. */
+inline fun Long.fastCoerceAtLeast(minimumValue: Long): Long {
+ return if (this < minimumValue) minimumValue else this
+}
+
+/** Ensures that this value is not greater than the specified [maximumValue]. */
+inline fun Long.fastCoerceAtMost(maximumValue: Long): Long {
+ return if (this > maximumValue) maximumValue else this
+}
+
+/**
* Returns `true` if this float is a finite floating-point value; returns `false` otherwise (for
* `NaN` and infinity).
*/
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index a1a5b93..cc929b6 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -3189,10 +3189,8 @@
}
@androidx.compose.runtime.Stable public interface WindowInfo {
- method public default long getContainerSize();
method public default int getKeyboardModifiers();
method public boolean isWindowFocused();
- property public default long containerSize;
property public abstract boolean isWindowFocused;
property public default int keyboardModifiers;
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 1c68e96..2c9b7cf 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -3247,10 +3247,8 @@
}
@androidx.compose.runtime.Stable public interface WindowInfo {
- method public default long getContainerSize();
method public default int getKeyboardModifiers();
method public boolean isWindowFocused();
- property public default long containerSize;
property public abstract boolean isWindowFocused;
property public default int keyboardModifiers;
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
index 9c909f6..dfd58d7 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
@@ -34,6 +34,9 @@
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,6 +48,7 @@
private val nestedScrollingCaseFactory = { NestedScrollingTestCase() }
+ @Ignore("b/362302352")
@Test
fun nested_scroll_propagation() {
benchmarkRule.runBenchmarkFor(nestedScrollingCaseFactory) {
@@ -136,12 +140,12 @@
}
fun assertPostToggle() {
- assert(collectedDeltasOuter != Offset.Zero)
- assert(collectedDeltasMiddle != Offset.Zero)
- assert(collectedVelocityOuter != Velocity.Zero)
- assert(collectedVelocityMiddle != Velocity.Zero)
+ assertNotEquals(collectedDeltasOuter, Offset.Zero)
+ assertNotEquals(collectedDeltasMiddle, Offset.Zero)
+ assertNotEquals(collectedVelocityOuter, Velocity.Zero)
+ assertNotEquals(collectedVelocityMiddle, Velocity.Zero)
- assert(collectedDeltasOuter == scrollResult)
- assert(collectedVelocityOuter == velocityResult)
+ assertEquals(scrollResult, collectedDeltasOuter)
+ assertEquals(velocityResult, collectedVelocityOuter)
}
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/VelocityTrackerBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/VelocityTrackerBenchmark.kt
index d78ffcb..a0e4339 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/VelocityTrackerBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/VelocityTrackerBenchmark.kt
@@ -19,6 +19,7 @@
import androidx.compose.ui.input.pointer.util.VelocityTracker1D
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -95,7 +96,7 @@
velocityTracker.addDataPoint(dataPoint.timeMillis, dataPoint.motionValue)
}
- benchmarkRule.measureRepeated { assert(velocityTracker.calculateVelocity() != 0f) }
+ benchmarkRule.measureRepeated { assertTrue(velocityTracker.calculateVelocity() != 0f) }
}
companion object {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 433b926..8905d36 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -90,7 +90,6 @@
api("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
implementation("androidx.emoji2:emoji2:1.2.0")
- implementation("androidx.window:window:1.3.0")
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index 7aa5b1a..ad5c009 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -19,8 +19,10 @@
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.IndicationNodeFactory
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -28,6 +30,7 @@
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -59,7 +62,9 @@
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -1168,6 +1173,50 @@
}
}
+ @Test
+ fun testInvalidationAfterIndicationWasCreated() {
+ var stateToSwitch: MutableState<Boolean>? = null
+ var drawCount = 0
+ val indication =
+ object : IndicationNodeFactory {
+ override fun create(interactionSource: InteractionSource): DelegatableNode =
+ object : Modifier.Node(), DrawModifierNode {
+ val state = mutableStateOf(false).also { stateToSwitch = it }
+
+ override fun ContentDrawScope.draw() {
+ state.value // read state
+ drawCount++
+ drawContent()
+ }
+ }
+
+ override fun hashCode(): Int = super.hashCode()
+
+ override fun equals(other: Any?): Boolean = super.equals(other)
+ }
+
+ rule.setContent {
+ Box(
+ Modifier.size(40.dp)
+ .clickable(interactionSource = null, indication = indication) {}
+ .testTag("clickable")
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(drawCount).isEqualTo(0)
+ assertThat(stateToSwitch).isNull()
+ }
+ rule.onNodeWithTag("clickable").performClick()
+ rule.runOnIdle {
+ assertThat(stateToSwitch).isNotNull()
+ assertThat(drawCount).isEqualTo(1)
+ stateToSwitch?.value = true
+ }
+
+ rule.runOnIdle { assertThat(drawCount).isEqualTo(2) }
+ }
+
// captureToImage() requires API level 26
@RequiresApi(Build.VERSION_CODES.O)
private fun SemanticsNodeInteraction.captureToBitmap() = captureToImage().asAndroidBitmap()
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index c546076..a247619 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -481,6 +481,14 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+ fun testZeroRadiusBlurDoesNotCrash() {
+ val tag = "blurTag"
+ val size = 100f
+ rule.setContent { BoxBlur(tag, size, 0f) }
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O, maxSdkVersion = Build.VERSION_CODES.R)
fun testBlurNoopOnUnsupportedPlatforms() {
val tag = "blurTag"
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 223da87..38f3d5c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -2557,8 +2557,9 @@
// Verify initial offset, should be the same values for the "excluded" offset
positionToExcludedArray.forEachIndexed { index, (position, excluded) ->
// Rounding to avoid -0.0f
- assertEquals((index * boxSizePx).fastRoundToInt(), position.y.fastRoundToInt())
- assertEquals((index * boxSizePx).fastRoundToInt(), excluded.y.fastRoundToInt())
+ val expected = (index * boxSizePx).fastRoundToInt()
+ assertEquals("At index: $index", expected, position.y.fastRoundToInt())
+ assertEquals("At index: $index", expected, excluded.y.fastRoundToInt())
}
// Scroll to the end
@@ -2633,8 +2634,9 @@
// Verify initial offset, should be the same values for the "excluded" offset
positionToExcludedArray.forEachIndexed { index, (position, excluded) ->
// Rounding to avoid -0.0f
- assertEquals((index * boxSizePx).fastRoundToInt(), position.y.fastRoundToInt())
- assertEquals((index * boxSizePx).fastRoundToInt(), excluded.y.fastRoundToInt())
+ val expected = (index * boxSizePx).fastRoundToInt()
+ assertEquals("At index: $index", expected, position.y.fastRoundToInt())
+ assertEquals("At index: $index", expected, excluded.y.fastRoundToInt())
}
// Scroll to the end
@@ -3102,6 +3104,134 @@
assertEquals(0, lookingAheadPositionExcludingDmp.y.fastRoundToInt())
}
+ @Test
+ fun testLookaheadAndApproachCoordinatesAreConsistentOnFirstPass_usingAlign() =
+ with(rule.density) {
+ val rootSizePx = 300
+ val alignmentOffsetPx = IntOffset(0, 100)
+
+ // Both positions are expected to be from lookahead coordinates
+ var lookaheadPassPosition = Offset.Unspecified
+ var approachPassPosition = Offset.Unspecified
+
+ rule.setContent {
+ Box(Modifier.size(rootSizePx.toDp())) {
+ LookaheadScope {
+ Box(Modifier.align { _, _, _ -> alignmentOffsetPx }.fillMaxWidth()) {
+ // Capture lookahead coordinates from Lookahead and Approach pass.
+ Box(
+ Modifier.onLookaheadPassCoordinates(this@LookaheadScope) {
+ lookaheadScopeCoordinates,
+ coordinates ->
+ lookaheadPassPosition =
+ lookaheadScopeCoordinates.localPositionOf(coordinates)
+ }
+ .onApproachPassCoordinates(this@LookaheadScope) {
+ lookaheadScopeCoordinates,
+ coordinates ->
+ approachPassPosition =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(
+ coordinates
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // Assert both positions are equal on the first pass.
+ assertEquals(alignmentOffsetPx, lookaheadPassPosition.round())
+ assertEquals(alignmentOffsetPx, approachPassPosition.round())
+ }
+
+ @Test
+ fun testLookaheadAndApproachCoordinatesAreConsistentOnFirstPass_usingOffset() =
+ with(rule.density) {
+ val rootSizePx = 300
+ val alignmentOffsetPx = IntOffset(0, 100)
+
+ // Both positions are expected to be from lookahead coordinates
+ var lookaheadPassPosition = Offset.Unspecified
+ var approachPassPosition = Offset.Unspecified
+
+ rule.setContent {
+ Box(Modifier.size(rootSizePx.toDp())) {
+ LookaheadScope {
+ Box(Modifier.offset { alignmentOffsetPx }.fillMaxWidth()) {
+ // Capture lookahead coordinates from Lookahead and Approach pass.
+ Box(
+ Modifier.onLookaheadPassCoordinates(this@LookaheadScope) {
+ lookaheadScopeCoordinates,
+ coordinates ->
+ lookaheadPassPosition =
+ lookaheadScopeCoordinates.localPositionOf(coordinates)
+ }
+ .onApproachPassCoordinates(this@LookaheadScope) {
+ lookaheadScopeCoordinates,
+ coordinates ->
+ approachPassPosition =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(
+ coordinates
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ // Assert both positions are equal on the first pass.
+ assertEquals(alignmentOffsetPx, lookaheadPassPosition.round())
+ assertEquals(alignmentOffsetPx, approachPassPosition.round())
+ }
+
+ /** Capture LookaheadScope coordinates during the Lookahead pass. */
+ private fun Modifier.onLookaheadPassCoordinates(
+ lookaheadScope: LookaheadScope,
+ onLookaheadPassCoordinates:
+ (
+ lookaheadScopeCoordinates: LayoutCoordinates, layoutCoordinates: LayoutCoordinates
+ ) -> Unit
+ ): Modifier =
+ with(lookaheadScope) {
+ [email protected] { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ if (isLookingAhead) {
+ coordinates?.let { coordinates ->
+ onLookaheadPassCoordinates(lookaheadScopeCoordinates, coordinates)
+ }
+ }
+ placeable.place(0, 0)
+ }
+ }
+ }
+
+ /** Capture LookaheadScope coordinates during the Approach pass. */
+ private fun Modifier.onApproachPassCoordinates(
+ lookaheadScope: LookaheadScope,
+ onApproachPassCoordinates:
+ (
+ lookaheadScopeCoordinates: LayoutCoordinates, layoutCoordinates: LayoutCoordinates
+ ) -> Unit
+ ): Modifier =
+ with(lookaheadScope) {
+ [email protected](
+ isMeasurementApproachInProgress = { false },
+ isPlacementApproachInProgress = {
+ onApproachPassCoordinates(lookaheadScopeCoordinates, it)
+ false
+ },
+ approachMeasure = { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ }
+ )
+ }
+
private fun assertSameLayoutWithAndWithoutLookahead(
content: @Composable (modifier: Modifier) -> Unit
) {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
index 5d4e345..e543a0e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
@@ -27,7 +27,6 @@
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
@@ -271,24 +270,4 @@
rule.waitForIdle()
assertThat(keyModifiers.packedValue).isEqualTo(0)
}
-
- @Test
- fun windowInfo_containerSize() {
- // Arrange.
- var containerSize = IntSize.Zero
- var recompositions = 0
- rule.setContent {
- BasicText("Main Window")
- val windowInfo = LocalWindowInfo.current
- containerSize = windowInfo.containerSize
- recompositions++
- }
-
- // Act.
- rule.waitForIdle()
-
- // Assert.
- assertThat(containerSize).isNotEqualTo(IntSize.Zero)
- assertThat(recompositions).isEqualTo(1)
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt
index 57282e8..f484db7 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt
@@ -181,6 +181,12 @@
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
+
+ // TODO(b/354025981) This test is flaky when moving focus programmatically.
+ // Note: Moving focus programmatically among views is a stretch goal,
+ // as the view system does not have a moveFocus() API.
+ if (!moveFocusProgrammatically) return
+
// Arrange.
var isComposableFocused = false
setContent {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 176a32f..e64198f 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -188,7 +188,6 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastIsFinite
import androidx.compose.ui.util.fastLastOrNull
@@ -210,7 +209,6 @@
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
-import androidx.window.layout.WindowMetricsCalculator
import java.lang.reflect.Method
import java.util.function.Consumer
import kotlin.coroutines.CoroutineContext
@@ -577,10 +575,7 @@
// on a different position, but also in the position of each of the grandparents as all these
// positions add up to final global position)
private val globalLayoutListener =
- ViewTreeObserver.OnGlobalLayoutListener {
- updatePositionCacheAndDispatch()
- updateWindowMetrics()
- }
+ ViewTreeObserver.OnGlobalLayoutListener { updatePositionCacheAndDispatch() }
// executed when a scrolling container like ScrollView of RecyclerView performed the scroll,
// this could affect our global position
@@ -1603,7 +1598,7 @@
invalidateLayers(root)
}
measureAndLayout()
- Snapshot.sendApplyNotifications()
+ Snapshot.notifyObjectsInitialized()
isDrawingContent = true
// we don't have to observe here because the root has a layer modifier
@@ -1708,7 +1703,6 @@
override fun onAttachedToWindow() {
super.onAttachedToWindow()
_windowInfo.isWindowFocused = hasWindowFocus()
- updateWindowMetrics()
invalidateLayoutNodeMeasurement(root)
invalidateLayers(root)
snapshotObserver.startObserving()
@@ -2228,11 +2222,6 @@
viewToWindowMatrix.invertTo(windowToViewMatrix)
}
- private fun updateWindowMetrics() {
- val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
- _windowInfo.containerSize = metrics.bounds.toComposeIntSize()
- }
-
override fun onCheckIsTextEditor(): Boolean {
val parentSession =
textInputSessionMutex.currentSession
@@ -2265,7 +2254,6 @@
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
density = Density(context)
- updateWindowMetrics()
if (newConfig.fontWeightAdjustmentCompat != currentFontWeightAdjustment) {
currentFontWeightAdjustment = newConfig.fontWeightAdjustmentCompat
fontFamilyResolver = createFontFamilyResolver(context)
@@ -2855,5 +2843,3 @@
)
return ViewCompatShims.getContentCaptureSession(this)
}
-
-private fun Rect.toComposeIntSize() = IntSize(width = width(), height = height())
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/OutlineResolver.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/OutlineResolver.android.kt
index 9f26185..987b13a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/OutlineResolver.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/OutlineResolver.android.kt
@@ -18,6 +18,8 @@
import android.graphics.Outline as AndroidOutline
import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -278,8 +280,11 @@
@Suppress("deprecation")
private fun updateCacheWithPath(composePath: Path) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || composePath.isConvex) {
- // TODO(mount): Use setPath() for R+ when available.
- cachedOutline.setConvexPath(composePath.asAndroidPath())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ OutlineVerificationHelper.setPath(cachedOutline, composePath)
+ } else {
+ cachedOutline.setConvexPath(composePath.asAndroidPath())
+ }
usePathForClip = !cachedOutline.canClip()
} else {
isSupportedOutline = false // Concave outlines are not supported on older API levels
@@ -305,3 +310,12 @@
topLeftCornerRadius.x == radius
}
}
+
+@RequiresApi(Build.VERSION_CODES.R)
+internal object OutlineVerificationHelper {
+
+ @DoNotInline
+ fun setPath(outline: AndroidOutline, path: Path) {
+ outline.setPath(path.asAndroidPath())
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index 9c1faaf..52695a6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -141,20 +141,26 @@
)
}
} else {
- val rootDelegate = lookaheadDelegate.rootLookaheadDelegate
// This is a case of mixed coordinates where `this` is lookahead coords, and
// `sourceCoordinates` isn't. Therefore we'll break this into two parts:
// local position in lookahead coords space && local position in regular layout coords
// space.
+ val rootDelegate = lookaheadDelegate.rootLookaheadDelegate
+
val localLookaheadPos =
localPositionOf(
sourceCoordinates = rootDelegate.lookaheadLayoutCoordinates,
relativeToSource = relativeToSource,
includeMotionFrameOfReference = includeMotionFrameOfReference
- )
+ ) - rootDelegate.position.toOffset()
+
+ // If Lookahead is the hierarchy's absolute root (no parent), we may use its coordinates
+ // directly
+ val rootDelegateCoordinates =
+ rootDelegate.coordinator.parentCoordinates ?: rootDelegate.coordinator.coordinates
val localPos =
- rootDelegate.coordinator.coordinates.localPositionOf(
+ rootDelegateCoordinates.localPositionOf(
sourceCoordinates = sourceCoordinates,
relativeToSource = Offset.Zero,
includeMotionFrameOfReference = includeMotionFrameOfReference
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt
index 6cba4e3..d92f726 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt
@@ -24,7 +24,6 @@
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
-import androidx.compose.ui.unit.IntSize
/** Provides information about the Window that is hosting this compose hierarchy. */
@Stable
@@ -42,10 +41,6 @@
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
val keyboardModifiers: PointerKeyboardModifiers
get() = WindowInfoImpl.GlobalKeyboardModifiers.value
-
- /** Size of the window's content container in pixels. */
- val containerSize: IntSize
- get() = IntSize.Zero
}
@Composable
@@ -59,13 +54,12 @@
internal class WindowInfoImpl : WindowInfo {
private val _isWindowFocused = mutableStateOf(false)
- private val _containerSize = mutableStateOf(IntSize.Zero)
override var isWindowFocused: Boolean
- get() = _isWindowFocused.value
set(value) {
_isWindowFocused.value = value
}
+ get() = _isWindowFocused.value
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
override var keyboardModifiers: PointerKeyboardModifiers
@@ -74,12 +68,6 @@
GlobalKeyboardModifiers.value = value
}
- override var containerSize: IntSize
- get() = _containerSize.value
- set(value) {
- _containerSize.value = value
- }
-
companion object {
// One instance across all windows makes sense, since the state of KeyboardModifiers is
// common for all windows.
diff --git a/constraintlayout/constraintlayout-compose/api/1.1.0-beta01.txt b/constraintlayout/constraintlayout-compose/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..f9829bc
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/api/1.1.0-beta01.txt
@@ -0,0 +1,1013 @@
+// Signature format: 4.0
+package androidx.constraintlayout.compose {
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class Arc {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.Arc.Companion Companion;
+ }
+
+ public static final class Arc.Companion {
+ method public androidx.constraintlayout.compose.Arc getAbove();
+ method public androidx.constraintlayout.compose.Arc getBelow();
+ method public androidx.constraintlayout.compose.Arc getFlip();
+ method public androidx.constraintlayout.compose.Arc getNone();
+ method public androidx.constraintlayout.compose.Arc getStartHorizontal();
+ method public androidx.constraintlayout.compose.Arc getStartVertical();
+ property public final androidx.constraintlayout.compose.Arc Above;
+ property public final androidx.constraintlayout.compose.Arc Below;
+ property public final androidx.constraintlayout.compose.Arc Flip;
+ property public final androidx.constraintlayout.compose.Arc None;
+ property public final androidx.constraintlayout.compose.Arc StartHorizontal;
+ property public final androidx.constraintlayout.compose.Arc StartVertical;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public abstract sealed class BaseKeyFrameScope {
+ method protected final <E extends androidx.constraintlayout.compose.NamedPropertyOrValue> kotlin.properties.ObservableProperty<E> addNameOnPropertyChange(E initialValue, optional String? nameOverride);
+ method protected final <T> kotlin.properties.ObservableProperty<T> addOnPropertyChange(T initialValue, optional String? nameOverride);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public abstract sealed class BaseKeyFramesScope {
+ method public final androidx.constraintlayout.compose.Easing getEasing();
+ method public final void setEasing(androidx.constraintlayout.compose.Easing);
+ property public final androidx.constraintlayout.compose.Easing easing;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+ }
+
+ @androidx.compose.runtime.Immutable public final class ChainStyle {
+ field public static final androidx.constraintlayout.compose.ChainStyle.Companion Companion;
+ }
+
+ public static final class ChainStyle.Companion {
+ method @androidx.compose.runtime.Stable public androidx.constraintlayout.compose.ChainStyle Packed(float bias);
+ method public androidx.constraintlayout.compose.ChainStyle getPacked();
+ method public androidx.constraintlayout.compose.ChainStyle getSpread();
+ method public androidx.constraintlayout.compose.ChainStyle getSpreadInside();
+ property public final androidx.constraintlayout.compose.ChainStyle Packed;
+ property public final androidx.constraintlayout.compose.ChainStyle Spread;
+ property public final androidx.constraintlayout.compose.ChainStyle SpreadInside;
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class ConstrainScope {
+ method public androidx.constraintlayout.compose.Dimension asDimension(float);
+ method public void centerAround(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor);
+ method public void centerAround(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor);
+ method public void centerHorizontallyTo(androidx.constraintlayout.compose.ConstrainedLayoutReference other, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void centerTo(androidx.constraintlayout.compose.ConstrainedLayoutReference other);
+ method public void centerVerticallyTo(androidx.constraintlayout.compose.ConstrainedLayoutReference other, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void circular(androidx.constraintlayout.compose.ConstrainedLayoutReference other, float angle, float distance);
+ method public void clearConstraints();
+ method public void clearHorizontal();
+ method public void clearVertical();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
+ method public float getAlpha();
+ method public androidx.constraintlayout.compose.BaselineAnchorable getBaseline();
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
+ method public androidx.constraintlayout.compose.Dimension getHeight();
+ method public float getHorizontalBias();
+ method public float getHorizontalChainWeight();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getStart();
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getTop();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public float getVerticalBias();
+ method public float getVerticalChainWeight();
+ method public androidx.constraintlayout.compose.Visibility getVisibility();
+ method public androidx.constraintlayout.compose.Dimension getWidth();
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional @FloatRange(from=0.0, to=1.0) float horizontalBias, optional @FloatRange(from=0.0, to=1.0) float verticalBias);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void resetDimensions();
+ method public void resetTransforms();
+ method public void setAlpha(float);
+ method public void setHeight(androidx.constraintlayout.compose.Dimension);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalChainWeight(float);
+ method public void setPivotX(float);
+ method public void setPivotY(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ method public void setVerticalBias(float);
+ method public void setVerticalChainWeight(float);
+ method public void setVisibility(androidx.constraintlayout.compose.Visibility);
+ method public void setWidth(androidx.constraintlayout.compose.Dimension);
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteLeft;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteRight;
+ property public final float alpha;
+ property public final androidx.constraintlayout.compose.BaselineAnchorable baseline;
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable end;
+ property public final androidx.constraintlayout.compose.Dimension height;
+ property public final float horizontalBias;
+ property public final float horizontalChainWeight;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
+ property public final float pivotX;
+ property public final float pivotY;
+ property public final float rotationX;
+ property public final float rotationY;
+ property public final float rotationZ;
+ property public final float scaleX;
+ property public final float scaleY;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable start;
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable top;
+ property public final float translationX;
+ property public final float translationY;
+ property public final float translationZ;
+ property public final float verticalBias;
+ property public final float verticalChainWeight;
+ property public final androidx.constraintlayout.compose.Visibility visibility;
+ property public final androidx.constraintlayout.compose.Dimension width;
+ }
+
+ @androidx.compose.runtime.Stable public final class ConstrainedLayoutReference extends androidx.constraintlayout.compose.LayoutReference {
+ ctor public ConstrainedLayoutReference(Object id);
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteRight();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor getBaseline();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getBottom();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getEnd();
+ method public Object getId();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getStart();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getTop();
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteLeft;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteRight;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor baseline;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end;
+ property public Object id;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top;
+ }
+
+ public abstract class ConstraintLayoutBaseScope {
+ ctor public ConstraintLayoutBaseScope();
+ method public final void applyTo(androidx.constraintlayout.compose.State state);
+ method public final androidx.constraintlayout.compose.ConstrainScope constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+ method public final void constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference[] refs, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+ method public final androidx.constraintlayout.compose.HorizontalChainScope constrain(androidx.constraintlayout.compose.HorizontalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.HorizontalChainScope,kotlin.Unit> constrainBlock);
+ method public final androidx.constraintlayout.compose.VerticalChainScope constrain(androidx.constraintlayout.compose.VerticalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.VerticalChainScope,kotlin.Unit> constrainBlock);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteLeftBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteRightBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createBottomBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference[] elements, optional float spacing, optional float[] weights);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createEndBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference?[] elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float padding, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference?[] elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingHorizontal, optional float paddingVertical, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference?[] elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference[] elements, @IntRange(from=1L) int rows, @IntRange(from=1L) int columns, optional boolean isHorizontalArrangement, optional float verticalSpacing, optional float horizontalSpacing, optional float[] rowWeights, optional float[] columnWeights, optional androidx.constraintlayout.compose.Skip[] skips, optional androidx.constraintlayout.compose.Span[] spans, optional int flags);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromBottom(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromBottom(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromEnd(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromEnd(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromStart(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromStart(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float fraction);
+ method public final androidx.constraintlayout.compose.HorizontalChainReference createHorizontalChain(androidx.constraintlayout.compose.LayoutReference[] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference[] elements, optional float spacing, optional float[] weights);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createStartBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createTopBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference[] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+ method @Deprecated protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> getTasks();
+ method public void reset();
+ method public final androidx.constraintlayout.compose.LayoutReference withChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional float weight);
+ method public final androidx.constraintlayout.compose.LayoutReference withHorizontalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional float weight);
+ method public final androidx.constraintlayout.compose.LayoutReference withVerticalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional float weight);
+ property @Deprecated protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> tasks;
+ }
+
+ @androidx.compose.runtime.Stable public static final class ConstraintLayoutBaseScope.BaselineAnchor {
+ method public androidx.constraintlayout.compose.LayoutReference component2();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor copy(Object id, androidx.constraintlayout.compose.LayoutReference reference);
+ method public androidx.constraintlayout.compose.LayoutReference getReference();
+ property public final androidx.constraintlayout.compose.LayoutReference reference;
+ }
+
+ @androidx.compose.runtime.Stable public static final class ConstraintLayoutBaseScope.HorizontalAnchor {
+ method public androidx.constraintlayout.compose.LayoutReference component3();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor copy(Object id, int index, androidx.constraintlayout.compose.LayoutReference reference);
+ method public androidx.constraintlayout.compose.LayoutReference getReference();
+ property public final androidx.constraintlayout.compose.LayoutReference reference;
+ }
+
+ @androidx.compose.runtime.Stable public static final class ConstraintLayoutBaseScope.VerticalAnchor {
+ method public androidx.constraintlayout.compose.LayoutReference component3();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor copy(Object id, int index, androidx.constraintlayout.compose.LayoutReference reference);
+ method public androidx.constraintlayout.compose.LayoutReference getReference();
+ property public final androidx.constraintlayout.compose.LayoutReference reference;
+ }
+
+ public final class ConstraintLayoutKt {
+ method @androidx.compose.runtime.Composable public static inline void ConstraintLayout(optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? animateChangesSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintLayoutScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline void ConstraintLayout(optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional boolean animateChanges, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void ConstraintLayout(androidx.constraintlayout.compose.ConstraintSet constraintSet, optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? animateChangesSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline void ConstraintLayout(androidx.constraintlayout.compose.ConstraintSet constraintSet, optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional boolean animateChanges, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(androidx.constraintlayout.compose.ConstraintSet extendConstraintSet, @org.intellij.lang.annotations.Language("json5") String jsonContent);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(androidx.constraintlayout.compose.ConstraintSet extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> description);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(@org.intellij.lang.annotations.Language("json5") String jsonContent);
+ method @androidx.compose.runtime.Composable public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(@org.intellij.lang.annotations.Language("json5") String content, optional @org.intellij.lang.annotations.Language("json5") String? overrideVariables);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> description);
+ method public static androidx.constraintlayout.compose.Dimension.MaxCoercible atLeast(androidx.constraintlayout.compose.Dimension.Coercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension atLeast(androidx.constraintlayout.compose.Dimension.MinCoercible, float dp);
+ method @Deprecated public static androidx.constraintlayout.compose.Dimension atLeastWrapContent(androidx.constraintlayout.compose.Dimension.MinCoercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension.MinCoercible atMost(androidx.constraintlayout.compose.Dimension.Coercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension atMost(androidx.constraintlayout.compose.Dimension.MaxCoercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension.MaxCoercible getAtLeastWrapContent(androidx.constraintlayout.compose.Dimension.Coercible);
+ method public static androidx.constraintlayout.compose.Dimension getAtLeastWrapContent(androidx.constraintlayout.compose.Dimension.MinCoercible);
+ method public static androidx.constraintlayout.compose.Dimension.MinCoercible getAtMostWrapContent(androidx.constraintlayout.compose.Dimension.Coercible);
+ method public static androidx.constraintlayout.compose.Dimension getAtMostWrapContent(androidx.constraintlayout.compose.Dimension.MaxCoercible);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintLayoutScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier constrainAs(androidx.compose.ui.Modifier, androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRef();
+ method @androidx.compose.runtime.Stable public androidx.constraintlayout.compose.ConstraintLayoutScope.ConstrainedLayoutReferences createRefs();
+ }
+
+ public final class ConstraintLayoutScope.ConstrainedLayoutReferences {
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
+ }
+
+ public final class ConstraintLayoutTagKt {
+ method public static Object? getConstraintLayoutId(androidx.compose.ui.layout.Measurable);
+ method public static Object? getConstraintLayoutTag(androidx.compose.ui.layout.Measurable);
+ method public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, String layoutId, optional String? tag);
+ }
+
+ public interface ConstraintLayoutTagParentData {
+ method public String getConstraintLayoutId();
+ method public String getConstraintLayoutTag();
+ property public abstract String constraintLayoutId;
+ property public abstract String constraintLayoutTag;
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
+ method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
+ method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ method public default androidx.constraintlayout.compose.ConstraintSet override(String name, float value);
+ }
+
+ public final class ConstraintSetRef {
+ method public androidx.constraintlayout.compose.ConstraintSetRef copy(String name);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintSetScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+ method public androidx.constraintlayout.compose.ConstraintSetScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+ }
+
+ public final class ConstraintSetScope.ConstrainedLayoutReferences {
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class CurveFit {
+ method public String getName();
+ property public String name;
+ field public static final androidx.constraintlayout.compose.CurveFit.Companion Companion;
+ }
+
+ public static final class CurveFit.Companion {
+ method public androidx.constraintlayout.compose.CurveFit getLinear();
+ method public androidx.constraintlayout.compose.CurveFit getSpline();
+ property public final androidx.constraintlayout.compose.CurveFit Linear;
+ property public final androidx.constraintlayout.compose.CurveFit Spline;
+ }
+
+ @kotlin.jvm.JvmInline public final value class DebugFlags {
+ ctor public DebugFlags(optional boolean showBounds, optional boolean showPaths, optional boolean showKeyPositions);
+ method public boolean getShowBounds();
+ method public boolean getShowKeyPositions();
+ method public boolean getShowPaths();
+ property public final boolean showBounds;
+ property public final boolean showKeyPositions;
+ property public final boolean showPaths;
+ field public static final androidx.constraintlayout.compose.DebugFlags.Companion Companion;
+ }
+
+ public static final class DebugFlags.Companion {
+ method public int getAll();
+ method public int getNone();
+ property public final int All;
+ property public final int None;
+ }
+
+ public final class DesignElements {
+ method public void define(String name, kotlin.jvm.functions.Function2<? super java.lang.String,? super java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit> function);
+ method public java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>> getMap();
+ method public void setMap(java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>>);
+ property public final java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>> map;
+ field public static final androidx.constraintlayout.compose.DesignElements INSTANCE;
+ }
+
+ public interface DesignInfoProvider {
+ method public String getDesignInfo(int startX, int startY, String args);
+ }
+
+ public interface Dimension {
+ field public static final androidx.constraintlayout.compose.Dimension.Companion Companion;
+ }
+
+ public static interface Dimension.Coercible extends androidx.constraintlayout.compose.Dimension {
+ }
+
+ public static final class Dimension.Companion {
+ method public androidx.constraintlayout.compose.Dimension.Coercible getFillToConstraints();
+ method public androidx.constraintlayout.compose.Dimension getMatchParent();
+ method public androidx.constraintlayout.compose.Dimension.Coercible getPreferredWrapContent();
+ method public androidx.constraintlayout.compose.Dimension getWrapContent();
+ method public androidx.constraintlayout.compose.Dimension percent(float percent);
+ method public androidx.constraintlayout.compose.Dimension.MinCoercible preferredValue(float dp);
+ method public androidx.constraintlayout.compose.Dimension ratio(String ratio);
+ method public androidx.constraintlayout.compose.Dimension value(float dp);
+ property public final androidx.constraintlayout.compose.Dimension.Coercible fillToConstraints;
+ property public final androidx.constraintlayout.compose.Dimension matchParent;
+ property public final androidx.constraintlayout.compose.Dimension.Coercible preferredWrapContent;
+ property public final androidx.constraintlayout.compose.Dimension wrapContent;
+ }
+
+ public static interface Dimension.MaxCoercible extends androidx.constraintlayout.compose.Dimension {
+ }
+
+ public static interface Dimension.MinCoercible extends androidx.constraintlayout.compose.Dimension {
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class Easing {
+ method public String getName();
+ property public String name;
+ field public static final androidx.constraintlayout.compose.Easing.Companion Companion;
+ }
+
+ public static final class Easing.Companion {
+ method public androidx.constraintlayout.compose.Easing cubic(float x1, float y1, float x2, float y2);
+ method public androidx.constraintlayout.compose.Easing getAccelerate();
+ method public androidx.constraintlayout.compose.Easing getAnticipate();
+ method public androidx.constraintlayout.compose.Easing getDecelerate();
+ method public androidx.constraintlayout.compose.Easing getLinear();
+ method public androidx.constraintlayout.compose.Easing getOvershoot();
+ method public androidx.constraintlayout.compose.Easing getStandard();
+ property public final androidx.constraintlayout.compose.Easing Accelerate;
+ property public final androidx.constraintlayout.compose.Easing Anticipate;
+ property public final androidx.constraintlayout.compose.Easing Decelerate;
+ property public final androidx.constraintlayout.compose.Easing Linear;
+ property public final androidx.constraintlayout.compose.Easing Overshoot;
+ property public final androidx.constraintlayout.compose.Easing Standard;
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="MotionLayout API is experimental and it is likely to change.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMotionApi {
+ }
+
+ @androidx.compose.runtime.Immutable public final class FlowStyle {
+ field public static final androidx.constraintlayout.compose.FlowStyle.Companion Companion;
+ }
+
+ public static final class FlowStyle.Companion {
+ method public androidx.constraintlayout.compose.FlowStyle getPacked();
+ method public androidx.constraintlayout.compose.FlowStyle getSpread();
+ method public androidx.constraintlayout.compose.FlowStyle getSpreadInside();
+ property public final androidx.constraintlayout.compose.FlowStyle Packed;
+ property public final androidx.constraintlayout.compose.FlowStyle Spread;
+ property public final androidx.constraintlayout.compose.FlowStyle SpreadInside;
+ }
+
+ @kotlin.jvm.JvmInline public final value class GridFlag {
+ method public boolean isPlaceLayoutsOnSpansFirst();
+ method public infix int or(int other);
+ property public final boolean isPlaceLayoutsOnSpansFirst;
+ field public static final androidx.constraintlayout.compose.GridFlag.Companion Companion;
+ }
+
+ public static final class GridFlag.Companion {
+ method public int getNone();
+ method public int getPlaceLayoutsOnSpansFirst();
+ property public final int None;
+ property public final int PlaceLayoutsOnSpansFirst;
+ }
+
+ @androidx.compose.runtime.Immutable public final class HorizontalAlign {
+ field public static final androidx.constraintlayout.compose.HorizontalAlign.Companion Companion;
+ }
+
+ public static final class HorizontalAlign.Companion {
+ method public androidx.constraintlayout.compose.HorizontalAlign getCenter();
+ method public androidx.constraintlayout.compose.HorizontalAlign getEnd();
+ method public androidx.constraintlayout.compose.HorizontalAlign getStart();
+ property public final androidx.constraintlayout.compose.HorizontalAlign Center;
+ property public final androidx.constraintlayout.compose.HorizontalAlign End;
+ property public final androidx.constraintlayout.compose.HorizontalAlign Start;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+ }
+
+ @androidx.compose.runtime.Stable public final class HorizontalChainReference extends androidx.constraintlayout.compose.LayoutReference {
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteRight();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getEnd();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getStart();
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteLeft;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteRight;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start;
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class HorizontalChainScope {
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getStart();
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteLeft;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteRight;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable end;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable start;
+ }
+
+ public final class InvalidationStrategy {
+ ctor public InvalidationStrategy(optional kotlin.jvm.functions.Function3<? super androidx.constraintlayout.compose.InvalidationStrategySpecification,? super androidx.compose.ui.unit.Constraints,? super androidx.compose.ui.unit.Constraints,java.lang.Boolean>? onIncomingConstraints, kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange);
+ method public kotlin.jvm.functions.Function3<androidx.constraintlayout.compose.InvalidationStrategySpecification,androidx.compose.ui.unit.Constraints,androidx.compose.ui.unit.Constraints,java.lang.Boolean>? getOnIncomingConstraints();
+ method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnObservedStateChange();
+ property public final kotlin.jvm.functions.Function3<androidx.constraintlayout.compose.InvalidationStrategySpecification,androidx.compose.ui.unit.Constraints,androidx.compose.ui.unit.Constraints,java.lang.Boolean>? onIncomingConstraints;
+ property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange;
+ field public static final androidx.constraintlayout.compose.InvalidationStrategy.Companion Companion;
+ }
+
+ public static final class InvalidationStrategy.Companion {
+ method public androidx.constraintlayout.compose.InvalidationStrategy getDefaultInvalidationStrategy();
+ property public final androidx.constraintlayout.compose.InvalidationStrategy DefaultInvalidationStrategy;
+ }
+
+ public final class InvalidationStrategySpecification {
+ method public boolean shouldInvalidateOnFixedHeight(long oldConstraints, long newConstraints, int skipCount, int threshold);
+ method public boolean shouldInvalidateOnFixedWidth(long oldConstraints, long newConstraints, int skipCount, int threshold);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyAttributeScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
+ method public float getAlpha();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public void setAlpha(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ property public final float alpha;
+ property public final float rotationX;
+ property public final float rotationY;
+ property public final float rotationZ;
+ property public final float scaleX;
+ property public final float scaleY;
+ property public final float translationX;
+ property public final float translationY;
+ property public final float translationZ;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyAttributesScope extends androidx.constraintlayout.compose.BaseKeyFramesScope {
+ method public void frame(@IntRange(from=0L, to=100L) int frame, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyAttributeScope,kotlin.Unit> keyFrameContent);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyCycleScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
+ method public float getAlpha();
+ method public float getOffset();
+ method public float getPeriod();
+ method public float getPhase();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public void setAlpha(float);
+ method public void setOffset(float);
+ method public void setPeriod(float);
+ method public void setPhase(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ property public final float alpha;
+ property public final float offset;
+ property public final float period;
+ property public final float phase;
+ property public final float rotationX;
+ property public final float rotationY;
+ property public final float rotationZ;
+ property public final float scaleX;
+ property public final float scaleY;
+ property public final float translationX;
+ property public final float translationY;
+ property public final float translationZ;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyCyclesScope extends androidx.constraintlayout.compose.BaseKeyFramesScope {
+ method public void frame(@IntRange(from=0L, to=100L) int frame, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyCycleScope,kotlin.Unit> keyFrameContent);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyPositionScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
+ method public androidx.constraintlayout.compose.CurveFit? getCurveFit();
+ method public float getPercentHeight();
+ method public float getPercentWidth();
+ method public float getPercentX();
+ method public float getPercentY();
+ method public void setCurveFit(androidx.constraintlayout.compose.CurveFit?);
+ method public void setPercentHeight(float);
+ method public void setPercentWidth(float);
+ method public void setPercentX(float);
+ method public void setPercentY(float);
+ property public final androidx.constraintlayout.compose.CurveFit? curveFit;
+ property public final float percentHeight;
+ property public final float percentWidth;
+ property public final float percentX;
+ property public final float percentY;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyPositionsScope extends androidx.constraintlayout.compose.BaseKeyFramesScope {
+ method public void frame(@IntRange(from=0L, to=100L) int frame, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyPositionScope,kotlin.Unit> keyFrameContent);
+ method public androidx.constraintlayout.compose.RelativePosition getType();
+ method public void setType(androidx.constraintlayout.compose.RelativePosition);
+ property public final androidx.constraintlayout.compose.RelativePosition type;
+ }
+
+ public enum LayoutInfoFlags {
+ enum_constant public static final androidx.constraintlayout.compose.LayoutInfoFlags BOUNDS;
+ enum_constant public static final androidx.constraintlayout.compose.LayoutInfoFlags NONE;
+ }
+
+ public interface LayoutInformationReceiver {
+ method public androidx.constraintlayout.compose.MotionLayoutDebugFlags getForcedDrawDebug();
+ method public int getForcedHeight();
+ method public float getForcedProgress();
+ method public int getForcedWidth();
+ method public androidx.constraintlayout.compose.LayoutInfoFlags getLayoutInformationMode();
+ method public void onNewProgress(float progress);
+ method public void resetForcedProgress();
+ method public void setLayoutInformation(String information);
+ method public void setUpdateFlag(androidx.compose.runtime.MutableState<java.lang.Long> needsUpdate);
+ }
+
+ @androidx.compose.runtime.Stable public abstract class LayoutReference {
+ }
+
+ public final class MotionCarouselKt {
+ method @androidx.compose.runtime.Composable public static void ItemHolder(int i, String slotPrefix, boolean showSlot, kotlin.jvm.functions.Function0<kotlin.Unit> function);
+ method @androidx.compose.runtime.Composable public static void MotionCarousel(androidx.constraintlayout.compose.MotionScene motionScene, int initialSlotIndex, int numSlots, optional String backwardTransition, optional String forwardTransition, optional String slotPrefix, optional boolean showSlots, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionCarouselScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.constraintlayout.compose.MotionCarouselScope, java.util.List<? extends T> items, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsWithProperties(androidx.constraintlayout.compose.MotionCarouselScope, java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super T,? super androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties>,kotlin.Unit> itemContent);
+ }
+
+ public interface MotionCarouselScope {
+ method public void items(int count, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> itemContent);
+ method public void itemsWithProperties(int count, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties>,kotlin.Unit> itemContent);
+ }
+
+ public interface MotionItemsProvider {
+ method public int count();
+ method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
+ method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index, androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties> properties);
+ method public boolean hasItemsWithProperties();
+ }
+
+ public enum MotionLayoutDebugFlags {
+ enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags NONE;
+ enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags SHOW_ALL;
+ enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags UNKNOWN;
+ }
+
+ @Deprecated public enum MotionLayoutFlag {
+ enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
+ enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
+ }
+
+ public final class MotionLayoutKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, float progress, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionLayoutScope {
+ method public long customColor(String id, String name);
+ method public float customDistance(String id, String name);
+ method public float customFloat(String id, String name);
+ method public long customFontSize(String id, String name);
+ method public int customInt(String id, String name);
+ method public androidx.constraintlayout.compose.MotionLayoutScope.CustomProperties customProperties(String id);
+ method @Deprecated public long motionColor(String id, String name);
+ method @Deprecated public float motionDistance(String id, String name);
+ method @Deprecated public float motionFloat(String id, String name);
+ method @Deprecated public long motionFontSize(String id, String name);
+ method @Deprecated public int motionInt(String id, String name);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties> motionProperties(String id);
+ method @Deprecated public androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties motionProperties(String id, String tag);
+ method public androidx.compose.ui.Modifier onStartEndBoundsChanged(androidx.compose.ui.Modifier, Object layoutId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.geometry.Rect,? super androidx.compose.ui.geometry.Rect,kotlin.Unit> onBoundsChanged);
+ }
+
+ public final class MotionLayoutScope.CustomProperties {
+ method public long color(String name);
+ method public float distance(String name);
+ method public float float(String name);
+ method public long fontSize(String name);
+ method public int int(String name);
+ }
+
+ public final class MotionLayoutScope.MotionProperties {
+ method public long color(String name);
+ method public float distance(String name);
+ method public float float(String name);
+ method public long fontSize(String name);
+ method public String id();
+ method public int int(String name);
+ method public String? tag();
+ }
+
+ @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.constraintlayout.compose.ExperimentalMotionApi public interface MotionScene extends androidx.constraintlayout.core.state.CoreMotionScene {
+ method public androidx.constraintlayout.compose.ConstraintSet? getConstraintSetInstance(String name);
+ method public androidx.constraintlayout.compose.Transition? getTransitionInstance(String name);
+ }
+
+ public final class MotionSceneKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(@org.intellij.lang.annotations.Language("json5") String content);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionSceneScope {
+ method public androidx.constraintlayout.compose.ConstraintSetRef addConstraintSet(androidx.constraintlayout.compose.ConstraintSet constraintSet, optional String? name);
+ method public void addTransition(androidx.constraintlayout.compose.Transition transition, optional String? name);
+ method public androidx.constraintlayout.compose.ConstraintSetRef constraintSet(optional String? name, optional androidx.constraintlayout.compose.ConstraintSetRef? extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> constraintSetContent);
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+ method public androidx.constraintlayout.compose.MotionSceneScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+ method public void customColor(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+ method public void customColor(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+ method public void customDistance(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+ method public void customDistance(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
+ method public void customFloat(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+ method public void customFloat(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
+ method public void customFontSize(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+ method public void customFontSize(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+ method public void customInt(androidx.constraintlayout.compose.ConstrainScope, String name, int value);
+ method public void customInt(androidx.constraintlayout.compose.KeyAttributeScope, String name, int value);
+ method public void defaultTransition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
+ method public float getStaggeredWeight(androidx.constraintlayout.compose.ConstrainScope);
+ method public void setStaggeredWeight(androidx.constraintlayout.compose.ConstrainScope, float);
+ method public void transition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional String? name, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
+ }
+
+ public final class MotionSceneScope.ConstrainedLayoutReferences {
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
+ }
+
+ public final class MotionSceneScopeKt {
+ method @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionSceneScope,kotlin.Unit> motionSceneContent);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class OnSwipe {
+ ctor public OnSwipe(androidx.constraintlayout.compose.ConstrainedLayoutReference anchor, androidx.constraintlayout.compose.SwipeSide side, androidx.constraintlayout.compose.SwipeDirection direction, optional float dragScale, optional float dragThreshold, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo, optional androidx.constraintlayout.compose.SwipeTouchUp onTouchUp, optional androidx.constraintlayout.compose.SwipeMode mode);
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getAnchor();
+ method public androidx.constraintlayout.compose.SwipeDirection getDirection();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference? getDragAround();
+ method public float getDragScale();
+ method public float getDragThreshold();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference? getLimitBoundsTo();
+ method public androidx.constraintlayout.compose.SwipeMode getMode();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getOnTouchUp();
+ method public androidx.constraintlayout.compose.SwipeSide getSide();
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference anchor;
+ property public final androidx.constraintlayout.compose.SwipeDirection direction;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround;
+ property public final float dragScale;
+ property public final float dragThreshold;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo;
+ property public final androidx.constraintlayout.compose.SwipeMode mode;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp onTouchUp;
+ property public final androidx.constraintlayout.compose.SwipeSide side;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class RelativePosition {
+ method public String getName();
+ property public String name;
+ field public static final androidx.constraintlayout.compose.RelativePosition.Companion Companion;
+ }
+
+ public static final class RelativePosition.Companion {
+ method public androidx.constraintlayout.compose.RelativePosition getDelta();
+ method public androidx.constraintlayout.compose.RelativePosition getParent();
+ method public androidx.constraintlayout.compose.RelativePosition getPath();
+ property public final androidx.constraintlayout.compose.RelativePosition Delta;
+ property public final androidx.constraintlayout.compose.RelativePosition Parent;
+ property public final androidx.constraintlayout.compose.RelativePosition Path;
+ }
+
+ @kotlin.jvm.JvmInline public final value class Skip {
+ ctor public Skip(@IntRange(from=0L) int position, @IntRange(from=1L) int size);
+ ctor public Skip(@IntRange(from=0L) int position, @IntRange(from=1L) int rows, @IntRange(from=1L) int columns);
+ method public String getDescription();
+ property public final String description;
+ }
+
+ @kotlin.jvm.JvmInline public final value class Span {
+ ctor public Span(@IntRange(from=0L) int position, @IntRange(from=1L) int size);
+ ctor public Span(@IntRange(from=0L) int position, @IntRange(from=1L) int rows, @IntRange(from=1L) int columns);
+ ctor public Span(String description);
+ method public String getDescription();
+ property public final String description;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SpringBoundary {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SpringBoundary.Companion Companion;
+ }
+
+ public static final class SpringBoundary.Companion {
+ method public androidx.constraintlayout.compose.SpringBoundary getBounceBoth();
+ method public androidx.constraintlayout.compose.SpringBoundary getBounceEnd();
+ method public androidx.constraintlayout.compose.SpringBoundary getBounceStart();
+ method public androidx.constraintlayout.compose.SpringBoundary getOvershoot();
+ property public final androidx.constraintlayout.compose.SpringBoundary BounceBoth;
+ property public final androidx.constraintlayout.compose.SpringBoundary BounceEnd;
+ property public final androidx.constraintlayout.compose.SpringBoundary BounceStart;
+ property public final androidx.constraintlayout.compose.SpringBoundary Overshoot;
+ }
+
+ public final class State extends androidx.constraintlayout.core.state.State {
+ ctor public State(androidx.compose.ui.unit.Density density);
+ method public androidx.compose.ui.unit.Density getDensity();
+ method @Deprecated public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
+ method public long getRootIncomingConstraints();
+ method @Deprecated public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection);
+ method public void setRootIncomingConstraints(long);
+ property public final androidx.compose.ui.unit.Density density;
+ property @Deprecated public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
+ property public final long rootIncomingConstraints;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeDirection {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeDirection.Companion Companion;
+ }
+
+ public static final class SwipeDirection.Companion {
+ method public androidx.constraintlayout.compose.SwipeDirection getClockwise();
+ method public androidx.constraintlayout.compose.SwipeDirection getCounterclockwise();
+ method public androidx.constraintlayout.compose.SwipeDirection getDown();
+ method public androidx.constraintlayout.compose.SwipeDirection getEnd();
+ method public androidx.constraintlayout.compose.SwipeDirection getLeft();
+ method public androidx.constraintlayout.compose.SwipeDirection getRight();
+ method public androidx.constraintlayout.compose.SwipeDirection getStart();
+ method public androidx.constraintlayout.compose.SwipeDirection getUp();
+ property public final androidx.constraintlayout.compose.SwipeDirection Clockwise;
+ property public final androidx.constraintlayout.compose.SwipeDirection Counterclockwise;
+ property public final androidx.constraintlayout.compose.SwipeDirection Down;
+ property public final androidx.constraintlayout.compose.SwipeDirection End;
+ property public final androidx.constraintlayout.compose.SwipeDirection Left;
+ property public final androidx.constraintlayout.compose.SwipeDirection Right;
+ property public final androidx.constraintlayout.compose.SwipeDirection Start;
+ property public final androidx.constraintlayout.compose.SwipeDirection Up;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeMode {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeMode.Companion Companion;
+ }
+
+ public static final class SwipeMode.Companion {
+ method public androidx.constraintlayout.compose.SwipeMode getSpring();
+ method public androidx.constraintlayout.compose.SwipeMode getVelocity();
+ method public androidx.constraintlayout.compose.SwipeMode spring(optional float mass, optional float stiffness, optional float damping, optional float threshold, optional androidx.constraintlayout.compose.SpringBoundary boundary);
+ method public androidx.constraintlayout.compose.SwipeMode velocity(optional float maxVelocity, optional float maxAcceleration);
+ property public final androidx.constraintlayout.compose.SwipeMode Spring;
+ property public final androidx.constraintlayout.compose.SwipeMode Velocity;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeSide {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeSide.Companion Companion;
+ }
+
+ public static final class SwipeSide.Companion {
+ method public androidx.constraintlayout.compose.SwipeSide getBottom();
+ method public androidx.constraintlayout.compose.SwipeSide getEnd();
+ method public androidx.constraintlayout.compose.SwipeSide getLeft();
+ method public androidx.constraintlayout.compose.SwipeSide getMiddle();
+ method public androidx.constraintlayout.compose.SwipeSide getRight();
+ method public androidx.constraintlayout.compose.SwipeSide getStart();
+ method public androidx.constraintlayout.compose.SwipeSide getTop();
+ property public final androidx.constraintlayout.compose.SwipeSide Bottom;
+ property public final androidx.constraintlayout.compose.SwipeSide End;
+ property public final androidx.constraintlayout.compose.SwipeSide Left;
+ property public final androidx.constraintlayout.compose.SwipeSide Middle;
+ property public final androidx.constraintlayout.compose.SwipeSide Right;
+ property public final androidx.constraintlayout.compose.SwipeSide Start;
+ property public final androidx.constraintlayout.compose.SwipeSide Top;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeTouchUp {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeTouchUp.Companion Companion;
+ }
+
+ public static final class SwipeTouchUp.Companion {
+ method public androidx.constraintlayout.compose.SwipeTouchUp getAutoComplete();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getDecelerate();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getNeverCompleteEnd();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getNeverCompleteStart();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getStop();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getToEnd();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getToStart();
+ property public final androidx.constraintlayout.compose.SwipeTouchUp AutoComplete;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp Decelerate;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp NeverCompleteEnd;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp NeverCompleteStart;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp Stop;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp ToEnd;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp ToStart;
+ }
+
+ public final class ToolingUtilsKt {
+ method public static androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.constraintlayout.compose.DesignInfoProvider> getDesignInfoDataKey();
+ property public static final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.constraintlayout.compose.DesignInfoProvider> DesignInfoDataKey;
+ }
+
+ @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.constraintlayout.compose.ExperimentalMotionApi public interface Transition {
+ method public String getEndConstraintSetId();
+ method public String getStartConstraintSetId();
+ }
+
+ public final class TransitionKt {
+ method @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.Transition Transition(@org.intellij.lang.annotations.Language("json5") String content);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class TransitionScope {
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+ method public float getMaxStaggerDelay();
+ method public androidx.constraintlayout.compose.Arc getMotionArc();
+ method public androidx.constraintlayout.compose.OnSwipe? getOnSwipe();
+ method public void keyAttributes(androidx.constraintlayout.compose.ConstrainedLayoutReference[] targets, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyAttributesScope,kotlin.Unit> keyAttributesContent);
+ method public void keyCycles(androidx.constraintlayout.compose.ConstrainedLayoutReference[] targets, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyCyclesScope,kotlin.Unit> keyCyclesContent);
+ method public void keyPositions(androidx.constraintlayout.compose.ConstrainedLayoutReference[] targets, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyPositionsScope,kotlin.Unit> keyPositionsContent);
+ method public void setMaxStaggerDelay(float);
+ method public void setMotionArc(androidx.constraintlayout.compose.Arc);
+ method public void setOnSwipe(androidx.constraintlayout.compose.OnSwipe?);
+ property public final float maxStaggerDelay;
+ property public final androidx.constraintlayout.compose.Arc motionArc;
+ property public final androidx.constraintlayout.compose.OnSwipe? onSwipe;
+ }
+
+ public final class TransitionScopeKt {
+ method @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.Transition Transition(optional String from, optional String to, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> content);
+ }
+
+ @androidx.compose.runtime.Immutable public final class VerticalAlign {
+ field public static final androidx.constraintlayout.compose.VerticalAlign.Companion Companion;
+ }
+
+ public static final class VerticalAlign.Companion {
+ method public androidx.constraintlayout.compose.VerticalAlign getBaseline();
+ method public androidx.constraintlayout.compose.VerticalAlign getBottom();
+ method public androidx.constraintlayout.compose.VerticalAlign getCenter();
+ method public androidx.constraintlayout.compose.VerticalAlign getTop();
+ property public final androidx.constraintlayout.compose.VerticalAlign Baseline;
+ property public final androidx.constraintlayout.compose.VerticalAlign Bottom;
+ property public final androidx.constraintlayout.compose.VerticalAlign Center;
+ property public final androidx.constraintlayout.compose.VerticalAlign Top;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
+ }
+
+ @androidx.compose.runtime.Stable public final class VerticalChainReference extends androidx.constraintlayout.compose.LayoutReference {
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getBottom();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getTop();
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top;
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class VerticalChainScope {
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getTop();
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable top;
+ }
+
+ @androidx.compose.runtime.Immutable public final class Visibility {
+ field public static final androidx.constraintlayout.compose.Visibility.Companion Companion;
+ }
+
+ public static final class Visibility.Companion {
+ method public androidx.constraintlayout.compose.Visibility getGone();
+ method public androidx.constraintlayout.compose.Visibility getInvisible();
+ method public androidx.constraintlayout.compose.Visibility getVisible();
+ property public final androidx.constraintlayout.compose.Visibility Gone;
+ property public final androidx.constraintlayout.compose.Visibility Invisible;
+ property public final androidx.constraintlayout.compose.Visibility Visible;
+ }
+
+ @androidx.compose.runtime.Immutable public final class Wrap {
+ field public static final androidx.constraintlayout.compose.Wrap.Companion Companion;
+ }
+
+ public static final class Wrap.Companion {
+ method public androidx.constraintlayout.compose.Wrap getAligned();
+ method public androidx.constraintlayout.compose.Wrap getChain();
+ method public androidx.constraintlayout.compose.Wrap getNone();
+ property public final androidx.constraintlayout.compose.Wrap Aligned;
+ property public final androidx.constraintlayout.compose.Wrap Chain;
+ property public final androidx.constraintlayout.compose.Wrap None;
+ }
+
+}
+
diff --git a/activity/activity-compose/api/res-1.10.0-beta01.txt b/constraintlayout/constraintlayout-compose/api/res-1.1.0-beta01.txt
similarity index 100%
rename from activity/activity-compose/api/res-1.10.0-beta01.txt
rename to constraintlayout/constraintlayout-compose/api/res-1.1.0-beta01.txt
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_1.1.0-beta01.txt b/constraintlayout/constraintlayout-compose/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..f2836bb
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,1103 @@
+// Signature format: 4.0
+package androidx.constraintlayout.compose {
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class Arc {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.Arc.Companion Companion;
+ }
+
+ public static final class Arc.Companion {
+ method public androidx.constraintlayout.compose.Arc getAbove();
+ method public androidx.constraintlayout.compose.Arc getBelow();
+ method public androidx.constraintlayout.compose.Arc getFlip();
+ method public androidx.constraintlayout.compose.Arc getNone();
+ method public androidx.constraintlayout.compose.Arc getStartHorizontal();
+ method public androidx.constraintlayout.compose.Arc getStartVertical();
+ property public final androidx.constraintlayout.compose.Arc Above;
+ property public final androidx.constraintlayout.compose.Arc Below;
+ property public final androidx.constraintlayout.compose.Arc Flip;
+ property public final androidx.constraintlayout.compose.Arc None;
+ property public final androidx.constraintlayout.compose.Arc StartHorizontal;
+ property public final androidx.constraintlayout.compose.Arc StartVertical;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public abstract sealed class BaseKeyFrameScope {
+ method protected final <E extends androidx.constraintlayout.compose.NamedPropertyOrValue> kotlin.properties.ObservableProperty<E> addNameOnPropertyChange(E initialValue, optional String? nameOverride);
+ method protected final <T> kotlin.properties.ObservableProperty<T> addOnPropertyChange(T initialValue, optional String? nameOverride);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public abstract sealed class BaseKeyFramesScope {
+ method public final androidx.constraintlayout.compose.Easing getEasing();
+ method public final void setEasing(androidx.constraintlayout.compose.Easing);
+ property public final androidx.constraintlayout.compose.Easing easing;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+ }
+
+ @androidx.compose.runtime.Immutable public final class ChainStyle {
+ field public static final androidx.constraintlayout.compose.ChainStyle.Companion Companion;
+ }
+
+ public static final class ChainStyle.Companion {
+ method @androidx.compose.runtime.Stable public androidx.constraintlayout.compose.ChainStyle Packed(float bias);
+ method public androidx.constraintlayout.compose.ChainStyle getPacked();
+ method public androidx.constraintlayout.compose.ChainStyle getSpread();
+ method public androidx.constraintlayout.compose.ChainStyle getSpreadInside();
+ property public final androidx.constraintlayout.compose.ChainStyle Packed;
+ property public final androidx.constraintlayout.compose.ChainStyle Spread;
+ property public final androidx.constraintlayout.compose.ChainStyle SpreadInside;
+ }
+
+ @kotlin.PublishedApi internal enum CompositionSource {
+ enum_constant public static final androidx.constraintlayout.compose.CompositionSource Content;
+ enum_constant public static final androidx.constraintlayout.compose.CompositionSource Unknown;
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class ConstrainScope {
+ method public androidx.constraintlayout.compose.Dimension asDimension(float);
+ method public void centerAround(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor);
+ method public void centerAround(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor);
+ method public void centerHorizontallyTo(androidx.constraintlayout.compose.ConstrainedLayoutReference other, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void centerTo(androidx.constraintlayout.compose.ConstrainedLayoutReference other);
+ method public void centerVerticallyTo(androidx.constraintlayout.compose.ConstrainedLayoutReference other, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void circular(androidx.constraintlayout.compose.ConstrainedLayoutReference other, float angle, float distance);
+ method public void clearConstraints();
+ method public void clearHorizontal();
+ method public void clearVertical();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
+ method public float getAlpha();
+ method public androidx.constraintlayout.compose.BaselineAnchorable getBaseline();
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
+ method public androidx.constraintlayout.compose.Dimension getHeight();
+ method public float getHorizontalBias();
+ method public float getHorizontalChainWeight();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getStart();
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getTop();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public float getVerticalBias();
+ method public float getVerticalChainWeight();
+ method public androidx.constraintlayout.compose.Visibility getVisibility();
+ method public androidx.constraintlayout.compose.Dimension getWidth();
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional @FloatRange(from=0.0, to=1.0) float horizontalBias, optional @FloatRange(from=0.0, to=1.0) float verticalBias);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start, androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional @FloatRange(from=0.0, to=1.0) float bias);
+ method public void resetDimensions();
+ method public void resetTransforms();
+ method public void setAlpha(float);
+ method public void setHeight(androidx.constraintlayout.compose.Dimension);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalChainWeight(float);
+ method public void setPivotX(float);
+ method public void setPivotY(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ method public void setVerticalBias(float);
+ method public void setVerticalChainWeight(float);
+ method public void setVisibility(androidx.constraintlayout.compose.Visibility);
+ method public void setWidth(androidx.constraintlayout.compose.Dimension);
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteLeft;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteRight;
+ property public final float alpha;
+ property public final androidx.constraintlayout.compose.BaselineAnchorable baseline;
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable end;
+ property public final androidx.constraintlayout.compose.Dimension height;
+ property public final float horizontalBias;
+ property public final float horizontalChainWeight;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
+ property public final float pivotX;
+ property public final float pivotY;
+ property public final float rotationX;
+ property public final float rotationY;
+ property public final float rotationZ;
+ property public final float scaleX;
+ property public final float scaleY;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable start;
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable top;
+ property public final float translationX;
+ property public final float translationY;
+ property public final float translationZ;
+ property public final float verticalBias;
+ property public final float verticalChainWeight;
+ property public final androidx.constraintlayout.compose.Visibility visibility;
+ property public final androidx.constraintlayout.compose.Dimension width;
+ }
+
+ @androidx.compose.runtime.Stable public final class ConstrainedLayoutReference extends androidx.constraintlayout.compose.LayoutReference {
+ ctor public ConstrainedLayoutReference(Object id);
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteRight();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor getBaseline();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getBottom();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getEnd();
+ method public Object getId();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getStart();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getTop();
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteLeft;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteRight;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor baseline;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end;
+ property public Object id;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top;
+ }
+
+ public abstract class ConstraintLayoutBaseScope {
+ ctor public ConstraintLayoutBaseScope();
+ method public final void applyTo(androidx.constraintlayout.compose.State state);
+ method public final androidx.constraintlayout.compose.ConstrainScope constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+ method public final void constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference[] refs, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+ method public final androidx.constraintlayout.compose.HorizontalChainScope constrain(androidx.constraintlayout.compose.HorizontalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.HorizontalChainScope,kotlin.Unit> constrainBlock);
+ method public final androidx.constraintlayout.compose.VerticalChainScope constrain(androidx.constraintlayout.compose.VerticalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.VerticalChainScope,kotlin.Unit> constrainBlock);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteLeftBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createAbsoluteRightBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createBottomBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createColumn(androidx.constraintlayout.compose.LayoutReference[] elements, optional float spacing, optional float[] weights);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createEndBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference?[] elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float padding, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference?[] elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingHorizontal, optional float paddingVertical, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createFlow(androidx.constraintlayout.compose.LayoutReference?[] elements, optional boolean flowVertically, optional float verticalGap, optional float horizontalGap, optional int maxElement, optional float paddingLeft, optional float paddingTop, optional float paddingRight, optional float paddingBottom, optional androidx.constraintlayout.compose.Wrap wrapMode, optional androidx.constraintlayout.compose.VerticalAlign verticalAlign, optional androidx.constraintlayout.compose.HorizontalAlign horizontalAlign, optional float horizontalFlowBias, optional float verticalFlowBias, optional androidx.constraintlayout.compose.FlowStyle verticalStyle, optional androidx.constraintlayout.compose.FlowStyle horizontalStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createGrid(androidx.constraintlayout.compose.LayoutReference[] elements, @IntRange(from=1L) int rows, @IntRange(from=1L) int columns, optional boolean isHorizontalArrangement, optional float verticalSpacing, optional float horizontalSpacing, optional float[] rowWeights, optional float[] columnWeights, optional androidx.constraintlayout.compose.Skip[] skips, optional androidx.constraintlayout.compose.Span[] spans, optional int flags);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteLeft(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromAbsoluteRight(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromBottom(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromBottom(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromEnd(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromEnd(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromStart(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createGuidelineFromStart(float fraction);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float offset);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createGuidelineFromTop(float fraction);
+ method public final androidx.constraintlayout.compose.HorizontalChainReference createHorizontalChain(androidx.constraintlayout.compose.LayoutReference[] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+ method public final androidx.constraintlayout.compose.ConstrainedLayoutReference createRow(androidx.constraintlayout.compose.LayoutReference[] elements, optional float spacing, optional float[] weights);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor createStartBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor createTopBarrier(androidx.constraintlayout.compose.LayoutReference[] elements, optional float margin);
+ method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference[] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
+ method @Deprecated protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> getTasks();
+ method public void reset();
+ method public final androidx.constraintlayout.compose.LayoutReference withChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional float weight);
+ method public final androidx.constraintlayout.compose.LayoutReference withHorizontalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional float weight);
+ method public final androidx.constraintlayout.compose.LayoutReference withVerticalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional float weight);
+ property @Deprecated protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> tasks;
+ field @kotlin.PublishedApi internal final androidx.constraintlayout.core.parser.CLObject containerObject;
+ field @kotlin.PublishedApi internal int helpersHashCode;
+ }
+
+ @androidx.compose.runtime.Stable public static final class ConstraintLayoutBaseScope.BaselineAnchor {
+ method public androidx.constraintlayout.compose.LayoutReference component2();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor copy(Object id, androidx.constraintlayout.compose.LayoutReference reference);
+ method public androidx.constraintlayout.compose.LayoutReference getReference();
+ property public final androidx.constraintlayout.compose.LayoutReference reference;
+ }
+
+ @androidx.compose.runtime.Stable public static final class ConstraintLayoutBaseScope.HorizontalAnchor {
+ method public androidx.constraintlayout.compose.LayoutReference component3();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor copy(Object id, int index, androidx.constraintlayout.compose.LayoutReference reference);
+ method public androidx.constraintlayout.compose.LayoutReference getReference();
+ property public final androidx.constraintlayout.compose.LayoutReference reference;
+ }
+
+ @androidx.compose.runtime.Stable public static final class ConstraintLayoutBaseScope.VerticalAnchor {
+ method public androidx.constraintlayout.compose.LayoutReference component3();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor copy(Object id, int index, androidx.constraintlayout.compose.LayoutReference reference);
+ method public androidx.constraintlayout.compose.LayoutReference getReference();
+ property public final androidx.constraintlayout.compose.LayoutReference reference;
+ }
+
+ public final class ConstraintLayoutKt {
+ method @androidx.compose.runtime.Composable public static inline void ConstraintLayout(optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? animateChangesSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintLayoutScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline void ConstraintLayout(optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional boolean animateChanges, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void ConstraintLayout(androidx.constraintlayout.compose.ConstraintSet constraintSet, optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? animateChangesSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static inline void ConstraintLayout(androidx.constraintlayout.compose.ConstraintSet constraintSet, optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, optional boolean animateChanges, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(androidx.constraintlayout.compose.ConstraintSet extendConstraintSet, @org.intellij.lang.annotations.Language("json5") String jsonContent);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(androidx.constraintlayout.compose.ConstraintSet extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> description);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(@org.intellij.lang.annotations.Language("json5") String jsonContent);
+ method @androidx.compose.runtime.Composable public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(@org.intellij.lang.annotations.Language("json5") String content, optional @org.intellij.lang.annotations.Language("json5") String? overrideVariables);
+ method public static androidx.constraintlayout.compose.ConstraintSet ConstraintSet(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> description);
+ method public static androidx.constraintlayout.compose.Dimension.MaxCoercible atLeast(androidx.constraintlayout.compose.Dimension.Coercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension atLeast(androidx.constraintlayout.compose.Dimension.MinCoercible, float dp);
+ method @Deprecated public static androidx.constraintlayout.compose.Dimension atLeastWrapContent(androidx.constraintlayout.compose.Dimension.MinCoercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension.MinCoercible atMost(androidx.constraintlayout.compose.Dimension.Coercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension atMost(androidx.constraintlayout.compose.Dimension.MaxCoercible, float dp);
+ method public static androidx.constraintlayout.compose.Dimension.MaxCoercible getAtLeastWrapContent(androidx.constraintlayout.compose.Dimension.Coercible);
+ method public static androidx.constraintlayout.compose.Dimension getAtLeastWrapContent(androidx.constraintlayout.compose.Dimension.MinCoercible);
+ method public static androidx.constraintlayout.compose.Dimension.MinCoercible getAtMostWrapContent(androidx.constraintlayout.compose.Dimension.Coercible);
+ method public static androidx.constraintlayout.compose.Dimension getAtMostWrapContent(androidx.constraintlayout.compose.Dimension.MaxCoercible);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintLayoutScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
+ ctor @kotlin.PublishedApi internal ConstraintLayoutScope();
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier constrainAs(androidx.compose.ui.Modifier, androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRef();
+ method @androidx.compose.runtime.Stable public androidx.constraintlayout.compose.ConstraintLayoutScope.ConstrainedLayoutReferences createRefs();
+ field @kotlin.PublishedApi internal boolean isAnimateChanges;
+ }
+
+ public final class ConstraintLayoutScope.ConstrainedLayoutReferences {
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
+ }
+
+ public final class ConstraintLayoutTagKt {
+ method public static Object? getConstraintLayoutId(androidx.compose.ui.layout.Measurable);
+ method public static Object? getConstraintLayoutTag(androidx.compose.ui.layout.Measurable);
+ method public static androidx.compose.ui.Modifier layoutId(androidx.compose.ui.Modifier, String layoutId, optional String? tag);
+ }
+
+ public interface ConstraintLayoutTagParentData {
+ method public String getConstraintLayoutId();
+ method public String getConstraintLayoutTag();
+ property public abstract String constraintLayoutId;
+ property public abstract String constraintLayoutTag;
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface ConstraintSet {
+ method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ method public default void applyTo(androidx.constraintlayout.core.state.Transition transition, int type);
+ method public default boolean isDirty(java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ method public default androidx.constraintlayout.compose.ConstraintSet override(String name, float value);
+ }
+
+ @kotlin.PublishedApi internal final class ConstraintSetForInlineDsl implements androidx.constraintlayout.compose.ConstraintSet androidx.compose.runtime.RememberObserver {
+ ctor public ConstraintSetForInlineDsl(androidx.constraintlayout.compose.ConstraintLayoutScope scope);
+ method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ method public boolean getKnownDirty();
+ method public androidx.constraintlayout.compose.ConstraintLayoutScope getScope();
+ method public void onAbandoned();
+ method public void onForgotten();
+ method public void onRemembered();
+ method public void setKnownDirty(boolean);
+ property public final boolean knownDirty;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutScope scope;
+ }
+
+ public final class ConstraintSetRef {
+ method public androidx.constraintlayout.compose.ConstraintSetRef copy(String name);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintSetScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+ method public androidx.constraintlayout.compose.ConstraintSetScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+ }
+
+ public final class ConstraintSetScope.ConstrainedLayoutReferences {
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class CurveFit {
+ method public String getName();
+ property public String name;
+ field public static final androidx.constraintlayout.compose.CurveFit.Companion Companion;
+ }
+
+ public static final class CurveFit.Companion {
+ method public androidx.constraintlayout.compose.CurveFit getLinear();
+ method public androidx.constraintlayout.compose.CurveFit getSpline();
+ property public final androidx.constraintlayout.compose.CurveFit Linear;
+ property public final androidx.constraintlayout.compose.CurveFit Spline;
+ }
+
+ @kotlin.jvm.JvmInline public final value class DebugFlags {
+ ctor public DebugFlags(optional boolean showBounds, optional boolean showPaths, optional boolean showKeyPositions);
+ method public boolean getShowBounds();
+ method public boolean getShowKeyPositions();
+ method public boolean getShowPaths();
+ property public final boolean showBounds;
+ property public final boolean showKeyPositions;
+ property public final boolean showPaths;
+ field public static final androidx.constraintlayout.compose.DebugFlags.Companion Companion;
+ }
+
+ public static final class DebugFlags.Companion {
+ method public int getAll();
+ method public int getNone();
+ property public final int All;
+ property public final int None;
+ }
+
+ public final class DesignElements {
+ method public void define(String name, kotlin.jvm.functions.Function2<? super java.lang.String,? super java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit> function);
+ method public java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>> getMap();
+ method public void setMap(java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>>);
+ property public final java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>> map;
+ field public static final androidx.constraintlayout.compose.DesignElements INSTANCE;
+ }
+
+ public interface DesignInfoProvider {
+ method public String getDesignInfo(int startX, int startY, String args);
+ }
+
+ public interface Dimension {
+ field public static final androidx.constraintlayout.compose.Dimension.Companion Companion;
+ }
+
+ public static interface Dimension.Coercible extends androidx.constraintlayout.compose.Dimension {
+ }
+
+ public static final class Dimension.Companion {
+ method public androidx.constraintlayout.compose.Dimension.Coercible getFillToConstraints();
+ method public androidx.constraintlayout.compose.Dimension getMatchParent();
+ method public androidx.constraintlayout.compose.Dimension.Coercible getPreferredWrapContent();
+ method public androidx.constraintlayout.compose.Dimension getWrapContent();
+ method public androidx.constraintlayout.compose.Dimension percent(float percent);
+ method public androidx.constraintlayout.compose.Dimension.MinCoercible preferredValue(float dp);
+ method public androidx.constraintlayout.compose.Dimension ratio(String ratio);
+ method public androidx.constraintlayout.compose.Dimension value(float dp);
+ property public final androidx.constraintlayout.compose.Dimension.Coercible fillToConstraints;
+ property public final androidx.constraintlayout.compose.Dimension matchParent;
+ property public final androidx.constraintlayout.compose.Dimension.Coercible preferredWrapContent;
+ property public final androidx.constraintlayout.compose.Dimension wrapContent;
+ }
+
+ public static interface Dimension.MaxCoercible extends androidx.constraintlayout.compose.Dimension {
+ }
+
+ public static interface Dimension.MinCoercible extends androidx.constraintlayout.compose.Dimension {
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class Easing {
+ method public String getName();
+ property public String name;
+ field public static final androidx.constraintlayout.compose.Easing.Companion Companion;
+ }
+
+ public static final class Easing.Companion {
+ method public androidx.constraintlayout.compose.Easing cubic(float x1, float y1, float x2, float y2);
+ method public androidx.constraintlayout.compose.Easing getAccelerate();
+ method public androidx.constraintlayout.compose.Easing getAnticipate();
+ method public androidx.constraintlayout.compose.Easing getDecelerate();
+ method public androidx.constraintlayout.compose.Easing getLinear();
+ method public androidx.constraintlayout.compose.Easing getOvershoot();
+ method public androidx.constraintlayout.compose.Easing getStandard();
+ property public final androidx.constraintlayout.compose.Easing Accelerate;
+ property public final androidx.constraintlayout.compose.Easing Anticipate;
+ property public final androidx.constraintlayout.compose.Easing Decelerate;
+ property public final androidx.constraintlayout.compose.Easing Linear;
+ property public final androidx.constraintlayout.compose.Easing Overshoot;
+ property public final androidx.constraintlayout.compose.Easing Standard;
+ }
+
+ @kotlin.PublishedApi internal abstract class EditableJSONLayout implements androidx.constraintlayout.compose.LayoutInformationReceiver {
+ ctor public EditableJSONLayout(@org.intellij.lang.annotations.Language("json5") String content);
+ method public final String getCurrentContent();
+ method public final String? getDebugName();
+ method public androidx.constraintlayout.compose.MotionLayoutDebugFlags getForcedDrawDebug();
+ method public int getForcedHeight();
+ method public int getForcedWidth();
+ method public final String getLayoutInformation();
+ method public androidx.constraintlayout.compose.LayoutInfoFlags getLayoutInformationMode();
+ method protected final void initialization();
+ method protected final void onDrawDebug(int debugMode);
+ method protected final void onLayoutInformation(int mode);
+ method protected void onNewContent(String content);
+ method public final void onNewDimensions(int width, int height);
+ method public final void setCurrentContent(String content);
+ method public final void setDebugName(String? name);
+ method public void setLayoutInformation(String information);
+ method public void setUpdateFlag(androidx.compose.runtime.MutableState<java.lang.Long> needsUpdate);
+ method protected final void signalUpdate();
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="MotionLayout API is experimental and it is likely to change.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMotionApi {
+ }
+
+ @androidx.compose.runtime.Immutable public final class FlowStyle {
+ field public static final androidx.constraintlayout.compose.FlowStyle.Companion Companion;
+ }
+
+ public static final class FlowStyle.Companion {
+ method public androidx.constraintlayout.compose.FlowStyle getPacked();
+ method public androidx.constraintlayout.compose.FlowStyle getSpread();
+ method public androidx.constraintlayout.compose.FlowStyle getSpreadInside();
+ property public final androidx.constraintlayout.compose.FlowStyle Packed;
+ property public final androidx.constraintlayout.compose.FlowStyle Spread;
+ property public final androidx.constraintlayout.compose.FlowStyle SpreadInside;
+ }
+
+ @kotlin.jvm.JvmInline public final value class GridFlag {
+ method public boolean isPlaceLayoutsOnSpansFirst();
+ method public infix int or(int other);
+ property public final boolean isPlaceLayoutsOnSpansFirst;
+ field public static final androidx.constraintlayout.compose.GridFlag.Companion Companion;
+ }
+
+ public static final class GridFlag.Companion {
+ method public int getNone();
+ method public int getPlaceLayoutsOnSpansFirst();
+ property public final int None;
+ property public final int PlaceLayoutsOnSpansFirst;
+ }
+
+ @androidx.compose.runtime.Immutable public final class HorizontalAlign {
+ field public static final androidx.constraintlayout.compose.HorizontalAlign.Companion Companion;
+ }
+
+ public static final class HorizontalAlign.Companion {
+ method public androidx.constraintlayout.compose.HorizontalAlign getCenter();
+ method public androidx.constraintlayout.compose.HorizontalAlign getEnd();
+ method public androidx.constraintlayout.compose.HorizontalAlign getStart();
+ property public final androidx.constraintlayout.compose.HorizontalAlign Center;
+ property public final androidx.constraintlayout.compose.HorizontalAlign End;
+ property public final androidx.constraintlayout.compose.HorizontalAlign Start;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+ }
+
+ @androidx.compose.runtime.Stable public final class HorizontalChainReference extends androidx.constraintlayout.compose.LayoutReference {
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getAbsoluteRight();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getEnd();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor getStart();
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteLeft;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor absoluteRight;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor end;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor start;
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class HorizontalChainScope {
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
+ method public androidx.constraintlayout.compose.VerticalAnchorable getStart();
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteLeft;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable absoluteRight;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable end;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
+ property public final androidx.constraintlayout.compose.VerticalAnchorable start;
+ }
+
+ public final class InvalidationStrategy {
+ ctor public InvalidationStrategy(optional kotlin.jvm.functions.Function3<? super androidx.constraintlayout.compose.InvalidationStrategySpecification,? super androidx.compose.ui.unit.Constraints,? super androidx.compose.ui.unit.Constraints,java.lang.Boolean>? onIncomingConstraints, kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange);
+ method public kotlin.jvm.functions.Function3<androidx.constraintlayout.compose.InvalidationStrategySpecification,androidx.compose.ui.unit.Constraints,androidx.compose.ui.unit.Constraints,java.lang.Boolean>? getOnIncomingConstraints();
+ method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnObservedStateChange();
+ property public final kotlin.jvm.functions.Function3<androidx.constraintlayout.compose.InvalidationStrategySpecification,androidx.compose.ui.unit.Constraints,androidx.compose.ui.unit.Constraints,java.lang.Boolean>? onIncomingConstraints;
+ property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange;
+ field public static final androidx.constraintlayout.compose.InvalidationStrategy.Companion Companion;
+ }
+
+ public static final class InvalidationStrategy.Companion {
+ method public androidx.constraintlayout.compose.InvalidationStrategy getDefaultInvalidationStrategy();
+ property public final androidx.constraintlayout.compose.InvalidationStrategy DefaultInvalidationStrategy;
+ }
+
+ public final class InvalidationStrategySpecification {
+ method public boolean shouldInvalidateOnFixedHeight(long oldConstraints, long newConstraints, int skipCount, int threshold);
+ method public boolean shouldInvalidateOnFixedWidth(long oldConstraints, long newConstraints, int skipCount, int threshold);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyAttributeScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
+ method public float getAlpha();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public void setAlpha(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ property public final float alpha;
+ property public final float rotationX;
+ property public final float rotationY;
+ property public final float rotationZ;
+ property public final float scaleX;
+ property public final float scaleY;
+ property public final float translationX;
+ property public final float translationY;
+ property public final float translationZ;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyAttributesScope extends androidx.constraintlayout.compose.BaseKeyFramesScope {
+ method public void frame(@IntRange(from=0L, to=100L) int frame, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyAttributeScope,kotlin.Unit> keyFrameContent);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyCycleScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
+ method public float getAlpha();
+ method public float getOffset();
+ method public float getPeriod();
+ method public float getPhase();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public void setAlpha(float);
+ method public void setOffset(float);
+ method public void setPeriod(float);
+ method public void setPhase(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ property public final float alpha;
+ property public final float offset;
+ property public final float period;
+ property public final float phase;
+ property public final float rotationX;
+ property public final float rotationY;
+ property public final float rotationZ;
+ property public final float scaleX;
+ property public final float scaleY;
+ property public final float translationX;
+ property public final float translationY;
+ property public final float translationZ;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyCyclesScope extends androidx.constraintlayout.compose.BaseKeyFramesScope {
+ method public void frame(@IntRange(from=0L, to=100L) int frame, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyCycleScope,kotlin.Unit> keyFrameContent);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyPositionScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
+ method public androidx.constraintlayout.compose.CurveFit? getCurveFit();
+ method public float getPercentHeight();
+ method public float getPercentWidth();
+ method public float getPercentX();
+ method public float getPercentY();
+ method public void setCurveFit(androidx.constraintlayout.compose.CurveFit?);
+ method public void setPercentHeight(float);
+ method public void setPercentWidth(float);
+ method public void setPercentX(float);
+ method public void setPercentY(float);
+ property public final androidx.constraintlayout.compose.CurveFit? curveFit;
+ property public final float percentHeight;
+ property public final float percentWidth;
+ property public final float percentX;
+ property public final float percentY;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class KeyPositionsScope extends androidx.constraintlayout.compose.BaseKeyFramesScope {
+ method public void frame(@IntRange(from=0L, to=100L) int frame, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyPositionScope,kotlin.Unit> keyFrameContent);
+ method public androidx.constraintlayout.compose.RelativePosition getType();
+ method public void setType(androidx.constraintlayout.compose.RelativePosition);
+ property public final androidx.constraintlayout.compose.RelativePosition type;
+ }
+
+ public final class LateMotionLayoutKt {
+ method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void LateMotionLayout(androidx.compose.runtime.MutableState<androidx.constraintlayout.compose.ConstraintSet?> start, androidx.compose.runtime.MutableState<androidx.constraintlayout.compose.ConstraintSet?> end, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlinx.coroutines.channels.Channel<androidx.constraintlayout.compose.ConstraintSet> channel, androidx.compose.runtime.State<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, int optimizationLevel, kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public enum LayoutInfoFlags {
+ enum_constant public static final androidx.constraintlayout.compose.LayoutInfoFlags BOUNDS;
+ enum_constant public static final androidx.constraintlayout.compose.LayoutInfoFlags NONE;
+ }
+
+ public interface LayoutInformationReceiver {
+ method public androidx.constraintlayout.compose.MotionLayoutDebugFlags getForcedDrawDebug();
+ method public int getForcedHeight();
+ method public float getForcedProgress();
+ method public int getForcedWidth();
+ method public androidx.constraintlayout.compose.LayoutInfoFlags getLayoutInformationMode();
+ method public void onNewProgress(float progress);
+ method public void resetForcedProgress();
+ method public void setLayoutInformation(String information);
+ method public void setUpdateFlag(androidx.compose.runtime.MutableState<java.lang.Long> needsUpdate);
+ }
+
+ @androidx.compose.runtime.Stable public abstract class LayoutReference {
+ }
+
+ @kotlin.PublishedApi internal class Measurer implements androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer androidx.constraintlayout.compose.DesignInfoProvider {
+ ctor public Measurer(androidx.compose.ui.unit.Density density);
+ method public final void addLayoutInformationReceiver(androidx.constraintlayout.compose.LayoutInformationReceiver? layoutReceiver);
+ method protected final void applyRootSize(long constraints);
+ method public void computeLayoutResult();
+ method @androidx.compose.runtime.Composable public final void createDesignElements();
+ method public void didMeasures();
+ method @androidx.compose.runtime.Composable public final void drawDebugBounds(androidx.compose.foundation.layout.BoxScope, float forcedScaleFactor);
+ method public final void drawDebugBounds(androidx.compose.ui.graphics.drawscope.DrawScope, float forcedScaleFactor);
+ method public String getDesignInfo(int startX, int startY, String args);
+ method public final float getForcedScaleFactor();
+ method protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.constraintlayout.core.state.WidgetFrame> getFrameCache();
+ method public final int getLayoutCurrentHeight();
+ method public final int getLayoutCurrentWidth();
+ method protected final androidx.constraintlayout.compose.LayoutInformationReceiver? getLayoutInformationReceiver();
+ method protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.compose.ui.layout.Placeable> getPlaceables();
+ method protected final androidx.constraintlayout.core.widgets.ConstraintWidgetContainer getRoot();
+ method protected final androidx.constraintlayout.compose.State getState();
+ method public void measure(androidx.constraintlayout.core.widgets.ConstraintWidget constraintWidget, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure measure);
+ method public final void parseDesignElements(androidx.constraintlayout.compose.ConstraintSet constraintSet);
+ method public final void performLayout(androidx.compose.ui.layout.Placeable.PlacementScope, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ method public final long performMeasure(long constraints, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.ConstraintSet constraintSet, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, int optimizationLevel);
+ method public final void setForcedScaleFactor(float);
+ method protected final void setLayoutInformationReceiver(androidx.constraintlayout.compose.LayoutInformationReceiver?);
+ property public final float forcedScaleFactor;
+ property protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.constraintlayout.core.state.WidgetFrame> frameCache;
+ property public final int layoutCurrentHeight;
+ property public final int layoutCurrentWidth;
+ property protected final androidx.constraintlayout.compose.LayoutInformationReceiver? layoutInformationReceiver;
+ property protected final java.util.Map<androidx.compose.ui.layout.Measurable,androidx.compose.ui.layout.Placeable> placeables;
+ property protected final androidx.constraintlayout.core.widgets.ConstraintWidgetContainer root;
+ property protected final androidx.constraintlayout.compose.State state;
+ }
+
+ public final class MotionCarouselKt {
+ method @androidx.compose.runtime.Composable public static void ItemHolder(int i, String slotPrefix, boolean showSlot, kotlin.jvm.functions.Function0<kotlin.Unit> function);
+ method @androidx.compose.runtime.Composable public static void MotionCarousel(androidx.constraintlayout.compose.MotionScene motionScene, int initialSlotIndex, int numSlots, optional String backwardTransition, optional String forwardTransition, optional String slotPrefix, optional boolean showSlots, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionCarouselScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.constraintlayout.compose.MotionCarouselScope, java.util.List<? extends T> items, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsWithProperties(androidx.constraintlayout.compose.MotionCarouselScope, java.util.List<? extends T> items, kotlin.jvm.functions.Function2<? super T,? super androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties>,kotlin.Unit> itemContent);
+ }
+
+ public interface MotionCarouselScope {
+ method public void items(int count, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> itemContent);
+ method public void itemsWithProperties(int count, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties>,kotlin.Unit> itemContent);
+ }
+
+ public interface MotionItemsProvider {
+ method public int count();
+ method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
+ method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index, androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties> properties);
+ method public boolean hasItemsWithProperties();
+ }
+
+ public enum MotionLayoutDebugFlags {
+ enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags NONE;
+ enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags SHOW_ALL;
+ enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags UNKNOWN;
+ }
+
+ @Deprecated public enum MotionLayoutFlag {
+ enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
+ enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
+ }
+
+ public final class MotionLayoutKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, float progress, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.constraintlayout.compose.Transition? transition, float progress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, int optimizationLevel, boolean showBounds, boolean showPaths, boolean showKeyPositions, androidx.compose.ui.Modifier modifier, androidx.compose.runtime.MutableState<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, String transitionName, int optimizationLevel, int debugFlags, androidx.compose.ui.Modifier modifier, androidx.compose.runtime.MutableState<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, androidx.compose.runtime.MutableState<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionLayoutScope {
+ method public long customColor(String id, String name);
+ method public float customDistance(String id, String name);
+ method public float customFloat(String id, String name);
+ method public long customFontSize(String id, String name);
+ method public int customInt(String id, String name);
+ method public androidx.constraintlayout.compose.MotionLayoutScope.CustomProperties customProperties(String id);
+ method @Deprecated public long motionColor(String id, String name);
+ method @Deprecated public float motionDistance(String id, String name);
+ method @Deprecated public float motionFloat(String id, String name);
+ method @Deprecated public long motionFontSize(String id, String name);
+ method @Deprecated public int motionInt(String id, String name);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties> motionProperties(String id);
+ method @Deprecated public androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties motionProperties(String id, String tag);
+ method public androidx.compose.ui.Modifier onStartEndBoundsChanged(androidx.compose.ui.Modifier, Object layoutId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.geometry.Rect,? super androidx.compose.ui.geometry.Rect,kotlin.Unit> onBoundsChanged);
+ }
+
+ public final class MotionLayoutScope.CustomProperties {
+ method public long color(String name);
+ method public float distance(String name);
+ method public float float(String name);
+ method public long fontSize(String name);
+ method public int int(String name);
+ }
+
+ public final class MotionLayoutScope.MotionProperties {
+ method public long color(String name);
+ method public float distance(String name);
+ method public float float(String name);
+ method public long fontSize(String name);
+ method public String id();
+ method public int int(String name);
+ method public String? tag();
+ }
+
+ @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.constraintlayout.compose.ExperimentalMotionApi public interface MotionScene extends androidx.constraintlayout.core.state.CoreMotionScene {
+ method public androidx.constraintlayout.compose.ConstraintSet? getConstraintSetInstance(String name);
+ method public androidx.constraintlayout.compose.Transition? getTransitionInstance(String name);
+ }
+
+ public final class MotionSceneKt {
+ method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(@org.intellij.lang.annotations.Language("json5") String content);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionSceneScope {
+ method public androidx.constraintlayout.compose.ConstraintSetRef addConstraintSet(androidx.constraintlayout.compose.ConstraintSet constraintSet, optional String? name);
+ method public void addTransition(androidx.constraintlayout.compose.Transition transition, optional String? name);
+ method public androidx.constraintlayout.compose.ConstraintSetRef constraintSet(optional String? name, optional androidx.constraintlayout.compose.ConstraintSetRef? extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> constraintSetContent);
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+ method public androidx.constraintlayout.compose.MotionSceneScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+ method public void customColor(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+ method public void customColor(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+ method public void customDistance(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+ method public void customDistance(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
+ method public void customFloat(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+ method public void customFloat(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
+ method public void customFontSize(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+ method public void customFontSize(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+ method public void customInt(androidx.constraintlayout.compose.ConstrainScope, String name, int value);
+ method public void customInt(androidx.constraintlayout.compose.KeyAttributeScope, String name, int value);
+ method public void defaultTransition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
+ method public float getStaggeredWeight(androidx.constraintlayout.compose.ConstrainScope);
+ method public void setStaggeredWeight(androidx.constraintlayout.compose.ConstrainScope, float);
+ method public void transition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional String? name, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
+ }
+
+ public final class MotionSceneScope.ConstrainedLayoutReferences {
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+ method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
+ }
+
+ public final class MotionSceneScopeKt {
+ method @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionSceneScope,kotlin.Unit> motionSceneContent);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class OnSwipe {
+ ctor public OnSwipe(androidx.constraintlayout.compose.ConstrainedLayoutReference anchor, androidx.constraintlayout.compose.SwipeSide side, androidx.constraintlayout.compose.SwipeDirection direction, optional float dragScale, optional float dragThreshold, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo, optional androidx.constraintlayout.compose.SwipeTouchUp onTouchUp, optional androidx.constraintlayout.compose.SwipeMode mode);
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getAnchor();
+ method public androidx.constraintlayout.compose.SwipeDirection getDirection();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference? getDragAround();
+ method public float getDragScale();
+ method public float getDragThreshold();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference? getLimitBoundsTo();
+ method public androidx.constraintlayout.compose.SwipeMode getMode();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getOnTouchUp();
+ method public androidx.constraintlayout.compose.SwipeSide getSide();
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference anchor;
+ property public final androidx.constraintlayout.compose.SwipeDirection direction;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround;
+ property public final float dragScale;
+ property public final float dragThreshold;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo;
+ property public final androidx.constraintlayout.compose.SwipeMode mode;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp onTouchUp;
+ property public final androidx.constraintlayout.compose.SwipeSide side;
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.PublishedApi internal final class RawConstraintSet implements androidx.constraintlayout.compose.ConstraintSet {
+ ctor public RawConstraintSet(androidx.constraintlayout.core.parser.CLObject clObject);
+ method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class RelativePosition {
+ method public String getName();
+ property public String name;
+ field public static final androidx.constraintlayout.compose.RelativePosition.Companion Companion;
+ }
+
+ public static final class RelativePosition.Companion {
+ method public androidx.constraintlayout.compose.RelativePosition getDelta();
+ method public androidx.constraintlayout.compose.RelativePosition getParent();
+ method public androidx.constraintlayout.compose.RelativePosition getPath();
+ property public final androidx.constraintlayout.compose.RelativePosition Delta;
+ property public final androidx.constraintlayout.compose.RelativePosition Parent;
+ property public final androidx.constraintlayout.compose.RelativePosition Path;
+ }
+
+ @kotlin.jvm.JvmInline public final value class Skip {
+ ctor public Skip(@IntRange(from=0L) int position, @IntRange(from=1L) int size);
+ ctor public Skip(@IntRange(from=0L) int position, @IntRange(from=1L) int rows, @IntRange(from=1L) int columns);
+ method public String getDescription();
+ property public final String description;
+ }
+
+ @kotlin.jvm.JvmInline public final value class Span {
+ ctor public Span(@IntRange(from=0L) int position, @IntRange(from=1L) int size);
+ ctor public Span(@IntRange(from=0L) int position, @IntRange(from=1L) int rows, @IntRange(from=1L) int columns);
+ ctor public Span(String description);
+ method public String getDescription();
+ property public final String description;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SpringBoundary {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SpringBoundary.Companion Companion;
+ }
+
+ public static final class SpringBoundary.Companion {
+ method public androidx.constraintlayout.compose.SpringBoundary getBounceBoth();
+ method public androidx.constraintlayout.compose.SpringBoundary getBounceEnd();
+ method public androidx.constraintlayout.compose.SpringBoundary getBounceStart();
+ method public androidx.constraintlayout.compose.SpringBoundary getOvershoot();
+ property public final androidx.constraintlayout.compose.SpringBoundary BounceBoth;
+ property public final androidx.constraintlayout.compose.SpringBoundary BounceEnd;
+ property public final androidx.constraintlayout.compose.SpringBoundary BounceStart;
+ property public final androidx.constraintlayout.compose.SpringBoundary Overshoot;
+ }
+
+ public final class State extends androidx.constraintlayout.core.state.State {
+ ctor public State(androidx.compose.ui.unit.Density density);
+ method public androidx.compose.ui.unit.Density getDensity();
+ method @Deprecated public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
+ method public long getRootIncomingConstraints();
+ method @Deprecated public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection);
+ method public void setRootIncomingConstraints(long);
+ property public final androidx.compose.ui.unit.Density density;
+ property @Deprecated public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
+ property public final long rootIncomingConstraints;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeDirection {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeDirection.Companion Companion;
+ }
+
+ public static final class SwipeDirection.Companion {
+ method public androidx.constraintlayout.compose.SwipeDirection getClockwise();
+ method public androidx.constraintlayout.compose.SwipeDirection getCounterclockwise();
+ method public androidx.constraintlayout.compose.SwipeDirection getDown();
+ method public androidx.constraintlayout.compose.SwipeDirection getEnd();
+ method public androidx.constraintlayout.compose.SwipeDirection getLeft();
+ method public androidx.constraintlayout.compose.SwipeDirection getRight();
+ method public androidx.constraintlayout.compose.SwipeDirection getStart();
+ method public androidx.constraintlayout.compose.SwipeDirection getUp();
+ property public final androidx.constraintlayout.compose.SwipeDirection Clockwise;
+ property public final androidx.constraintlayout.compose.SwipeDirection Counterclockwise;
+ property public final androidx.constraintlayout.compose.SwipeDirection Down;
+ property public final androidx.constraintlayout.compose.SwipeDirection End;
+ property public final androidx.constraintlayout.compose.SwipeDirection Left;
+ property public final androidx.constraintlayout.compose.SwipeDirection Right;
+ property public final androidx.constraintlayout.compose.SwipeDirection Start;
+ property public final androidx.constraintlayout.compose.SwipeDirection Up;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeMode {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeMode.Companion Companion;
+ }
+
+ public static final class SwipeMode.Companion {
+ method public androidx.constraintlayout.compose.SwipeMode getSpring();
+ method public androidx.constraintlayout.compose.SwipeMode getVelocity();
+ method public androidx.constraintlayout.compose.SwipeMode spring(optional float mass, optional float stiffness, optional float damping, optional float threshold, optional androidx.constraintlayout.compose.SpringBoundary boundary);
+ method public androidx.constraintlayout.compose.SwipeMode velocity(optional float maxVelocity, optional float maxAcceleration);
+ property public final androidx.constraintlayout.compose.SwipeMode Spring;
+ property public final androidx.constraintlayout.compose.SwipeMode Velocity;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeSide {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeSide.Companion Companion;
+ }
+
+ public static final class SwipeSide.Companion {
+ method public androidx.constraintlayout.compose.SwipeSide getBottom();
+ method public androidx.constraintlayout.compose.SwipeSide getEnd();
+ method public androidx.constraintlayout.compose.SwipeSide getLeft();
+ method public androidx.constraintlayout.compose.SwipeSide getMiddle();
+ method public androidx.constraintlayout.compose.SwipeSide getRight();
+ method public androidx.constraintlayout.compose.SwipeSide getStart();
+ method public androidx.constraintlayout.compose.SwipeSide getTop();
+ property public final androidx.constraintlayout.compose.SwipeSide Bottom;
+ property public final androidx.constraintlayout.compose.SwipeSide End;
+ property public final androidx.constraintlayout.compose.SwipeSide Left;
+ property public final androidx.constraintlayout.compose.SwipeSide Middle;
+ property public final androidx.constraintlayout.compose.SwipeSide Right;
+ property public final androidx.constraintlayout.compose.SwipeSide Start;
+ property public final androidx.constraintlayout.compose.SwipeSide Top;
+ }
+
+ @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public final class SwipeTouchUp {
+ method public String getName();
+ property public final String name;
+ field public static final androidx.constraintlayout.compose.SwipeTouchUp.Companion Companion;
+ }
+
+ public static final class SwipeTouchUp.Companion {
+ method public androidx.constraintlayout.compose.SwipeTouchUp getAutoComplete();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getDecelerate();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getNeverCompleteEnd();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getNeverCompleteStart();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getStop();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getToEnd();
+ method public androidx.constraintlayout.compose.SwipeTouchUp getToStart();
+ property public final androidx.constraintlayout.compose.SwipeTouchUp AutoComplete;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp Decelerate;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp NeverCompleteEnd;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp NeverCompleteStart;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp Stop;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp ToEnd;
+ property public final androidx.constraintlayout.compose.SwipeTouchUp ToStart;
+ }
+
+ public final class ToolingUtilsKt {
+ method public static androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.constraintlayout.compose.DesignInfoProvider> getDesignInfoDataKey();
+ property public static final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.constraintlayout.compose.DesignInfoProvider> DesignInfoDataKey;
+ field @kotlin.PublishedApi internal static final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.constraintlayout.compose.DesignInfoProvider> designInfoProvider$delegate;
+ }
+
+ @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.constraintlayout.compose.ExperimentalMotionApi public interface Transition {
+ method public String getEndConstraintSetId();
+ method public String getStartConstraintSetId();
+ }
+
+ public final class TransitionKt {
+ method @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.Transition Transition(@org.intellij.lang.annotations.Language("json5") String content);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class TransitionScope {
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+ method public float getMaxStaggerDelay();
+ method public androidx.constraintlayout.compose.Arc getMotionArc();
+ method public androidx.constraintlayout.compose.OnSwipe? getOnSwipe();
+ method public void keyAttributes(androidx.constraintlayout.compose.ConstrainedLayoutReference[] targets, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyAttributesScope,kotlin.Unit> keyAttributesContent);
+ method public void keyCycles(androidx.constraintlayout.compose.ConstrainedLayoutReference[] targets, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyCyclesScope,kotlin.Unit> keyCyclesContent);
+ method public void keyPositions(androidx.constraintlayout.compose.ConstrainedLayoutReference[] targets, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.KeyPositionsScope,kotlin.Unit> keyPositionsContent);
+ method public void setMaxStaggerDelay(float);
+ method public void setMotionArc(androidx.constraintlayout.compose.Arc);
+ method public void setOnSwipe(androidx.constraintlayout.compose.OnSwipe?);
+ property public final float maxStaggerDelay;
+ property public final androidx.constraintlayout.compose.Arc motionArc;
+ property public final androidx.constraintlayout.compose.OnSwipe? onSwipe;
+ }
+
+ public final class TransitionScopeKt {
+ method @SuppressCompatibility @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.Transition Transition(optional String from, optional String to, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> content);
+ }
+
+ @androidx.compose.runtime.Immutable public final class VerticalAlign {
+ field public static final androidx.constraintlayout.compose.VerticalAlign.Companion Companion;
+ }
+
+ public static final class VerticalAlign.Companion {
+ method public androidx.constraintlayout.compose.VerticalAlign getBaseline();
+ method public androidx.constraintlayout.compose.VerticalAlign getBottom();
+ method public androidx.constraintlayout.compose.VerticalAlign getCenter();
+ method public androidx.constraintlayout.compose.VerticalAlign getTop();
+ property public final androidx.constraintlayout.compose.VerticalAlign Baseline;
+ property public final androidx.constraintlayout.compose.VerticalAlign Bottom;
+ property public final androidx.constraintlayout.compose.VerticalAlign Center;
+ property public final androidx.constraintlayout.compose.VerticalAlign Top;
+ }
+
+ @kotlin.jvm.JvmDefaultWithCompatibility public interface VerticalAnchorable {
+ method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor, optional float margin, optional float goneMargin);
+ }
+
+ @androidx.compose.runtime.Stable public final class VerticalChainReference extends androidx.constraintlayout.compose.LayoutReference {
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getBottom();
+ method public androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor getTop();
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor bottom;
+ property public final androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor top;
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class VerticalChainScope {
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
+ method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
+ method public androidx.constraintlayout.compose.HorizontalAnchorable getTop();
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
+ property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
+ property public final androidx.constraintlayout.compose.HorizontalAnchorable top;
+ }
+
+ @androidx.compose.runtime.Immutable public final class Visibility {
+ field public static final androidx.constraintlayout.compose.Visibility.Companion Companion;
+ }
+
+ public static final class Visibility.Companion {
+ method public androidx.constraintlayout.compose.Visibility getGone();
+ method public androidx.constraintlayout.compose.Visibility getInvisible();
+ method public androidx.constraintlayout.compose.Visibility getVisible();
+ property public final androidx.constraintlayout.compose.Visibility Gone;
+ property public final androidx.constraintlayout.compose.Visibility Invisible;
+ property public final androidx.constraintlayout.compose.Visibility Visible;
+ }
+
+ @androidx.compose.runtime.Immutable public final class Wrap {
+ field public static final androidx.constraintlayout.compose.Wrap.Companion Companion;
+ }
+
+ public static final class Wrap.Companion {
+ method public androidx.constraintlayout.compose.Wrap getAligned();
+ method public androidx.constraintlayout.compose.Wrap getChain();
+ method public androidx.constraintlayout.compose.Wrap getNone();
+ property public final androidx.constraintlayout.compose.Wrap Aligned;
+ property public final androidx.constraintlayout.compose.Wrap Chain;
+ property public final androidx.constraintlayout.compose.Wrap None;
+ }
+
+}
+
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
index daa4e5d..cf8d5c8f 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
@@ -121,7 +121,7 @@
packageName = PACKAGE_NAME,
metrics = listOf(FrameTimingMetric()),
compilationMode = CompilationMode.DEFAULT,
- iterations = 10,
+ iterations = 8,
// HOT causes issues with the measure block logic where multiple click actions are
// triggered at once
startupMode = StartupMode.WARM,
diff --git a/constraintlayout/constraintlayout-core/api/1.1.0-beta01.txt b/constraintlayout/constraintlayout-core/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..5be1d12
--- /dev/null
+++ b/constraintlayout/constraintlayout-core/api/1.1.0-beta01.txt
@@ -0,0 +1,3393 @@
+// Signature format: 4.0
+package androidx.constraintlayout.core {
+
+ public class ArrayLinkedVariables implements androidx.constraintlayout.core.ArrayRow.ArrayRowVariables {
+ method public void add(androidx.constraintlayout.core.SolverVariable!, float, boolean);
+ method public final void clear();
+ method public boolean contains(androidx.constraintlayout.core.SolverVariable!);
+ method public void display();
+ method public void divideByAmount(float);
+ method public final float get(androidx.constraintlayout.core.SolverVariable!);
+ method public int getCurrentSize();
+ method public int getHead();
+ method public final int getId(int);
+ method public final int getNextIndice(int);
+ method public final float getValue(int);
+ method public androidx.constraintlayout.core.SolverVariable! getVariable(int);
+ method public float getVariableValue(int);
+ method public int indexOf(androidx.constraintlayout.core.SolverVariable!);
+ method public void invert();
+ method public final void put(androidx.constraintlayout.core.SolverVariable!, float);
+ method public final float remove(androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public int sizeInBytes();
+ method public float use(androidx.constraintlayout.core.ArrayRow!, boolean);
+ field protected final androidx.constraintlayout.core.Cache! mCache;
+ }
+
+ public class ArrayRow {
+ ctor public ArrayRow();
+ ctor public ArrayRow(androidx.constraintlayout.core.Cache!);
+ method public androidx.constraintlayout.core.ArrayRow! addError(androidx.constraintlayout.core.LinearSystem!, int);
+ method public void addError(androidx.constraintlayout.core.SolverVariable!);
+ method public void clear();
+ method public androidx.constraintlayout.core.ArrayRow! createRowDimensionRatio(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEqualDimension(float, float, float, androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEqualMatchDimensions(float, float, float, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEquals(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEquals(androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowGreaterThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowGreaterThan(androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!);
+ method public androidx.constraintlayout.core.ArrayRow! createRowLowerThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowWithAngle(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public androidx.constraintlayout.core.SolverVariable! getKey();
+ method public androidx.constraintlayout.core.SolverVariable! getPivotCandidate(androidx.constraintlayout.core.LinearSystem!, boolean[]!);
+ method public void initFromRow(androidx.constraintlayout.core.LinearSystem.Row!);
+ method public boolean isEmpty();
+ method public androidx.constraintlayout.core.SolverVariable! pickPivot(androidx.constraintlayout.core.SolverVariable!);
+ method public void reset();
+ method public void updateFromFinalVariable(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public void updateFromRow(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.ArrayRow!, boolean);
+ method public void updateFromSynonymVariable(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public void updateFromSystem(androidx.constraintlayout.core.LinearSystem!);
+ field public androidx.constraintlayout.core.ArrayRow.ArrayRowVariables! variables;
+ }
+
+ public static interface ArrayRow.ArrayRowVariables {
+ method public void add(androidx.constraintlayout.core.SolverVariable!, float, boolean);
+ method public void clear();
+ method public boolean contains(androidx.constraintlayout.core.SolverVariable!);
+ method public void display();
+ method public void divideByAmount(float);
+ method public float get(androidx.constraintlayout.core.SolverVariable!);
+ method public int getCurrentSize();
+ method public androidx.constraintlayout.core.SolverVariable! getVariable(int);
+ method public float getVariableValue(int);
+ method public int indexOf(androidx.constraintlayout.core.SolverVariable!);
+ method public void invert();
+ method public void put(androidx.constraintlayout.core.SolverVariable!, float);
+ method public float remove(androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public int sizeInBytes();
+ method public float use(androidx.constraintlayout.core.ArrayRow!, boolean);
+ }
+
+ public class Cache {
+ ctor public Cache();
+ }
+
+ public class GoalRow extends androidx.constraintlayout.core.ArrayRow {
+ ctor public GoalRow(androidx.constraintlayout.core.Cache!);
+ }
+
+ public class LinearSystem {
+ ctor public LinearSystem();
+ method public void addCenterPoint(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintWidget!, float, int);
+ method public void addCentering(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, float, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addConstraint(androidx.constraintlayout.core.ArrayRow!);
+ method public androidx.constraintlayout.core.ArrayRow! addEquality(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addEquality(androidx.constraintlayout.core.SolverVariable!, int);
+ method public void addGreaterBarrier(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, boolean);
+ method public void addGreaterThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addLowerBarrier(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, boolean);
+ method public void addLowerThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addRatio(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float, int);
+ method public void addSynonym(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.SolverVariable! createErrorVariable(int, String!);
+ method public androidx.constraintlayout.core.SolverVariable! createExtraVariable();
+ method public androidx.constraintlayout.core.SolverVariable! createObjectVariable(Object!);
+ method public androidx.constraintlayout.core.ArrayRow! createRow();
+ method public static androidx.constraintlayout.core.ArrayRow! createRowDimensionPercent(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public androidx.constraintlayout.core.SolverVariable! createSlackVariable();
+ method public void displayReadableRows();
+ method public void displayVariablesReadableRows();
+ method public void fillMetrics(androidx.constraintlayout.core.Metrics!);
+ method public androidx.constraintlayout.core.Cache! getCache();
+ method public int getMemoryUsed();
+ method public static androidx.constraintlayout.core.Metrics! getMetrics();
+ method public int getNumEquations();
+ method public int getNumVariables();
+ method public int getObjectVariableValue(Object!);
+ method public void minimize() throws java.lang.Exception;
+ method public void removeRow(androidx.constraintlayout.core.ArrayRow!);
+ method public void reset();
+ field public static long ARRAY_ROW_CREATION;
+ field public static final boolean DEBUG = false;
+ field public static final boolean FULL_DEBUG = false;
+ field public static long OPTIMIZED_ARRAY_ROW_CREATION;
+ field public static boolean OPTIMIZED_ENGINE;
+ field public static boolean SIMPLIFY_SYNONYMS;
+ field public static boolean SKIP_COLUMNS;
+ field public static boolean USE_BASIC_SYNONYMS;
+ field public static boolean USE_DEPENDENCY_ORDERING;
+ field public static boolean USE_SYNONYMS;
+ field public boolean graphOptimizer;
+ field public boolean hasSimpleDefinition;
+ field public boolean newgraphOptimizer;
+ field public static androidx.constraintlayout.core.Metrics! sMetrics;
+ }
+
+ public class Metrics {
+ ctor public Metrics();
+ method public void copy(androidx.constraintlayout.core.Metrics!);
+ method public void reset();
+ field public long additionalMeasures;
+ field public long bfs;
+ field public long constraints;
+ field public long determineGroups;
+ field public long errors;
+ field public long extravariables;
+ field public long fullySolved;
+ field public long graphOptimizer;
+ field public long graphSolved;
+ field public long grouping;
+ field public long infeasibleDetermineGroups;
+ field public long iterations;
+ field public long lastTableSize;
+ field public long layouts;
+ field public long linearSolved;
+ field public long mChildCount;
+ field public long mEquations;
+ field public long mMeasureCalls;
+ field public long mMeasureDuration;
+ field public int mNumberOfLayouts;
+ field public int mNumberOfMeasures;
+ field public long mSimpleEquations;
+ field public long mSolverPasses;
+ field public long mVariables;
+ field public long maxRows;
+ field public long maxTableSize;
+ field public long maxVariables;
+ field public long measuredMatchWidgets;
+ field public long measuredWidgets;
+ field public long measures;
+ field public long measuresLayoutDuration;
+ field public long measuresWidgetsDuration;
+ field public long measuresWrap;
+ field public long measuresWrapInfeasible;
+ field public long minimize;
+ field public long minimizeGoal;
+ field public long nonresolvedWidgets;
+ field public long optimize;
+ field public long pivots;
+ field public java.util.ArrayList<java.lang.String!>! problematicLayouts;
+ field public long resolutions;
+ field public long resolvedWidgets;
+ field public long simpleconstraints;
+ field public long slackvariables;
+ field public long tableSizeIncrease;
+ field public long variables;
+ field public long widgets;
+ }
+
+ public class PriorityGoalRow extends androidx.constraintlayout.core.ArrayRow {
+ ctor public PriorityGoalRow(androidx.constraintlayout.core.Cache!);
+ }
+
+ public class SolverVariable implements java.lang.Comparable<androidx.constraintlayout.core.SolverVariable!> {
+ ctor public SolverVariable(androidx.constraintlayout.core.SolverVariable.Type!, String!);
+ ctor public SolverVariable(String!, androidx.constraintlayout.core.SolverVariable.Type!);
+ method public final void addToRow(androidx.constraintlayout.core.ArrayRow!);
+ method public int compareTo(androidx.constraintlayout.core.SolverVariable!);
+ method public String! getName();
+ method public final void removeFromRow(androidx.constraintlayout.core.ArrayRow!);
+ method public void reset();
+ method public void setFinalValue(androidx.constraintlayout.core.LinearSystem!, float);
+ method public void setName(String!);
+ method public void setSynonym(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public void setType(androidx.constraintlayout.core.SolverVariable.Type!, String!);
+ method public final void updateReferencesWithNewDefinition(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.ArrayRow!);
+ field public static final int STRENGTH_BARRIER = 6; // 0x6
+ field public static final int STRENGTH_CENTERING = 7; // 0x7
+ field public static final int STRENGTH_EQUALITY = 5; // 0x5
+ field public static final int STRENGTH_FIXED = 8; // 0x8
+ field public static final int STRENGTH_HIGH = 3; // 0x3
+ field public static final int STRENGTH_HIGHEST = 4; // 0x4
+ field public static final int STRENGTH_LOW = 1; // 0x1
+ field public static final int STRENGTH_MEDIUM = 2; // 0x2
+ field public static final int STRENGTH_NONE = 0; // 0x0
+ field public float computedValue;
+ field public int id;
+ field public boolean inGoal;
+ field public boolean isFinalValue;
+ field public int strength;
+ field public int usageInRowCount;
+ }
+
+ public enum SolverVariable.Type {
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type CONSTANT;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type ERROR;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type SLACK;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type UNKNOWN;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type UNRESTRICTED;
+ }
+
+ public class SolverVariableValues implements androidx.constraintlayout.core.ArrayRow.ArrayRowVariables {
+ method public void add(androidx.constraintlayout.core.SolverVariable!, float, boolean);
+ method public void clear();
+ method public boolean contains(androidx.constraintlayout.core.SolverVariable!);
+ method public void display();
+ method public void divideByAmount(float);
+ method public float get(androidx.constraintlayout.core.SolverVariable!);
+ method public int getCurrentSize();
+ method public androidx.constraintlayout.core.SolverVariable! getVariable(int);
+ method public float getVariableValue(int);
+ method public int indexOf(androidx.constraintlayout.core.SolverVariable!);
+ method public void invert();
+ method public void put(androidx.constraintlayout.core.SolverVariable!, float);
+ method public float remove(androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public int sizeInBytes();
+ method public float use(androidx.constraintlayout.core.ArrayRow!, boolean);
+ field protected final androidx.constraintlayout.core.Cache! mCache;
+ }
+
+}
+
+package androidx.constraintlayout.core.dsl {
+
+ public class Barrier extends androidx.constraintlayout.core.dsl.Helper {
+ ctor public Barrier(String!);
+ ctor public Barrier(String!, String!);
+ method public androidx.constraintlayout.core.dsl.Barrier! addReference(androidx.constraintlayout.core.dsl.Ref!);
+ method public androidx.constraintlayout.core.dsl.Barrier! addReference(String!);
+ method public androidx.constraintlayout.core.dsl.Constraint.Side! getDirection();
+ method public int getMargin();
+ method public String! referencesToString();
+ method public void setDirection(androidx.constraintlayout.core.dsl.Constraint.Side!);
+ method public void setMargin(int);
+ }
+
+ public abstract class Chain extends androidx.constraintlayout.core.dsl.Helper {
+ ctor public Chain(String!);
+ method public androidx.constraintlayout.core.dsl.Chain! addReference(androidx.constraintlayout.core.dsl.Ref!);
+ method public androidx.constraintlayout.core.dsl.Chain! addReference(String!);
+ method public androidx.constraintlayout.core.dsl.Chain.Style! getStyle();
+ method public String! referencesToString();
+ method public void setStyle(androidx.constraintlayout.core.dsl.Chain.Style!);
+ field protected java.util.ArrayList<androidx.constraintlayout.core.dsl.Ref!>! references;
+ field protected static final java.util.Map<androidx.constraintlayout.core.dsl.Chain.Style!,java.lang.String!>! styleMap;
+ }
+
+ public class Chain.Anchor {
+ method public void build(StringBuilder!);
+ method public String! getId();
+ }
+
+ public enum Chain.Style {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Chain.Style PACKED;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Chain.Style SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Chain.Style SPREAD_INSIDE;
+ }
+
+ public class Constraint {
+ ctor public Constraint(String!);
+ method protected void append(StringBuilder!, String!, float);
+ method public String! convertStringArrayToString(String![]!);
+ method public androidx.constraintlayout.core.dsl.Constraint.VAnchor! getBaseline();
+ method public androidx.constraintlayout.core.dsl.Constraint.VAnchor! getBottom();
+ method public float getCircleAngle();
+ method public String! getCircleConstraint();
+ method public int getCircleRadius();
+ method public String! getDimensionRatio();
+ method public int getEditorAbsoluteX();
+ method public int getEditorAbsoluteY();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getEnd();
+ method public int getHeight();
+ method public androidx.constraintlayout.core.dsl.Constraint.Behaviour! getHeightDefault();
+ method public int getHeightMax();
+ method public int getHeightMin();
+ method public float getHeightPercent();
+ method public float getHorizontalBias();
+ method public androidx.constraintlayout.core.dsl.Constraint.ChainMode! getHorizontalChainStyle();
+ method public float getHorizontalWeight();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getLeft();
+ method public String![]! getReferenceIds();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getRight();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getStart();
+ method public androidx.constraintlayout.core.dsl.Constraint.VAnchor! getTop();
+ method public float getVerticalBias();
+ method public androidx.constraintlayout.core.dsl.Constraint.ChainMode! getVerticalChainStyle();
+ method public float getVerticalWeight();
+ method public int getWidth();
+ method public androidx.constraintlayout.core.dsl.Constraint.Behaviour! getWidthDefault();
+ method public int getWidthMax();
+ method public int getWidthMin();
+ method public float getWidthPercent();
+ method public boolean isConstrainedHeight();
+ method public boolean isConstrainedWidth();
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void setCircleAngle(float);
+ method public void setCircleConstraint(String!);
+ method public void setCircleRadius(int);
+ method public void setConstrainedHeight(boolean);
+ method public void setConstrainedWidth(boolean);
+ method public void setDimensionRatio(String!);
+ method public void setEditorAbsoluteX(int);
+ method public void setEditorAbsoluteY(int);
+ method public void setHeight(int);
+ method public void setHeightDefault(androidx.constraintlayout.core.dsl.Constraint.Behaviour!);
+ method public void setHeightMax(int);
+ method public void setHeightMin(int);
+ method public void setHeightPercent(float);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalChainStyle(androidx.constraintlayout.core.dsl.Constraint.ChainMode!);
+ method public void setHorizontalWeight(float);
+ method public void setReferenceIds(String![]!);
+ method public void setVerticalBias(float);
+ method public void setVerticalChainStyle(androidx.constraintlayout.core.dsl.Constraint.ChainMode!);
+ method public void setVerticalWeight(float);
+ method public void setWidth(int);
+ method public void setWidthDefault(androidx.constraintlayout.core.dsl.Constraint.Behaviour!);
+ method public void setWidthMax(int);
+ method public void setWidthMin(int);
+ method public void setWidthPercent(float);
+ field public static final androidx.constraintlayout.core.dsl.Constraint! PARENT;
+ }
+
+ public class Constraint.Anchor {
+ method public void build(StringBuilder!);
+ method public String! getId();
+ }
+
+ public enum Constraint.Behaviour {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour PERCENT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour RATIO;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour RESOLVED;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour WRAP;
+ }
+
+ public enum Constraint.ChainMode {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.ChainMode PACKED;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.ChainMode SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.ChainMode SPREAD_INSIDE;
+ }
+
+ public class Constraint.HAnchor extends androidx.constraintlayout.core.dsl.Constraint.Anchor {
+ }
+
+ public enum Constraint.HSide {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide START;
+ }
+
+ public enum Constraint.Side {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side TOP;
+ }
+
+ public class Constraint.VAnchor extends androidx.constraintlayout.core.dsl.Constraint.Anchor {
+ }
+
+ public enum Constraint.VSide {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.VSide BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.VSide BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.VSide TOP;
+ }
+
+ public class ConstraintSet {
+ ctor public ConstraintSet(String!);
+ method public void add(androidx.constraintlayout.core.dsl.Constraint!);
+ method public void add(androidx.constraintlayout.core.dsl.Helper!);
+ }
+
+ public abstract class Guideline extends androidx.constraintlayout.core.dsl.Helper {
+ method public int getEnd();
+ method public float getPercent();
+ method public int getStart();
+ method public void setEnd(int);
+ method public void setPercent(float);
+ method public void setStart(int);
+ }
+
+ public class HChain extends androidx.constraintlayout.core.dsl.Chain {
+ ctor public HChain(String!);
+ ctor public HChain(String!, String!);
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getEnd();
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getLeft();
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getRight();
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getStart();
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ }
+
+ public class HChain.HAnchor extends androidx.constraintlayout.core.dsl.Chain.Anchor {
+ }
+
+ public class Helper {
+ ctor public Helper(String!, androidx.constraintlayout.core.dsl.Helper.HelperType!);
+ ctor public Helper(String!, androidx.constraintlayout.core.dsl.Helper.HelperType!, String!);
+ method public void append(java.util.Map<java.lang.String!,java.lang.String!>!, StringBuilder!);
+ method public java.util.Map<java.lang.String!,java.lang.String!>! convertConfigToMap();
+ method public String! getConfig();
+ method public String! getId();
+ method public androidx.constraintlayout.core.dsl.Helper.HelperType! getType();
+ method public static void main(String![]!);
+ field protected String! config;
+ field protected java.util.Map<java.lang.String!,java.lang.String!>! configMap;
+ field protected final String! name;
+ field protected static final java.util.Map<androidx.constraintlayout.core.dsl.Constraint.Side!,java.lang.String!>! sideMap;
+ field protected androidx.constraintlayout.core.dsl.Helper.HelperType! type;
+ field protected static final java.util.Map<androidx.constraintlayout.core.dsl.Helper.Type!,java.lang.String!>! typeMap;
+ }
+
+ public static final class Helper.HelperType {
+ ctor public Helper.HelperType(String!);
+ }
+
+ public enum Helper.Type {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type BARRIER;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type HORIZONTAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type HORIZONTAL_GUIDELINE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type VERTICAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type VERTICAL_GUIDELINE;
+ }
+
+ public class KeyAttribute extends androidx.constraintlayout.core.dsl.Keys {
+ ctor public KeyAttribute(int, String!);
+ method protected void attributesToString(StringBuilder!);
+ method public float getAlpha();
+ method public androidx.constraintlayout.core.dsl.KeyAttribute.Fit! getCurveFit();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public float getRotation();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public String! getTarget();
+ method public String! getTransitionEasing();
+ method public float getTransitionPathRotate();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public androidx.constraintlayout.core.dsl.KeyAttribute.Visibility! getVisibility();
+ method public void setAlpha(float);
+ method public void setCurveFit(androidx.constraintlayout.core.dsl.KeyAttribute.Fit!);
+ method public void setPivotX(float);
+ method public void setPivotY(float);
+ method public void setRotation(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTarget(String!);
+ method public void setTransitionEasing(String!);
+ method public void setTransitionPathRotate(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ method public void setVisibility(androidx.constraintlayout.core.dsl.KeyAttribute.Visibility!);
+ field protected String! TYPE;
+ }
+
+ public enum KeyAttribute.Fit {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Fit LINEAR;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Fit SPLINE;
+ }
+
+ public enum KeyAttribute.Visibility {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Visibility GONE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Visibility INVISIBLE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Visibility VISIBLE;
+ }
+
+ public class KeyAttributes extends androidx.constraintlayout.core.dsl.Keys {
+ method protected void attributesToString(StringBuilder!);
+ method public float[]! getAlpha();
+ method public androidx.constraintlayout.core.dsl.KeyAttributes.Fit! getCurveFit();
+ method public float[]! getPivotX();
+ method public float[]! getPivotY();
+ method public float[]! getRotation();
+ method public float[]! getRotationX();
+ method public float[]! getRotationY();
+ method public float[]! getScaleX();
+ method public float[]! getScaleY();
+ method public String![]! getTarget();
+ method public String! getTransitionEasing();
+ method public float[]! getTransitionPathRotate();
+ method public float[]! getTranslationX();
+ method public float[]! getTranslationY();
+ method public float[]! getTranslationZ();
+ method public androidx.constraintlayout.core.dsl.KeyAttributes.Visibility![]! getVisibility();
+ method public void setAlpha(float...!);
+ method public void setCurveFit(androidx.constraintlayout.core.dsl.KeyAttributes.Fit!);
+ method public void setPivotX(float...!);
+ method public void setPivotY(float...!);
+ method public void setRotation(float...!);
+ method public void setRotationX(float...!);
+ method public void setRotationY(float...!);
+ method public void setScaleX(float[]!);
+ method public void setScaleY(float[]!);
+ method public void setTarget(String![]!);
+ method public void setTransitionEasing(String!);
+ method public void setTransitionPathRotate(float...!);
+ method public void setTranslationX(float[]!);
+ method public void setTranslationY(float[]!);
+ method public void setTranslationZ(float[]!);
+ method public void setVisibility(androidx.constraintlayout.core.dsl.KeyAttributes.Visibility!...!);
+ field protected String! TYPE;
+ }
+
+ public enum KeyAttributes.Fit {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Fit LINEAR;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Fit SPLINE;
+ }
+
+ public enum KeyAttributes.Visibility {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Visibility GONE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Visibility INVISIBLE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Visibility VISIBLE;
+ }
+
+ public class KeyCycle extends androidx.constraintlayout.core.dsl.KeyAttribute {
+ method public float getOffset();
+ method public float getPeriod();
+ method public float getPhase();
+ method public androidx.constraintlayout.core.dsl.KeyCycle.Wave! getShape();
+ method public void setOffset(float);
+ method public void setPeriod(float);
+ method public void setPhase(float);
+ method public void setShape(androidx.constraintlayout.core.dsl.KeyCycle.Wave!);
+ }
+
+ public enum KeyCycle.Wave {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave COS;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave REVERSE_SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave SIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave SQUARE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave TRIANGLE;
+ }
+
+ public class KeyCycles extends androidx.constraintlayout.core.dsl.KeyAttributes {
+ method public float[]! getWaveOffset();
+ method public float[]! getWavePeriod();
+ method public float[]! getWavePhase();
+ method public androidx.constraintlayout.core.dsl.KeyCycles.Wave! getWaveShape();
+ method public void setWaveOffset(float...!);
+ method public void setWavePeriod(float...!);
+ method public void setWavePhase(float...!);
+ method public void setWaveShape(androidx.constraintlayout.core.dsl.KeyCycles.Wave!);
+ }
+
+ public enum KeyCycles.Wave {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave COS;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave REVERSE_SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave SIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave SQUARE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave TRIANGLE;
+ }
+
+ public class KeyFrames {
+ ctor public KeyFrames();
+ method public void add(androidx.constraintlayout.core.dsl.Keys!);
+ }
+
+ public class KeyPosition extends androidx.constraintlayout.core.dsl.Keys {
+ ctor public KeyPosition(String!, int);
+ method public int getFrames();
+ method public float getPercentHeight();
+ method public float getPercentWidth();
+ method public float getPercentX();
+ method public float getPercentY();
+ method public androidx.constraintlayout.core.dsl.KeyPosition.Type! getPositionType();
+ method public String! getTarget();
+ method public String! getTransitionEasing();
+ method public void setFrames(int);
+ method public void setPercentHeight(float);
+ method public void setPercentWidth(float);
+ method public void setPercentX(float);
+ method public void setPercentY(float);
+ method public void setPositionType(androidx.constraintlayout.core.dsl.KeyPosition.Type!);
+ method public void setTarget(String!);
+ method public void setTransitionEasing(String!);
+ }
+
+ public enum KeyPosition.Type {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPosition.Type CARTESIAN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPosition.Type PATH;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPosition.Type SCREEN;
+ }
+
+ public class KeyPositions extends androidx.constraintlayout.core.dsl.Keys {
+ ctor public KeyPositions(int, java.lang.String!...!);
+ method public int[]! getFrames();
+ method public float[]! getPercentHeight();
+ method public float[]! getPercentWidth();
+ method public float[]! getPercentX();
+ method public float[]! getPercentY();
+ method public androidx.constraintlayout.core.dsl.KeyPositions.Type! getPositionType();
+ method public String![]! getTarget();
+ method public String! getTransitionEasing();
+ method public void setFrames(int...!);
+ method public void setPercentHeight(float...!);
+ method public void setPercentWidth(float...!);
+ method public void setPercentX(float...!);
+ method public void setPercentY(float...!);
+ method public void setPositionType(androidx.constraintlayout.core.dsl.KeyPositions.Type!);
+ method public void setTransitionEasing(String!);
+ }
+
+ public enum KeyPositions.Type {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPositions.Type CARTESIAN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPositions.Type PATH;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPositions.Type SCREEN;
+ }
+
+ public class Keys {
+ ctor public Keys();
+ method protected void append(StringBuilder!, String!, float);
+ method protected void append(StringBuilder!, String!, float[]!);
+ method protected void append(StringBuilder!, String!, int);
+ method protected void append(StringBuilder!, String!, String!);
+ method protected void append(StringBuilder!, String!, String![]!);
+ method protected String! unpack(String![]!);
+ }
+
+ public class MotionScene {
+ ctor public MotionScene();
+ method public void addConstraintSet(androidx.constraintlayout.core.dsl.ConstraintSet!);
+ method public void addTransition(androidx.constraintlayout.core.dsl.Transition!);
+ }
+
+ public class OnSwipe {
+ ctor public OnSwipe();
+ ctor public OnSwipe(String!, androidx.constraintlayout.core.dsl.OnSwipe.Side!, androidx.constraintlayout.core.dsl.OnSwipe.Drag!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Mode! getAutoCompleteMode();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Drag! getDragDirection();
+ method public float getDragScale();
+ method public float getDragThreshold();
+ method public String! getLimitBoundsTo();
+ method public float getMaxAcceleration();
+ method public float getMaxVelocity();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.TouchUp! getOnTouchUp();
+ method public String! getRotationCenterId();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Boundary! getSpringBoundary();
+ method public float getSpringDamping();
+ method public float getSpringMass();
+ method public float getSpringStiffness();
+ method public float getSpringStopThreshold();
+ method public String! getTouchAnchorId();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Side! getTouchAnchorSide();
+ method public void setAutoCompleteMode(androidx.constraintlayout.core.dsl.OnSwipe.Mode!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setDragDirection(androidx.constraintlayout.core.dsl.OnSwipe.Drag!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setDragScale(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setDragThreshold(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setLimitBoundsTo(String!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setMaxAcceleration(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setMaxVelocity(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setOnTouchUp(androidx.constraintlayout.core.dsl.OnSwipe.TouchUp!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setRotateCenter(String!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringBoundary(androidx.constraintlayout.core.dsl.OnSwipe.Boundary!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringDamping(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringMass(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringStiffness(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringStopThreshold(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setTouchAnchorId(String!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setTouchAnchorSide(androidx.constraintlayout.core.dsl.OnSwipe.Side!);
+ field public static final int FLAG_DISABLE_POST_SCROLL = 1; // 0x1
+ field public static final int FLAG_DISABLE_SCROLL = 2; // 0x2
+ }
+
+ public enum OnSwipe.Boundary {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary BOUNCE_BOTH;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary BOUNCE_END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary BOUNCE_START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary OVERSHOOT;
+ }
+
+ public enum OnSwipe.Drag {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag ANTICLOCKWISE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag CLOCKWISE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag DOWN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag UP;
+ }
+
+ public enum OnSwipe.Mode {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Mode SPRING;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Mode VELOCITY;
+ }
+
+ public enum OnSwipe.Side {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side MIDDLE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side TOP;
+ }
+
+ public enum OnSwipe.TouchUp {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp AUTOCOMPLETE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp DECELERATE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp DECELERATE_COMPLETE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp NEVER_COMPLETE_END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp NEVER_COMPLETE_START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp STOP;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp TO_END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp TO_START;
+ }
+
+ public class Ref {
+ method public static void addStringToReferences(String!, java.util.ArrayList<androidx.constraintlayout.core.dsl.Ref!>!);
+ method public String! getId();
+ method public float getPostMargin();
+ method public float getPreMargin();
+ method public float getWeight();
+ method public static float parseFloat(Object!);
+ method public static androidx.constraintlayout.core.dsl.Ref! parseStringToRef(String!);
+ method public void setId(String!);
+ method public void setPostMargin(float);
+ method public void setPreMargin(float);
+ method public void setWeight(float);
+ }
+
+ public class Transition {
+ ctor public Transition(String!, String!);
+ ctor public Transition(String!, String!, String!);
+ method public String! getId();
+ method public void setDuration(int);
+ method public void setFrom(String!);
+ method public void setId(String!);
+ method public void setKeyFrames(androidx.constraintlayout.core.dsl.Keys!);
+ method public void setOnSwipe(androidx.constraintlayout.core.dsl.OnSwipe!);
+ method public void setStagger(float);
+ method public void setTo(String!);
+ }
+
+ public class VChain extends androidx.constraintlayout.core.dsl.Chain {
+ ctor public VChain(String!);
+ ctor public VChain(String!, String!);
+ method public androidx.constraintlayout.core.dsl.VChain.VAnchor! getBaseline();
+ method public androidx.constraintlayout.core.dsl.VChain.VAnchor! getBottom();
+ method public androidx.constraintlayout.core.dsl.VChain.VAnchor! getTop();
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ }
+
+ public class VChain.VAnchor extends androidx.constraintlayout.core.dsl.Chain.Anchor {
+ }
+
+ public class VGuideline extends androidx.constraintlayout.core.dsl.Guideline {
+ ctor public VGuideline(String!);
+ ctor public VGuideline(String!, String!);
+ }
+
+}
+
+package androidx.constraintlayout.core.motion {
+
+ public class CustomAttribute {
+ ctor public CustomAttribute(androidx.constraintlayout.core.motion.CustomAttribute!, Object!);
+ ctor public CustomAttribute(String!, androidx.constraintlayout.core.motion.CustomAttribute.AttributeType!);
+ ctor public CustomAttribute(String!, androidx.constraintlayout.core.motion.CustomAttribute.AttributeType!, Object!, boolean);
+ method public boolean diff(androidx.constraintlayout.core.motion.CustomAttribute!);
+ method public androidx.constraintlayout.core.motion.CustomAttribute.AttributeType! getType();
+ method public float getValueToInterpolate();
+ method public void getValuesToInterpolate(float[]!);
+ method public static int hsvToRgb(float, float, float);
+ method public boolean isContinuous();
+ method public int numberOfInterpolatedValues();
+ method public void setColorValue(int);
+ method public void setFloatValue(float);
+ method public void setIntValue(int);
+ method public void setStringValue(String!);
+ method public void setValue(float[]!);
+ method public void setValue(Object!);
+ }
+
+ public enum CustomAttribute.AttributeType {
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType BOOLEAN_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType COLOR_DRAWABLE_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType COLOR_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType DIMENSION_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType FLOAT_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType INT_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType REFERENCE_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType STRING_TYPE;
+ }
+
+ public class CustomVariable {
+ ctor public CustomVariable(androidx.constraintlayout.core.motion.CustomVariable!);
+ ctor public CustomVariable(androidx.constraintlayout.core.motion.CustomVariable!, Object!);
+ ctor public CustomVariable(String!, int);
+ ctor public CustomVariable(String!, int, boolean);
+ ctor public CustomVariable(String!, int, float);
+ ctor public CustomVariable(String!, int, int);
+ ctor public CustomVariable(String!, int, Object!);
+ ctor public CustomVariable(String!, int, String!);
+ method public void applyToWidget(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public static String! colorString(int);
+ method public androidx.constraintlayout.core.motion.CustomVariable! copy();
+ method public boolean diff(androidx.constraintlayout.core.motion.CustomVariable!);
+ method public boolean getBooleanValue();
+ method public int getColorValue();
+ method public float getFloatValue();
+ method public int getIntegerValue();
+ method public int getInterpolatedColor(float[]!);
+ method public String! getName();
+ method public String! getStringValue();
+ method public int getType();
+ method public float getValueToInterpolate();
+ method public void getValuesToInterpolate(float[]!);
+ method public static int hsvToRgb(float, float, float);
+ method public boolean isContinuous();
+ method public int numberOfInterpolatedValues();
+ method public static int rgbaTocColor(float, float, float, float);
+ method public void setBooleanValue(boolean);
+ method public void setFloatValue(float);
+ method public void setIntValue(int);
+ method public void setInterpolatedValue(androidx.constraintlayout.core.motion.MotionWidget!, float[]!);
+ method public void setStringValue(String!);
+ method public void setValue(float[]!);
+ method public void setValue(Object!);
+ }
+
+ public class Motion implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public Motion(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void addKey(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public int buildKeyFrames(float[]!, int[]!, int[]!);
+ method public void buildPath(float[]!, int);
+ method public void buildRect(float, float[]!, int);
+ method public String! getAnimateRelativeTo();
+ method public void getCenter(double, float[]!, float[]!);
+ method public float getCenterX();
+ method public float getCenterY();
+ method public void getDpDt(float, float, float, float[]!);
+ method public int getDrawPath();
+ method public float getFinalHeight();
+ method public float getFinalWidth();
+ method public float getFinalX();
+ method public float getFinalY();
+ method public int getId(String!);
+ method public androidx.constraintlayout.core.motion.MotionPaths! getKeyFrame(int);
+ method public int getKeyFrameInfo(int, int[]!);
+ method public int getKeyFramePositions(int[]!, float[]!);
+ method public float getMotionStagger();
+ method public float getStartHeight();
+ method public float getStartWidth();
+ method public float getStartX();
+ method public float getStartY();
+ method public int getTransformPivotTarget();
+ method public androidx.constraintlayout.core.motion.MotionWidget! getView();
+ method public boolean interpolate(androidx.constraintlayout.core.motion.MotionWidget!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ method public void setDrawPath(int);
+ method public void setEnd(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void setIdString(String!);
+ method public void setPathMotionArc(int);
+ method public void setStaggerOffset(float);
+ method public void setStaggerScale(float);
+ method public void setStart(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void setStartState(androidx.constraintlayout.core.motion.utils.ViewState!, androidx.constraintlayout.core.motion.MotionWidget!, int, int, int);
+ method public void setTransformPivotTarget(int);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public void setView(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void setup(int, int, float, long);
+ method public void setupRelative(androidx.constraintlayout.core.motion.Motion!);
+ field public static final int DRAW_PATH_AS_CONFIGURED = 4; // 0x4
+ field public static final int DRAW_PATH_BASIC = 1; // 0x1
+ field public static final int DRAW_PATH_CARTESIAN = 3; // 0x3
+ field public static final int DRAW_PATH_NONE = 0; // 0x0
+ field public static final int DRAW_PATH_RECTANGLE = 5; // 0x5
+ field public static final int DRAW_PATH_RELATIVE = 2; // 0x2
+ field public static final int DRAW_PATH_SCREEN = 6; // 0x6
+ field public static final int HORIZONTAL_PATH_X = 2; // 0x2
+ field public static final int HORIZONTAL_PATH_Y = 3; // 0x3
+ field public static final int PATH_PERCENT = 0; // 0x0
+ field public static final int PATH_PERPENDICULAR = 1; // 0x1
+ field public static final int ROTATION_LEFT = 2; // 0x2
+ field public static final int ROTATION_RIGHT = 1; // 0x1
+ field public static final int VERTICAL_PATH_X = 4; // 0x4
+ field public static final int VERTICAL_PATH_Y = 5; // 0x5
+ field public String! mId;
+ }
+
+ public class MotionPaths implements java.lang.Comparable<androidx.constraintlayout.core.motion.MotionPaths!> {
+ ctor public MotionPaths();
+ ctor public MotionPaths(int, int, androidx.constraintlayout.core.motion.key.MotionKeyPosition!, androidx.constraintlayout.core.motion.MotionPaths!, androidx.constraintlayout.core.motion.MotionPaths!);
+ method public void applyParameters(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public int compareTo(androidx.constraintlayout.core.motion.MotionPaths!);
+ method public void configureRelativeTo(androidx.constraintlayout.core.motion.Motion!);
+ method public void setupRelative(androidx.constraintlayout.core.motion.Motion!, androidx.constraintlayout.core.motion.MotionPaths!);
+ field public static final int CARTESIAN = 0; // 0x0
+ field public static final boolean DEBUG = false;
+ field public static final boolean OLD_WAY = false;
+ field public static final int PERPENDICULAR = 1; // 0x1
+ field public static final int SCREEN = 2; // 0x2
+ field public static final String TAG = "MotionPaths";
+ field public String! mId;
+ }
+
+ public class MotionWidget implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public MotionWidget();
+ ctor public MotionWidget(androidx.constraintlayout.core.state.WidgetFrame!);
+ method public androidx.constraintlayout.core.motion.MotionWidget! findViewById(int);
+ method public float getAlpha();
+ method public int getBottom();
+ method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
+ method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
+ method public int getHeight();
+ method public int getId(String!);
+ method public int getLeft();
+ method public String! getName();
+ method public androidx.constraintlayout.core.motion.MotionWidget! getParent();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public int getRight();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public int getTop();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public float getValueAttributes(int);
+ method public int getVisibility();
+ method public androidx.constraintlayout.core.state.WidgetFrame! getWidgetFrame();
+ method public int getWidth();
+ method public int getX();
+ method public int getY();
+ method public void layout(int, int, int, int);
+ method public void setBounds(int, int, int, int);
+ method public void setCustomAttribute(String!, int, boolean);
+ method public void setCustomAttribute(String!, int, float);
+ method public void setCustomAttribute(String!, int, int);
+ method public void setCustomAttribute(String!, int, String!);
+ method public void setInterpolatedValue(androidx.constraintlayout.core.motion.CustomAttribute!, float[]!);
+ method public void setPivotX(float);
+ method public void setPivotY(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public boolean setValueAttributes(int, float);
+ method public boolean setValueMotion(int, float);
+ method public boolean setValueMotion(int, int);
+ method public boolean setValueMotion(int, String!);
+ method public void setVisibility(int);
+ method public void updateMotion(androidx.constraintlayout.core.motion.utils.TypedValues!);
+ field public static final int FILL_PARENT = -1; // 0xffffffff
+ field public static final int GONE_UNSET = -2147483648; // 0x80000000
+ field public static final int INVISIBLE = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int MATCH_PARENT = -1; // 0xffffffff
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int ROTATE_LEFT_OF_PORTRATE = 4; // 0x4
+ field public static final int ROTATE_NONE = 0; // 0x0
+ field public static final int ROTATE_PORTRATE_OF_LEFT = 2; // 0x2
+ field public static final int ROTATE_PORTRATE_OF_RIGHT = 1; // 0x1
+ field public static final int ROTATE_RIGHT_OF_PORTRATE = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int VISIBILITY_MODE_IGNORE = 1; // 0x1
+ field public static final int VISIBILITY_MODE_NORMAL = 0; // 0x0
+ field public static final int VISIBLE = 4; // 0x4
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ }
+
+ public static class MotionWidget.Motion {
+ ctor public MotionWidget.Motion();
+ field public int mAnimateCircleAngleTo;
+ field public String! mAnimateRelativeTo;
+ field public int mDrawPath;
+ field public float mMotionStagger;
+ field public int mPathMotionArc;
+ field public float mPathRotate;
+ field public int mPolarRelativeTo;
+ field public int mQuantizeInterpolatorID;
+ field public String! mQuantizeInterpolatorString;
+ field public int mQuantizeInterpolatorType;
+ field public float mQuantizeMotionPhase;
+ field public int mQuantizeMotionSteps;
+ field public String! mTransitionEasing;
+ }
+
+ public static class MotionWidget.PropertySet {
+ ctor public MotionWidget.PropertySet();
+ field public float alpha;
+ field public float mProgress;
+ field public int mVisibilityMode;
+ field public int visibility;
+ }
+
+}
+
+package androidx.constraintlayout.core.motion.key {
+
+ public class MotionConstraintSet {
+ ctor public MotionConstraintSet();
+ field public static final int ROTATE_LEFT_OF_PORTRATE = 4; // 0x4
+ field public static final int ROTATE_NONE = 0; // 0x0
+ field public static final int ROTATE_PORTRATE_OF_LEFT = 2; // 0x2
+ field public static final int ROTATE_PORTRATE_OF_RIGHT = 1; // 0x1
+ field public static final int ROTATE_RIGHT_OF_PORTRATE = 3; // 0x3
+ field public String! mIdString;
+ field public int mRotate;
+ }
+
+ public abstract class MotionKey implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public MotionKey();
+ method public abstract void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public abstract androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public androidx.constraintlayout.core.motion.key.MotionKey! copy(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public abstract void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getFramePosition();
+ method public void setCustomAttribute(String!, int, boolean);
+ method public void setCustomAttribute(String!, int, float);
+ method public void setCustomAttribute(String!, int, int);
+ method public void setCustomAttribute(String!, int, String!);
+ method public void setFramePosition(int);
+ method public void setInterpolation(java.util.HashMap<java.lang.String!,java.lang.Integer!>!);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! setViewId(int);
+ field public static final String ALPHA = "alpha";
+ field public static final String CUSTOM = "CUSTOM";
+ field public static final String ELEVATION = "elevation";
+ field public static final String ROTATION = "rotationZ";
+ field public static final String ROTATION_X = "rotationX";
+ field public static final String SCALE_X = "scaleX";
+ field public static final String SCALE_Y = "scaleY";
+ field public static final String TRANSITION_PATH_ROTATE = "transitionPathRotate";
+ field public static final String TRANSLATION_X = "translationX";
+ field public static final String TRANSLATION_Y = "translationY";
+ field public static int UNSET;
+ field public static final String VISIBILITY = "visibility";
+ field public java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.CustomVariable!>! mCustom;
+ field public int mFramePosition;
+ field public int mType;
+ }
+
+ public class MotionKeyAttributes extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyAttributes();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getCurveFit();
+ method public int getId(String!);
+ method public void printAttributes();
+ field public static final int KEY_TYPE = 1; // 0x1
+ }
+
+ public class MotionKeyCycle extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyCycle();
+ method public void addCycleValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void dump();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ method public float getValue(String!);
+ method public void printAttributes();
+ field public static final int KEY_TYPE = 4; // 0x4
+ field public static final int SHAPE_BOUNCE = 6; // 0x6
+ field public static final int SHAPE_COS_WAVE = 5; // 0x5
+ field public static final int SHAPE_REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SHAPE_SAW_WAVE = 3; // 0x3
+ field public static final int SHAPE_SIN_WAVE = 0; // 0x0
+ field public static final int SHAPE_SQUARE_WAVE = 1; // 0x1
+ field public static final int SHAPE_TRIANGLE_WAVE = 2; // 0x2
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_PHASE = "wavePhase";
+ field public static final String WAVE_SHAPE = "waveShape";
+ }
+
+ public class MotionKeyPosition extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyPosition();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ method public boolean intersects(int, int, androidx.constraintlayout.core.motion.utils.FloatRect!, androidx.constraintlayout.core.motion.utils.FloatRect!, float, float);
+ method public void positionAttributes(androidx.constraintlayout.core.motion.MotionWidget!, androidx.constraintlayout.core.motion.utils.FloatRect!, androidx.constraintlayout.core.motion.utils.FloatRect!, float, float, String![]!, float[]!);
+ field protected static final float SELECTION_SLOPE = 20.0f;
+ field public static final int TYPE_CARTESIAN = 0; // 0x0
+ field public static final int TYPE_PATH = 1; // 0x1
+ field public static final int TYPE_SCREEN = 2; // 0x2
+ field public float mAltPercentX;
+ field public float mAltPercentY;
+ field public int mCurveFit;
+ field public int mDrawPath;
+ field public int mPathMotionArc;
+ field public float mPercentHeight;
+ field public float mPercentWidth;
+ field public float mPercentX;
+ field public float mPercentY;
+ field public int mPositionType;
+ field public String! mTransitionEasing;
+ }
+
+ public class MotionKeyTimeCycle extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyTimeCycle();
+ method public void addTimeValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public androidx.constraintlayout.core.motion.key.MotionKeyTimeCycle! copy(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ field public static final int KEY_TYPE = 3; // 0x3
+ }
+
+ public class MotionKeyTrigger extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyTrigger();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void conditionallyFire(float, androidx.constraintlayout.core.motion.MotionWidget!);
+ method public androidx.constraintlayout.core.motion.key.MotionKeyTrigger! copy(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ field public static final String CROSS = "CROSS";
+ field public static final int KEY_TYPE = 5; // 0x5
+ field public static final String NEGATIVE_CROSS = "negativeCross";
+ field public static final String POSITIVE_CROSS = "positiveCross";
+ field public static final String POST_LAYOUT = "postLayout";
+ field public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
+ field public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
+ field public static final String TRIGGER_ID = "triggerID";
+ field public static final String TRIGGER_RECEIVER = "triggerReceiver";
+ field public static final String TRIGGER_SLACK = "triggerSlack";
+ field public static final int TYPE_CROSS = 312; // 0x138
+ field public static final int TYPE_NEGATIVE_CROSS = 310; // 0x136
+ field public static final int TYPE_POSITIVE_CROSS = 309; // 0x135
+ field public static final int TYPE_POST_LAYOUT = 304; // 0x130
+ field public static final int TYPE_TRIGGER_COLLISION_ID = 307; // 0x133
+ field public static final int TYPE_TRIGGER_COLLISION_VIEW = 306; // 0x132
+ field public static final int TYPE_TRIGGER_ID = 308; // 0x134
+ field public static final int TYPE_TRIGGER_RECEIVER = 311; // 0x137
+ field public static final int TYPE_TRIGGER_SLACK = 305; // 0x131
+ field public static final int TYPE_VIEW_TRANSITION_ON_CROSS = 301; // 0x12d
+ field public static final int TYPE_VIEW_TRANSITION_ON_NEGATIVE_CROSS = 303; // 0x12f
+ field public static final int TYPE_VIEW_TRANSITION_ON_POSITIVE_CROSS = 302; // 0x12e
+ field public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
+ field public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
+ field public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
+ }
+
+}
+
+package androidx.constraintlayout.core.motion.parse {
+
+ public class KeyParser {
+ ctor public KeyParser();
+ method public static void main(String![]!);
+ method public static androidx.constraintlayout.core.motion.utils.TypedBundle! parseAttributes(String!);
+ }
+
+}
+
+package androidx.constraintlayout.core.motion.utils {
+
+ public class ArcCurveFit extends androidx.constraintlayout.core.motion.utils.CurveFit {
+ ctor public ArcCurveFit(int[]!, double[]!, double[]![]!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getSlope(double, double[]!);
+ method public double getSlope(double, int);
+ method public double[]! getTimePoints();
+ field public static final int ARC_ABOVE = 5; // 0x5
+ field public static final int ARC_BELOW = 4; // 0x4
+ field public static final int ARC_START_FLIP = 3; // 0x3
+ field public static final int ARC_START_HORIZONTAL = 2; // 0x2
+ field public static final int ARC_START_LINEAR = 0; // 0x0
+ field public static final int ARC_START_VERTICAL = 1; // 0x1
+ }
+
+ public abstract class CurveFit {
+ ctor public CurveFit();
+ method public static androidx.constraintlayout.core.motion.utils.CurveFit! get(int, double[]!, double[]![]!);
+ method public static androidx.constraintlayout.core.motion.utils.CurveFit! getArc(int[]!, double[]!, double[]![]!);
+ method public abstract void getPos(double, double[]!);
+ method public abstract void getPos(double, float[]!);
+ method public abstract double getPos(double, int);
+ method public abstract void getSlope(double, double[]!);
+ method public abstract double getSlope(double, int);
+ method public abstract double[]! getTimePoints();
+ field public static final int CONSTANT = 2; // 0x2
+ field public static final int LINEAR = 1; // 0x1
+ field public static final int SPLINE = 0; // 0x0
+ }
+
+ public interface DifferentialInterpolator {
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ }
+
+ public class Easing {
+ ctor public Easing();
+ method public double get(double);
+ method public double getDiff(double);
+ method public static androidx.constraintlayout.core.motion.utils.Easing! getInterpolator(String!);
+ field public static String![]! NAMED_EASING;
+ }
+
+ public class FloatRect {
+ ctor public FloatRect();
+ method public final float centerX();
+ method public final float centerY();
+ field public float bottom;
+ field public float left;
+ field public float right;
+ field public float top;
+ }
+
+ public class HyperSpline {
+ ctor public HyperSpline();
+ ctor public HyperSpline(double[]![]!);
+ method public double approxLength(androidx.constraintlayout.core.motion.utils.HyperSpline.Cubic![]!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getVelocity(double, double[]!);
+ method public void setup(double[]![]!);
+ }
+
+ public static class HyperSpline.Cubic {
+ ctor public HyperSpline.Cubic(double, double, double, double);
+ method public double eval(double);
+ method public double vel(double);
+ }
+
+ public class KeyCache {
+ ctor public KeyCache();
+ method public float getFloatValue(Object!, String!, int);
+ method public void setFloatValue(Object!, String!, int, float);
+ }
+
+ public abstract class KeyCycleOscillator {
+ ctor public KeyCycleOscillator();
+ method public float get(float);
+ method public androidx.constraintlayout.core.motion.utils.CurveFit! getCurveFit();
+ method public float getSlope(float);
+ method public static androidx.constraintlayout.core.motion.utils.KeyCycleOscillator! makeWidgetCycle(String!);
+ method protected void setCustom(Object!);
+ method public void setPoint(int, int, String!, int, float, float, float, float);
+ method public void setPoint(int, int, String!, int, float, float, float, float, Object!);
+ method public void setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float);
+ method public void setType(String!);
+ method public void setup(float);
+ method public boolean variesByPath();
+ field public int mVariesBy;
+ }
+
+ public static class KeyCycleOscillator.PathRotateSet extends androidx.constraintlayout.core.motion.utils.KeyCycleOscillator {
+ ctor public KeyCycleOscillator.PathRotateSet(String!);
+ method public void setPathRotate(androidx.constraintlayout.core.motion.MotionWidget!, float, double, double);
+ }
+
+ public class KeyFrameArray {
+ ctor public KeyFrameArray();
+ }
+
+ public static class KeyFrameArray.CustomArray {
+ ctor public KeyFrameArray.CustomArray();
+ method public void append(int, androidx.constraintlayout.core.motion.CustomAttribute!);
+ method public void clear();
+ method public void dump();
+ method public int keyAt(int);
+ method public void remove(int);
+ method public int size();
+ method public androidx.constraintlayout.core.motion.CustomAttribute! valueAt(int);
+ }
+
+ public static class KeyFrameArray.CustomVar {
+ ctor public KeyFrameArray.CustomVar();
+ method public void append(int, androidx.constraintlayout.core.motion.CustomVariable!);
+ method public void clear();
+ method public void dump();
+ method public int keyAt(int);
+ method public void remove(int);
+ method public int size();
+ method public androidx.constraintlayout.core.motion.CustomVariable! valueAt(int);
+ }
+
+ public class LinearCurveFit extends androidx.constraintlayout.core.motion.utils.CurveFit {
+ ctor public LinearCurveFit(double[]!, double[]![]!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getSlope(double, double[]!);
+ method public double getSlope(double, int);
+ method public double[]! getTimePoints();
+ }
+
+ public class MonotonicCurveFit extends androidx.constraintlayout.core.motion.utils.CurveFit {
+ ctor public MonotonicCurveFit(double[]!, double[]![]!);
+ method public static androidx.constraintlayout.core.motion.utils.MonotonicCurveFit! buildWave(String!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getSlope(double, double[]!);
+ method public double getSlope(double, int);
+ method public double[]! getTimePoints();
+ }
+
+ public class Oscillator {
+ ctor public Oscillator();
+ method public void addPoint(double, float);
+ method public double getSlope(double, double, double);
+ method public double getValue(double, double);
+ method public void normalize();
+ method public void setType(int, String!);
+ field public static final int BOUNCE = 6; // 0x6
+ field public static final int COS_WAVE = 5; // 0x5
+ field public static final int CUSTOM = 7; // 0x7
+ field public static final int REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SAW_WAVE = 3; // 0x3
+ field public static final int SIN_WAVE = 0; // 0x0
+ field public static final int SQUARE_WAVE = 1; // 0x1
+ field public static String! TAG;
+ field public static final int TRIANGLE_WAVE = 2; // 0x2
+ }
+
+ public class Rect {
+ ctor public Rect();
+ method public int height();
+ method public int width();
+ field public int bottom;
+ field public int left;
+ field public int right;
+ field public int top;
+ }
+
+ public class Schlick extends androidx.constraintlayout.core.motion.utils.Easing {
+ }
+
+ public abstract class SplineSet {
+ ctor public SplineSet();
+ method public float get(float);
+ method public androidx.constraintlayout.core.motion.utils.CurveFit! getCurveFit();
+ method public float getSlope(float);
+ method public static androidx.constraintlayout.core.motion.utils.SplineSet! makeCustomSpline(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomArray!);
+ method public static androidx.constraintlayout.core.motion.utils.SplineSet! makeCustomSplineSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomVar!);
+ method public static androidx.constraintlayout.core.motion.utils.SplineSet! makeSpline(String!, long);
+ method public void setPoint(int, float);
+ method public void setProperty(androidx.constraintlayout.core.motion.utils.TypedValues!, float);
+ method public void setType(String!);
+ method public void setup(int);
+ field protected androidx.constraintlayout.core.motion.utils.CurveFit! mCurveFit;
+ field protected int[]! mTimePoints;
+ field protected float[]! mValues;
+ }
+
+ public static class SplineSet.CustomSet extends androidx.constraintlayout.core.motion.utils.SplineSet {
+ ctor public SplineSet.CustomSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomArray!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomAttribute!);
+ method public void setProperty(androidx.constraintlayout.core.state.WidgetFrame!, float);
+ }
+
+ public static class SplineSet.CustomSpline extends androidx.constraintlayout.core.motion.utils.SplineSet {
+ ctor public SplineSet.CustomSpline(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomVar!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomVariable!);
+ method public void setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float);
+ }
+
+ public class SpringStopEngine implements androidx.constraintlayout.core.motion.utils.StopEngine {
+ ctor public SpringStopEngine();
+ method public String! debug(String!, float);
+ method public float getAcceleration();
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ method public void springConfig(float, float, float, float, float, float, float, int);
+ }
+
+ public class StepCurve extends androidx.constraintlayout.core.motion.utils.Easing {
+ }
+
+ public interface StopEngine {
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ }
+
+ public class StopLogicEngine implements androidx.constraintlayout.core.motion.utils.StopEngine {
+ ctor public StopLogicEngine();
+ method public void config(float, float, float, float, float, float);
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ }
+
+ public static class StopLogicEngine.Decelerate implements androidx.constraintlayout.core.motion.utils.StopEngine {
+ ctor public StopLogicEngine.Decelerate();
+ method public void config(float, float, float);
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ }
+
+ public abstract class TimeCycleSplineSet {
+ ctor public TimeCycleSplineSet();
+ method protected float calcWave(float);
+ method public androidx.constraintlayout.core.motion.utils.CurveFit! getCurveFit();
+ method public void setPoint(int, float, float, int, float);
+ method protected void setStartTime(long);
+ method public void setType(String!);
+ method public void setup(int);
+ field protected static final int CURVE_OFFSET = 2; // 0x2
+ field protected static final int CURVE_PERIOD = 1; // 0x1
+ field protected static final int CURVE_VALUE = 0; // 0x0
+ field protected float[]! mCache;
+ field protected boolean mContinue;
+ field protected int mCount;
+ field protected androidx.constraintlayout.core.motion.utils.CurveFit! mCurveFit;
+ field protected float mLastCycle;
+ field protected long mLastTime;
+ field protected int[]! mTimePoints;
+ field protected String! mType;
+ field protected float[]![]! mValues;
+ field protected int mWaveShape;
+ field protected static float sVal2PI;
+ }
+
+ public static class TimeCycleSplineSet.CustomSet extends androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet {
+ ctor public TimeCycleSplineSet.CustomSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomArray!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomAttribute!, float, int, float);
+ method public boolean setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ public static class TimeCycleSplineSet.CustomVarSet extends androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet {
+ ctor public TimeCycleSplineSet.CustomVarSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomVar!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomVariable!, float, int, float);
+ method public boolean setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ protected static class TimeCycleSplineSet.Sort {
+ ctor protected TimeCycleSplineSet.Sort();
+ }
+
+ public class TypedBundle {
+ ctor public TypedBundle();
+ method public void add(int, boolean);
+ method public void add(int, float);
+ method public void add(int, int);
+ method public void add(int, String!);
+ method public void addIfNotNull(int, String!);
+ method public void applyDelta(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void applyDelta(androidx.constraintlayout.core.motion.utils.TypedValues!);
+ method public void clear();
+ method public int getInteger(int);
+ }
+
+ public interface TypedValues {
+ method public int getId(String!);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ field public static final int BOOLEAN_MASK = 1; // 0x1
+ field public static final int FLOAT_MASK = 4; // 0x4
+ field public static final int INT_MASK = 2; // 0x2
+ field public static final int STRING_MASK = 8; // 0x8
+ field public static final String S_CUSTOM = "CUSTOM";
+ field public static final int TYPE_FRAME_POSITION = 100; // 0x64
+ field public static final int TYPE_TARGET = 101; // 0x65
+ }
+
+ public static interface TypedValues.AttributesType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyAttributes";
+ field public static final String S_ALPHA = "alpha";
+ field public static final String S_CURVE_FIT = "curveFit";
+ field public static final String S_CUSTOM = "CUSTOM";
+ field public static final String S_EASING = "easing";
+ field public static final String S_ELEVATION = "elevation";
+ field public static final String S_FRAME = "frame";
+ field public static final String S_PATH_ROTATE = "pathRotate";
+ field public static final String S_PIVOT_TARGET = "pivotTarget";
+ field public static final String S_PIVOT_X = "pivotX";
+ field public static final String S_PIVOT_Y = "pivotY";
+ field public static final String S_PROGRESS = "progress";
+ field public static final String S_ROTATION_X = "rotationX";
+ field public static final String S_ROTATION_Y = "rotationY";
+ field public static final String S_ROTATION_Z = "rotationZ";
+ field public static final String S_SCALE_X = "scaleX";
+ field public static final String S_SCALE_Y = "scaleY";
+ field public static final String S_TARGET = "target";
+ field public static final String S_TRANSLATION_X = "translationX";
+ field public static final String S_TRANSLATION_Y = "translationY";
+ field public static final String S_TRANSLATION_Z = "translationZ";
+ field public static final String S_VISIBILITY = "visibility";
+ field public static final int TYPE_ALPHA = 303; // 0x12f
+ field public static final int TYPE_CURVE_FIT = 301; // 0x12d
+ field public static final int TYPE_EASING = 317; // 0x13d
+ field public static final int TYPE_ELEVATION = 307; // 0x133
+ field public static final int TYPE_PATH_ROTATE = 316; // 0x13c
+ field public static final int TYPE_PIVOT_TARGET = 318; // 0x13e
+ field public static final int TYPE_PIVOT_X = 313; // 0x139
+ field public static final int TYPE_PIVOT_Y = 314; // 0x13a
+ field public static final int TYPE_PROGRESS = 315; // 0x13b
+ field public static final int TYPE_ROTATION_X = 308; // 0x134
+ field public static final int TYPE_ROTATION_Y = 309; // 0x135
+ field public static final int TYPE_ROTATION_Z = 310; // 0x136
+ field public static final int TYPE_SCALE_X = 311; // 0x137
+ field public static final int TYPE_SCALE_Y = 312; // 0x138
+ field public static final int TYPE_TRANSLATION_X = 304; // 0x130
+ field public static final int TYPE_TRANSLATION_Y = 305; // 0x131
+ field public static final int TYPE_TRANSLATION_Z = 306; // 0x132
+ field public static final int TYPE_VISIBILITY = 302; // 0x12e
+ }
+
+ public static interface TypedValues.Custom {
+ method public static int getId(String!);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "Custom";
+ field public static final String S_BOOLEAN = "boolean";
+ field public static final String S_COLOR = "color";
+ field public static final String S_DIMENSION = "dimension";
+ field public static final String S_FLOAT = "float";
+ field public static final String S_INT = "integer";
+ field public static final String S_REFERENCE = "reference";
+ field public static final String S_STRING = "string";
+ field public static final int TYPE_BOOLEAN = 904; // 0x388
+ field public static final int TYPE_COLOR = 902; // 0x386
+ field public static final int TYPE_DIMENSION = 905; // 0x389
+ field public static final int TYPE_FLOAT = 901; // 0x385
+ field public static final int TYPE_INT = 900; // 0x384
+ field public static final int TYPE_REFERENCE = 906; // 0x38a
+ field public static final int TYPE_STRING = 903; // 0x387
+ }
+
+ public static interface TypedValues.CycleType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyCycle";
+ field public static final String S_ALPHA = "alpha";
+ field public static final String S_CURVE_FIT = "curveFit";
+ field public static final String S_CUSTOM_WAVE_SHAPE = "customWave";
+ field public static final String S_EASING = "easing";
+ field public static final String S_ELEVATION = "elevation";
+ field public static final String S_PATH_ROTATE = "pathRotate";
+ field public static final String S_PIVOT_X = "pivotX";
+ field public static final String S_PIVOT_Y = "pivotY";
+ field public static final String S_PROGRESS = "progress";
+ field public static final String S_ROTATION_X = "rotationX";
+ field public static final String S_ROTATION_Y = "rotationY";
+ field public static final String S_ROTATION_Z = "rotationZ";
+ field public static final String S_SCALE_X = "scaleX";
+ field public static final String S_SCALE_Y = "scaleY";
+ field public static final String S_TRANSLATION_X = "translationX";
+ field public static final String S_TRANSLATION_Y = "translationY";
+ field public static final String S_TRANSLATION_Z = "translationZ";
+ field public static final String S_VISIBILITY = "visibility";
+ field public static final String S_WAVE_OFFSET = "offset";
+ field public static final String S_WAVE_PERIOD = "period";
+ field public static final String S_WAVE_PHASE = "phase";
+ field public static final String S_WAVE_SHAPE = "waveShape";
+ field public static final int TYPE_ALPHA = 403; // 0x193
+ field public static final int TYPE_CURVE_FIT = 401; // 0x191
+ field public static final int TYPE_CUSTOM_WAVE_SHAPE = 422; // 0x1a6
+ field public static final int TYPE_EASING = 420; // 0x1a4
+ field public static final int TYPE_ELEVATION = 307; // 0x133
+ field public static final int TYPE_PATH_ROTATE = 416; // 0x1a0
+ field public static final int TYPE_PIVOT_X = 313; // 0x139
+ field public static final int TYPE_PIVOT_Y = 314; // 0x13a
+ field public static final int TYPE_PROGRESS = 315; // 0x13b
+ field public static final int TYPE_ROTATION_X = 308; // 0x134
+ field public static final int TYPE_ROTATION_Y = 309; // 0x135
+ field public static final int TYPE_ROTATION_Z = 310; // 0x136
+ field public static final int TYPE_SCALE_X = 311; // 0x137
+ field public static final int TYPE_SCALE_Y = 312; // 0x138
+ field public static final int TYPE_TRANSLATION_X = 304; // 0x130
+ field public static final int TYPE_TRANSLATION_Y = 305; // 0x131
+ field public static final int TYPE_TRANSLATION_Z = 306; // 0x132
+ field public static final int TYPE_VISIBILITY = 402; // 0x192
+ field public static final int TYPE_WAVE_OFFSET = 424; // 0x1a8
+ field public static final int TYPE_WAVE_PERIOD = 423; // 0x1a7
+ field public static final int TYPE_WAVE_PHASE = 425; // 0x1a9
+ field public static final int TYPE_WAVE_SHAPE = 421; // 0x1a5
+ }
+
+ public static interface TypedValues.MotionScene {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "MotionScene";
+ field public static final String S_DEFAULT_DURATION = "defaultDuration";
+ field public static final String S_LAYOUT_DURING_TRANSITION = "layoutDuringTransition";
+ field public static final int TYPE_DEFAULT_DURATION = 600; // 0x258
+ field public static final int TYPE_LAYOUT_DURING_TRANSITION = 601; // 0x259
+ }
+
+ public static interface TypedValues.MotionType {
+ method public static int getId(String!);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "Motion";
+ field public static final String S_ANIMATE_CIRCLEANGLE_TO = "AnimateCircleAngleTo";
+ field public static final String S_ANIMATE_RELATIVE_TO = "AnimateRelativeTo";
+ field public static final String S_DRAW_PATH = "DrawPath";
+ field public static final String S_EASING = "TransitionEasing";
+ field public static final String S_PATHMOTION_ARC = "PathMotionArc";
+ field public static final String S_PATH_ROTATE = "PathRotate";
+ field public static final String S_POLAR_RELATIVETO = "PolarRelativeTo";
+ field public static final String S_QUANTIZE_INTERPOLATOR = "QuantizeInterpolator";
+ field public static final String S_QUANTIZE_INTERPOLATOR_ID = "QuantizeInterpolatorID";
+ field public static final String S_QUANTIZE_INTERPOLATOR_TYPE = "QuantizeInterpolatorType";
+ field public static final String S_QUANTIZE_MOTIONSTEPS = "QuantizeMotionSteps";
+ field public static final String S_QUANTIZE_MOTION_PHASE = "QuantizeMotionPhase";
+ field public static final String S_STAGGER = "Stagger";
+ field public static final int TYPE_ANIMATE_CIRCLEANGLE_TO = 606; // 0x25e
+ field public static final int TYPE_ANIMATE_RELATIVE_TO = 605; // 0x25d
+ field public static final int TYPE_DRAW_PATH = 608; // 0x260
+ field public static final int TYPE_EASING = 603; // 0x25b
+ field public static final int TYPE_PATHMOTION_ARC = 607; // 0x25f
+ field public static final int TYPE_PATH_ROTATE = 601; // 0x259
+ field public static final int TYPE_POLAR_RELATIVETO = 609; // 0x261
+ field public static final int TYPE_QUANTIZE_INTERPOLATOR = 604; // 0x25c
+ field public static final int TYPE_QUANTIZE_INTERPOLATOR_ID = 612; // 0x264
+ field public static final int TYPE_QUANTIZE_INTERPOLATOR_TYPE = 611; // 0x263
+ field public static final int TYPE_QUANTIZE_MOTIONSTEPS = 610; // 0x262
+ field public static final int TYPE_QUANTIZE_MOTION_PHASE = 602; // 0x25a
+ field public static final int TYPE_STAGGER = 600; // 0x258
+ }
+
+ public static interface TypedValues.OnSwipe {
+ field public static final String AUTOCOMPLETE_MODE = "autocompletemode";
+ field public static final String![]! AUTOCOMPLETE_MODE_ENUM;
+ field public static final String DRAG_DIRECTION = "dragdirection";
+ field public static final String DRAG_SCALE = "dragscale";
+ field public static final String DRAG_THRESHOLD = "dragthreshold";
+ field public static final String LIMIT_BOUNDS_TO = "limitboundsto";
+ field public static final String MAX_ACCELERATION = "maxacceleration";
+ field public static final String MAX_VELOCITY = "maxvelocity";
+ field public static final String MOVE_WHEN_SCROLLAT_TOP = "movewhenscrollattop";
+ field public static final String NESTED_SCROLL_FLAGS = "nestedscrollflags";
+ field public static final String![]! NESTED_SCROLL_FLAGS_ENUM;
+ field public static final String ON_TOUCH_UP = "ontouchup";
+ field public static final String![]! ON_TOUCH_UP_ENUM;
+ field public static final String ROTATION_CENTER_ID = "rotationcenterid";
+ field public static final String SPRINGS_TOP_THRESHOLD = "springstopthreshold";
+ field public static final String SPRING_BOUNDARY = "springboundary";
+ field public static final String![]! SPRING_BOUNDARY_ENUM;
+ field public static final String SPRING_DAMPING = "springdamping";
+ field public static final String SPRING_MASS = "springmass";
+ field public static final String SPRING_STIFFNESS = "springstiffness";
+ field public static final String TOUCH_ANCHOR_ID = "touchanchorid";
+ field public static final String TOUCH_ANCHOR_SIDE = "touchanchorside";
+ field public static final String TOUCH_REGION_ID = "touchregionid";
+ }
+
+ public static interface TypedValues.PositionType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyPosition";
+ field public static final String S_DRAWPATH = "drawPath";
+ field public static final String S_PERCENT_HEIGHT = "percentHeight";
+ field public static final String S_PERCENT_WIDTH = "percentWidth";
+ field public static final String S_PERCENT_X = "percentX";
+ field public static final String S_PERCENT_Y = "percentY";
+ field public static final String S_SIZE_PERCENT = "sizePercent";
+ field public static final String S_TRANSITION_EASING = "transitionEasing";
+ field public static final int TYPE_CURVE_FIT = 508; // 0x1fc
+ field public static final int TYPE_DRAWPATH = 502; // 0x1f6
+ field public static final int TYPE_PATH_MOTION_ARC = 509; // 0x1fd
+ field public static final int TYPE_PERCENT_HEIGHT = 504; // 0x1f8
+ field public static final int TYPE_PERCENT_WIDTH = 503; // 0x1f7
+ field public static final int TYPE_PERCENT_X = 506; // 0x1fa
+ field public static final int TYPE_PERCENT_Y = 507; // 0x1fb
+ field public static final int TYPE_POSITION_TYPE = 510; // 0x1fe
+ field public static final int TYPE_SIZE_PERCENT = 505; // 0x1f9
+ field public static final int TYPE_TRANSITION_EASING = 501; // 0x1f5
+ }
+
+ public static interface TypedValues.TransitionType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "Transitions";
+ field public static final String S_AUTO_TRANSITION = "autoTransition";
+ field public static final String S_DURATION = "duration";
+ field public static final String S_FROM = "from";
+ field public static final String S_INTERPOLATOR = "motionInterpolator";
+ field public static final String S_PATH_MOTION_ARC = "pathMotionArc";
+ field public static final String S_STAGGERED = "staggered";
+ field public static final String S_TO = "to";
+ field public static final String S_TRANSITION_FLAGS = "transitionFlags";
+ field public static final int TYPE_AUTO_TRANSITION = 704; // 0x2c0
+ field public static final int TYPE_DURATION = 700; // 0x2bc
+ field public static final int TYPE_FROM = 701; // 0x2bd
+ field public static final int TYPE_INTERPOLATOR = 705; // 0x2c1
+ field public static final int TYPE_PATH_MOTION_ARC = 509; // 0x1fd
+ field public static final int TYPE_STAGGERED = 706; // 0x2c2
+ field public static final int TYPE_TO = 702; // 0x2be
+ field public static final int TYPE_TRANSITION_FLAGS = 707; // 0x2c3
+ }
+
+ public static interface TypedValues.TriggerType {
+ method public static int getId(String!);
+ field public static final String CROSS = "CROSS";
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyTrigger";
+ field public static final String NEGATIVE_CROSS = "negativeCross";
+ field public static final String POSITIVE_CROSS = "positiveCross";
+ field public static final String POST_LAYOUT = "postLayout";
+ field public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
+ field public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
+ field public static final String TRIGGER_ID = "triggerID";
+ field public static final String TRIGGER_RECEIVER = "triggerReceiver";
+ field public static final String TRIGGER_SLACK = "triggerSlack";
+ field public static final int TYPE_CROSS = 312; // 0x138
+ field public static final int TYPE_NEGATIVE_CROSS = 310; // 0x136
+ field public static final int TYPE_POSITIVE_CROSS = 309; // 0x135
+ field public static final int TYPE_POST_LAYOUT = 304; // 0x130
+ field public static final int TYPE_TRIGGER_COLLISION_ID = 307; // 0x133
+ field public static final int TYPE_TRIGGER_COLLISION_VIEW = 306; // 0x132
+ field public static final int TYPE_TRIGGER_ID = 308; // 0x134
+ field public static final int TYPE_TRIGGER_RECEIVER = 311; // 0x137
+ field public static final int TYPE_TRIGGER_SLACK = 305; // 0x131
+ field public static final int TYPE_VIEW_TRANSITION_ON_CROSS = 301; // 0x12d
+ field public static final int TYPE_VIEW_TRANSITION_ON_NEGATIVE_CROSS = 303; // 0x12f
+ field public static final int TYPE_VIEW_TRANSITION_ON_POSITIVE_CROSS = 302; // 0x12e
+ field public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
+ field public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
+ field public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
+ }
+
+ public class Utils {
+ ctor public Utils();
+ method public int getInterpolatedColor(float[]!);
+ method public static void log(String!);
+ method public static void log(String!, String!);
+ method public static void logStack(String!, int);
+ method public static void loge(String!, String!);
+ method public static int rgbaTocColor(float, float, float, float);
+ method public static void setDebugHandle(androidx.constraintlayout.core.motion.utils.Utils.DebugHandle!);
+ method public static void socketSend(String!);
+ }
+
+ public static interface Utils.DebugHandle {
+ method public void message(String!);
+ }
+
+ public class VelocityMatrix {
+ ctor public VelocityMatrix();
+ method public void applyTransform(float, float, int, int, float[]!);
+ method public void clear();
+ method public void setRotationVelocity(androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, float);
+ method public void setRotationVelocity(androidx.constraintlayout.core.motion.utils.SplineSet!, float);
+ method public void setScaleVelocity(androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, float);
+ method public void setScaleVelocity(androidx.constraintlayout.core.motion.utils.SplineSet!, androidx.constraintlayout.core.motion.utils.SplineSet!, float);
+ method public void setTranslationVelocity(androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, float);
+ method public void setTranslationVelocity(androidx.constraintlayout.core.motion.utils.SplineSet!, androidx.constraintlayout.core.motion.utils.SplineSet!, float);
+ }
+
+ public class ViewState {
+ ctor public ViewState();
+ method public void getState(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public int height();
+ method public int width();
+ field public int bottom;
+ field public int left;
+ field public int right;
+ field public float rotation;
+ field public int top;
+ }
+
+}
+
+package androidx.constraintlayout.core.parser {
+
+ public class CLArray extends androidx.constraintlayout.core.parser.CLContainer {
+ ctor public CLArray(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ }
+
+ public class CLContainer extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLContainer(char[]!);
+ method public void add(androidx.constraintlayout.core.parser.CLElement!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public void clear();
+ method public androidx.constraintlayout.core.parser.CLContainer clone();
+ method public androidx.constraintlayout.core.parser.CLElement! get(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLElement! get(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLArray! getArray(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLArray! getArray(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLArray! getArrayOrCreate(String!);
+ method public androidx.constraintlayout.core.parser.CLArray! getArrayOrNull(String!);
+ method public boolean getBoolean(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public boolean getBoolean(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public float getFloat(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public float getFloat(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public float getFloatOrNaN(String!);
+ method public int getInt(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public int getInt(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLObject! getObject(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLObject! getObject(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLObject! getObjectOrNull(String!);
+ method public androidx.constraintlayout.core.parser.CLElement! getOrNull(int);
+ method public androidx.constraintlayout.core.parser.CLElement! getOrNull(String!);
+ method public String! getString(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public String! getString(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public String! getStringOrNull(int);
+ method public String! getStringOrNull(String!);
+ method public boolean has(String!);
+ method public java.util.ArrayList<java.lang.String!>! names();
+ method public void put(String!, androidx.constraintlayout.core.parser.CLElement!);
+ method public void putNumber(String!, float);
+ method public void putString(String!, String!);
+ method public void remove(String!);
+ method public int size();
+ }
+
+ public class CLElement implements java.lang.Cloneable {
+ ctor public CLElement(char[]!);
+ method protected void addIndent(StringBuilder!, int);
+ method public androidx.constraintlayout.core.parser.CLElement clone();
+ method public String! content();
+ method public androidx.constraintlayout.core.parser.CLElement! getContainer();
+ method protected String! getDebugName();
+ method public long getEnd();
+ method public float getFloat();
+ method public int getInt();
+ method public int getLine();
+ method public long getStart();
+ method protected String! getStrClass();
+ method public boolean hasContent();
+ method public boolean isDone();
+ method public boolean isStarted();
+ method public boolean notStarted();
+ method public void setContainer(androidx.constraintlayout.core.parser.CLContainer!);
+ method public void setEnd(long);
+ method public void setLine(int);
+ method public void setStart(long);
+ method protected String! toFormattedJSON(int, int);
+ method protected String! toJSON();
+ field protected androidx.constraintlayout.core.parser.CLContainer! mContainer;
+ field protected long mEnd;
+ field protected long mStart;
+ field protected static int sBaseIndent;
+ field protected static int sMaxLine;
+ }
+
+ public class CLKey extends androidx.constraintlayout.core.parser.CLContainer {
+ ctor public CLKey(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(String!, androidx.constraintlayout.core.parser.CLElement!);
+ method public String! getName();
+ method public androidx.constraintlayout.core.parser.CLElement! getValue();
+ method public void set(androidx.constraintlayout.core.parser.CLElement!);
+ }
+
+ public class CLNumber extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLNumber(char[]!);
+ ctor public CLNumber(float);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public boolean isInt();
+ method public void putValue(float);
+ }
+
+ public class CLObject extends androidx.constraintlayout.core.parser.CLContainer implements java.lang.Iterable<androidx.constraintlayout.core.parser.CLKey!> {
+ ctor public CLObject(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLObject! allocate(char[]!);
+ method public androidx.constraintlayout.core.parser.CLObject clone();
+ method public java.util.Iterator<androidx.constraintlayout.core.parser.CLKey!>! iterator();
+ method public String! toFormattedJSON();
+ method public String! toFormattedJSON(int, int);
+ method public String! toJSON();
+ }
+
+ public class CLParser {
+ ctor public CLParser(String!);
+ method public androidx.constraintlayout.core.parser.CLObject! parse() throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static androidx.constraintlayout.core.parser.CLObject! parse(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ }
+
+ public class CLParsingException extends java.lang.Exception {
+ ctor public CLParsingException(String!, androidx.constraintlayout.core.parser.CLElement!);
+ method public String! reason();
+ }
+
+ public class CLString extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLString(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLString from(String);
+ }
+
+ public class CLToken extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLToken(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public boolean getBoolean() throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLToken.Type! getType();
+ method public boolean isNull() throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public boolean validate(char, long);
+ }
+
+}
+
+package androidx.constraintlayout.core.state {
+
+ public class ConstraintReference implements androidx.constraintlayout.core.state.Reference {
+ ctor public ConstraintReference(androidx.constraintlayout.core.state.State!);
+ method public void addCustomColor(String!, int);
+ method public void addCustomFloat(String!, float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! alpha(float);
+ method public void apply();
+ method public void applyWidgetConstraints();
+ method public androidx.constraintlayout.core.state.ConstraintReference! baseline();
+ method public androidx.constraintlayout.core.state.ConstraintReference! baselineToBaseline(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! baselineToBottom(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! baselineToTop(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! bias(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! bottom();
+ method public androidx.constraintlayout.core.state.ConstraintReference! bottomToBottom(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! bottomToTop(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! centerHorizontally(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! centerVertically(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! circularConstraint(Object!, float, float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! clear();
+ method public androidx.constraintlayout.core.state.ConstraintReference! clearAll();
+ method public androidx.constraintlayout.core.state.ConstraintReference! clearHorizontal();
+ method public androidx.constraintlayout.core.state.ConstraintReference! clearVertical();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! createConstraintWidget();
+ method public androidx.constraintlayout.core.state.ConstraintReference! end();
+ method public androidx.constraintlayout.core.state.ConstraintReference! endToEnd(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! endToStart(Object!);
+ method public float getAlpha();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public androidx.constraintlayout.core.state.helpers.Facade! getFacade();
+ method public androidx.constraintlayout.core.state.Dimension! getHeight();
+ method public int getHorizontalChainStyle();
+ method public float getHorizontalChainWeight();
+ method public Object! getKey();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public String! getTag();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public int getVerticalChainStyle(int);
+ method public float getVerticalChainWeight();
+ method public Object! getView();
+ method public androidx.constraintlayout.core.state.Dimension! getWidth();
+ method public androidx.constraintlayout.core.state.ConstraintReference! height(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! horizontalBias(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! left();
+ method public androidx.constraintlayout.core.state.ConstraintReference! leftToLeft(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! leftToRight(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! margin(int);
+ method public androidx.constraintlayout.core.state.ConstraintReference! margin(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! marginGone(int);
+ method public androidx.constraintlayout.core.state.ConstraintReference! marginGone(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! pivotX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! pivotY(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! right();
+ method public androidx.constraintlayout.core.state.ConstraintReference! rightToLeft(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rightToRight(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rotationX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rotationY(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rotationZ(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! scaleX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! scaleY(float);
+ method public void setConstraintWidget(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setFacade(androidx.constraintlayout.core.state.helpers.Facade!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! setHeight(androidx.constraintlayout.core.state.Dimension!);
+ method public void setHorizontalChainStyle(int);
+ method public void setHorizontalChainWeight(float);
+ method public void setKey(Object!);
+ method public void setTag(String!);
+ method public void setVerticalChainStyle(int);
+ method public void setVerticalChainWeight(float);
+ method public void setView(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! setWidth(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! start();
+ method public androidx.constraintlayout.core.state.ConstraintReference! startToEnd(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! startToStart(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! top();
+ method public androidx.constraintlayout.core.state.ConstraintReference! topToBottom(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! topToTop(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! translationX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! translationY(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! translationZ(float);
+ method public void validate() throws java.lang.Exception;
+ method public androidx.constraintlayout.core.state.ConstraintReference! verticalBias(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! visibility(int);
+ method public androidx.constraintlayout.core.state.ConstraintReference! width(androidx.constraintlayout.core.state.Dimension!);
+ field protected Object! mBottomToBottom;
+ field protected Object! mBottomToTop;
+ field protected Object! mEndToEnd;
+ field protected Object! mEndToStart;
+ field protected float mHorizontalBias;
+ field protected Object! mLeftToLeft;
+ field protected Object! mLeftToRight;
+ field protected int mMarginBottom;
+ field protected int mMarginBottomGone;
+ field protected int mMarginEnd;
+ field protected int mMarginEndGone;
+ field protected int mMarginLeft;
+ field protected int mMarginLeftGone;
+ field protected int mMarginRight;
+ field protected int mMarginRightGone;
+ field protected int mMarginStart;
+ field protected int mMarginStartGone;
+ field protected int mMarginTop;
+ field protected int mMarginTopGone;
+ field protected Object! mRightToLeft;
+ field protected Object! mRightToRight;
+ field protected Object! mStartToEnd;
+ field protected Object! mStartToStart;
+ field protected Object! mTopToBottom;
+ field protected Object! mTopToTop;
+ field protected float mVerticalBias;
+ }
+
+ public static interface ConstraintReference.ConstraintReferenceFactory {
+ method public androidx.constraintlayout.core.state.ConstraintReference! create(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class ConstraintSetParser {
+ ctor public ConstraintSetParser();
+ method public static void parseDesignElementsJSON(String!, java.util.ArrayList<androidx.constraintlayout.core.state.ConstraintSetParser.DesignElement!>!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static void parseJSON(String!, androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.ConstraintSetParser.LayoutVariables!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static void parseJSON(String!, androidx.constraintlayout.core.state.Transition!, int);
+ method public static void parseMotionSceneJSON(androidx.constraintlayout.core.state.CoreMotionScene!, String!);
+ }
+
+ public static class ConstraintSetParser.DesignElement {
+ method public String! getId();
+ method public java.util.HashMap<java.lang.String!,java.lang.String!>! getParams();
+ method public String! getType();
+ }
+
+ public static class ConstraintSetParser.LayoutVariables {
+ ctor public ConstraintSetParser.LayoutVariables();
+ method public void putOverride(String!, float);
+ }
+
+ public enum ConstraintSetParser.MotionLayoutDebugFlags {
+ enum_constant public static final androidx.constraintlayout.core.state.ConstraintSetParser.MotionLayoutDebugFlags NONE;
+ enum_constant public static final androidx.constraintlayout.core.state.ConstraintSetParser.MotionLayoutDebugFlags SHOW_ALL;
+ enum_constant public static final androidx.constraintlayout.core.state.ConstraintSetParser.MotionLayoutDebugFlags UNKNOWN;
+ }
+
+ public interface CoreMotionScene {
+ method public String! getConstraintSet(int);
+ method public String! getConstraintSet(String!);
+ method public String! getTransition(String!);
+ method public void setConstraintSetContent(String!, String!);
+ method public void setDebugName(String!);
+ method public void setTransitionContent(String!, String!);
+ }
+
+ public interface CorePixelDp {
+ method public float toPixels(float);
+ }
+
+ public class Dimension {
+ method public void apply(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ method public static androidx.constraintlayout.core.state.Dimension! createFixed(int);
+ method public static androidx.constraintlayout.core.state.Dimension! createFixed(Object!);
+ method public static androidx.constraintlayout.core.state.Dimension! createParent();
+ method public static androidx.constraintlayout.core.state.Dimension! createPercent(Object!, float);
+ method public static androidx.constraintlayout.core.state.Dimension! createRatio(String!);
+ method public static androidx.constraintlayout.core.state.Dimension! createSpread();
+ method public static androidx.constraintlayout.core.state.Dimension! createSuggested(int);
+ method public static androidx.constraintlayout.core.state.Dimension! createSuggested(Object!);
+ method public static androidx.constraintlayout.core.state.Dimension! createWrap();
+ method public boolean equalsFixedValue(int);
+ method public androidx.constraintlayout.core.state.Dimension! fixed(int);
+ method public androidx.constraintlayout.core.state.Dimension! fixed(Object!);
+ method public androidx.constraintlayout.core.state.Dimension! max(int);
+ method public androidx.constraintlayout.core.state.Dimension! max(Object!);
+ method public androidx.constraintlayout.core.state.Dimension! min(int);
+ method public androidx.constraintlayout.core.state.Dimension! min(Object!);
+ method public androidx.constraintlayout.core.state.Dimension! percent(Object!, float);
+ method public androidx.constraintlayout.core.state.Dimension! ratio(String!);
+ method public androidx.constraintlayout.core.state.Dimension! suggested(int);
+ method public androidx.constraintlayout.core.state.Dimension! suggested(Object!);
+ field public static final Object! FIXED_DIMENSION;
+ field public static final Object! PARENT_DIMENSION;
+ field public static final Object! PERCENT_DIMENSION;
+ field public static final Object! RATIO_DIMENSION;
+ field public static final Object! SPREAD_DIMENSION;
+ field public static final Object! WRAP_DIMENSION;
+ }
+
+ public enum Dimension.Type {
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type FIXED;
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type MATCH_CONSTRAINT;
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type MATCH_PARENT;
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type WRAP;
+ }
+
+ public class HelperReference extends androidx.constraintlayout.core.state.ConstraintReference implements androidx.constraintlayout.core.state.helpers.Facade {
+ ctor public HelperReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
+ method public androidx.constraintlayout.core.state.HelperReference! add(java.lang.Object!...!);
+ method public void applyBase();
+ method public androidx.constraintlayout.core.widgets.HelperWidget! getHelperWidget();
+ method public androidx.constraintlayout.core.state.State.Helper! getType();
+ method public void setHelperWidget(androidx.constraintlayout.core.widgets.HelperWidget!);
+ field protected final androidx.constraintlayout.core.state.State! mHelperState;
+ field protected java.util.ArrayList<java.lang.Object!>! mReferences;
+ }
+
+ public interface Interpolator {
+ method public float getInterpolation(float);
+ }
+
+ public interface Reference {
+ method public void apply();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public androidx.constraintlayout.core.state.helpers.Facade! getFacade();
+ method public Object! getKey();
+ method public void setConstraintWidget(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setKey(Object!);
+ }
+
+ public class Registry {
+ ctor public Registry();
+ method public String! currentContent(String!);
+ method public String! currentLayoutInformation(String!);
+ method public static androidx.constraintlayout.core.state.Registry! getInstance();
+ method public long getLastModified(String!);
+ method public java.util.Set<java.lang.String!>! getLayoutList();
+ method public void register(String!, androidx.constraintlayout.core.state.RegistryCallback!);
+ method public void setDrawDebug(String!, int);
+ method public void setLayoutInformationMode(String!, int);
+ method public void unregister(String!, androidx.constraintlayout.core.state.RegistryCallback!);
+ method public void updateContent(String!, String!);
+ method public void updateDimensions(String!, int, int);
+ method public void updateProgress(String!, float);
+ }
+
+ public interface RegistryCallback {
+ method public String! currentLayoutInformation();
+ method public String! currentMotionScene();
+ method public long getLastModified();
+ method public void onDimensions(int, int);
+ method public void onNewMotionScene(String!);
+ method public void onProgress(float);
+ method public void setDrawDebug(int);
+ method public void setLayoutInformationMode(int);
+ }
+
+ public class State {
+ ctor public State();
+ method public void apply(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ method public androidx.constraintlayout.core.state.helpers.BarrierReference! barrier(Object!, androidx.constraintlayout.core.state.State.Direction!);
+ method public void baselineNeededFor(Object!);
+ method public androidx.constraintlayout.core.state.helpers.AlignHorizontallyReference! centerHorizontally(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.AlignVerticallyReference! centerVertically(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! constraints(Object!);
+ method public int convertDimension(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! createConstraintReference(Object!);
+ method public void directMapping();
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getFlow(Object!, boolean);
+ method public androidx.constraintlayout.core.state.helpers.GridReference getGrid(Object, String);
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getHorizontalFlow();
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getHorizontalFlow(java.lang.Object!...!);
+ method public java.util.ArrayList<java.lang.String!>! getIdsForTag(String!);
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getVerticalFlow();
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getVerticalFlow(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! guideline(Object!, int);
+ method public androidx.constraintlayout.core.state.State! height(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.HelperReference! helper(Object!, androidx.constraintlayout.core.state.State.Helper!);
+ method public androidx.constraintlayout.core.state.helpers.HorizontalChainReference! horizontalChain();
+ method public androidx.constraintlayout.core.state.helpers.HorizontalChainReference! horizontalChain(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! horizontalGuideline(Object!);
+ method public boolean isBaselineNeeded(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method @Deprecated public boolean isLtr();
+ method public boolean isRtl();
+ method public void map(Object!, Object!);
+ method public void reset();
+ method public boolean sameFixedHeight(int);
+ method public boolean sameFixedWidth(int);
+ method public void setDpToPixel(androidx.constraintlayout.core.state.CorePixelDp!);
+ method public androidx.constraintlayout.core.state.State! setHeight(androidx.constraintlayout.core.state.Dimension!);
+ method @Deprecated public void setLtr(boolean);
+ method public void setRtl(boolean);
+ method public void setTag(String!, String!);
+ method public androidx.constraintlayout.core.state.State! setWidth(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.helpers.VerticalChainReference! verticalChain();
+ method public androidx.constraintlayout.core.state.helpers.VerticalChainReference! verticalChain(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! verticalGuideline(Object!);
+ method public androidx.constraintlayout.core.state.State! width(androidx.constraintlayout.core.state.Dimension!);
+ field public static final Integer PARENT;
+ field protected java.util.HashMap<java.lang.Object!,androidx.constraintlayout.core.state.HelperReference!>! mHelperReferences;
+ field public final androidx.constraintlayout.core.state.ConstraintReference! mParent;
+ field protected java.util.HashMap<java.lang.Object!,androidx.constraintlayout.core.state.Reference!>! mReferences;
+ }
+
+ public enum State.Chain {
+ method public static androidx.constraintlayout.core.state.State.Chain! getChainByString(String!);
+ method public static int getValueByString(String!);
+ enum_constant public static final androidx.constraintlayout.core.state.State.Chain PACKED;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Chain SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Chain SPREAD_INSIDE;
+ field public static java.util.Map<java.lang.String!,androidx.constraintlayout.core.state.State.Chain!>! chainMap;
+ field public static java.util.Map<java.lang.String!,java.lang.Integer!>! valueMap;
+ }
+
+ public enum State.Constraint {
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_TOP;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_TOP;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_HORIZONTALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_VERTICALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CIRCULAR_CONSTRAINT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint END_TO_END;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint END_TO_START;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint LEFT_TO_LEFT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint LEFT_TO_RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_LEFT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_END;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_START;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_TOP;
+ }
+
+ public enum State.Direction {
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction END;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction LEFT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction START;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction TOP;
+ }
+
+ public enum State.Helper {
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper ALIGN_HORIZONTALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper ALIGN_VERTICALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper BARRIER;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper COLUMN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper FLOW;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper GRID;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper HORIZONTAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper HORIZONTAL_FLOW;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper LAYER;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper ROW;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper VERTICAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper VERTICAL_FLOW;
+ }
+
+ public enum State.Wrap {
+ method public static androidx.constraintlayout.core.state.State.Wrap! getChainByString(String!);
+ method public static int getValueByString(String!);
+ enum_constant public static final androidx.constraintlayout.core.state.State.Wrap ALIGNED;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Wrap CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Wrap NONE;
+ field public static java.util.Map<java.lang.String!,java.lang.Integer!>! valueMap;
+ field public static java.util.Map<java.lang.String!,androidx.constraintlayout.core.state.State.Wrap!>! wrapMap;
+ }
+
+ public class Transition implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public Transition(androidx.constraintlayout.core.state.CorePixelDp);
+ method public void addCustomColor(int, String!, String!, int);
+ method public void addCustomFloat(int, String!, String!, float);
+ method public void addKeyAttribute(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void addKeyAttribute(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!, androidx.constraintlayout.core.motion.CustomVariable![]!);
+ method public void addKeyCycle(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void addKeyPosition(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void addKeyPosition(String!, int, int, float, float);
+ method public void calcStagger();
+ method public void clear();
+ method public boolean contains(String!);
+ method public float dragToProgress(float, int, int, float, float);
+ method public void fillKeyPositions(androidx.constraintlayout.core.state.WidgetFrame!, float[]!, float[]!, float[]!);
+ method public androidx.constraintlayout.core.state.Transition.KeyPosition! findNextPosition(String!, int);
+ method public androidx.constraintlayout.core.state.Transition.KeyPosition! findPreviousPosition(String!, int);
+ method public int getAutoTransition();
+ method public androidx.constraintlayout.core.state.WidgetFrame! getEnd(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getEnd(String!);
+ method public int getId(String!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getInterpolated(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getInterpolated(String!);
+ method public int getInterpolatedHeight();
+ method public int getInterpolatedWidth();
+ method public androidx.constraintlayout.core.state.Interpolator! getInterpolator();
+ method public static androidx.constraintlayout.core.state.Interpolator! getInterpolator(int, String!);
+ method public int getKeyFrames(String!, float[]!, int[]!, int[]!);
+ method public androidx.constraintlayout.core.motion.Motion! getMotion(String!);
+ method public int getNumberKeyPositions(androidx.constraintlayout.core.state.WidgetFrame!);
+ method public float[]! getPath(String!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getStart(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getStart(String!);
+ method public float getTouchUpProgress(long);
+ method public androidx.constraintlayout.core.state.Transition.WidgetState! getWidgetState(String!, androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ method public boolean hasOnSwipe();
+ method public boolean hasPositionKeyframes();
+ method public void interpolate(int, int, float);
+ method public boolean isEmpty();
+ method public boolean isTouchNotDone(float);
+ method public void setTouchUp(float, long, float, float);
+ method public void setTransitionProperties(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public void updateFrom(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int);
+ field public static final int END = 1; // 0x1
+ field public static final int INTERPOLATED = 2; // 0x2
+ field public static final int START = 0; // 0x0
+ }
+
+ public static class Transition.WidgetState {
+ ctor public Transition.WidgetState();
+ method public androidx.constraintlayout.core.state.WidgetFrame! getFrame(int);
+ method public void interpolate(int, int, float, androidx.constraintlayout.core.state.Transition!);
+ method public void setKeyAttribute(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void setKeyAttribute(androidx.constraintlayout.core.motion.utils.TypedBundle!, androidx.constraintlayout.core.motion.CustomVariable![]!);
+ method public void setKeyCycle(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void setKeyPosition(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void setPathRelative(androidx.constraintlayout.core.state.Transition.WidgetState!);
+ method public void update(androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ }
+
+ public class TransitionParser {
+ ctor public TransitionParser();
+ method @Deprecated public static void parse(androidx.constraintlayout.core.parser.CLObject!, androidx.constraintlayout.core.state.Transition!, androidx.constraintlayout.core.state.CorePixelDp!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static void parseKeyFrames(androidx.constraintlayout.core.parser.CLObject!, androidx.constraintlayout.core.state.Transition!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ }
+
+ public class WidgetFrame {
+ ctor public WidgetFrame();
+ ctor public WidgetFrame(androidx.constraintlayout.core.state.WidgetFrame!);
+ ctor public WidgetFrame(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void addCustomColor(String!, int);
+ method public void addCustomFloat(String!, float);
+ method public float centerX();
+ method public float centerY();
+ method public boolean containsCustom(String);
+ method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
+ method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
+ method public int getCustomColor(String!);
+ method public float getCustomFloat(String!);
+ method public String! getId();
+ method public androidx.constraintlayout.core.motion.utils.TypedBundle! getMotionProperties();
+ method public int height();
+ method public static void interpolate(int, int, androidx.constraintlayout.core.state.WidgetFrame!, androidx.constraintlayout.core.state.WidgetFrame!, androidx.constraintlayout.core.state.WidgetFrame!, androidx.constraintlayout.core.state.Transition!, float);
+ method public boolean isDefaultTransform();
+ method public StringBuilder! serialize(StringBuilder!);
+ method public StringBuilder! serialize(StringBuilder!, boolean);
+ method public void setCustomAttribute(String!, int, boolean);
+ method public void setCustomAttribute(String!, int, float);
+ method public void setCustomAttribute(String!, int, int);
+ method public void setCustomAttribute(String!, int, String!);
+ method public void setCustomValue(androidx.constraintlayout.core.motion.CustomAttribute!, float[]!);
+ method public boolean setValue(String!, androidx.constraintlayout.core.parser.CLElement!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.state.WidgetFrame! update();
+ method public androidx.constraintlayout.core.state.WidgetFrame! update(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void updateAttributes(androidx.constraintlayout.core.state.WidgetFrame!);
+ method public int width();
+ field public float alpha;
+ field public int bottom;
+ field public float interpolatedPos;
+ field public int left;
+ field public String! name;
+ field public static float phone_orientation;
+ field public float pivotX;
+ field public float pivotY;
+ field public int right;
+ field public float rotationX;
+ field public float rotationY;
+ field public float rotationZ;
+ field public float scaleX;
+ field public float scaleY;
+ field public int top;
+ field public float translationX;
+ field public float translationY;
+ field public float translationZ;
+ field public int visibility;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget! widget;
+ }
+
+}
+
+package androidx.constraintlayout.core.state.helpers {
+
+ public class AlignHorizontallyReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public AlignHorizontallyReference(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class AlignVerticallyReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public AlignVerticallyReference(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class BarrierReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public BarrierReference(androidx.constraintlayout.core.state.State!);
+ method public void setBarrierDirection(androidx.constraintlayout.core.state.State.Direction!);
+ }
+
+ public class ChainReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public ChainReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+ method public void addChainElement(String, float, float, float);
+ method public androidx.constraintlayout.core.state.helpers.ChainReference bias(float);
+ method public float getBias();
+ method protected float getPostMargin(String);
+ method protected float getPreMargin(String);
+ method public androidx.constraintlayout.core.state.State.Chain getStyle();
+ method protected float getWeight(String);
+ method public androidx.constraintlayout.core.state.helpers.ChainReference style(androidx.constraintlayout.core.state.State.Chain);
+ field protected float mBias;
+ field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPostMargin;
+ field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPreMargin;
+ field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapWeights;
+ field protected androidx.constraintlayout.core.state.State.Chain mStyle;
+ }
+
+ public interface Facade {
+ method public void apply();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ }
+
+ public class FlowReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public FlowReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
+ method public void addFlowElement(String!, float, float, float);
+ method public float getFirstHorizontalBias();
+ method public int getFirstHorizontalStyle();
+ method public float getFirstVerticalBias();
+ method public int getFirstVerticalStyle();
+ method public int getHorizontalAlign();
+ method public float getHorizontalBias();
+ method public int getHorizontalGap();
+ method public int getHorizontalStyle();
+ method public float getLastHorizontalBias();
+ method public int getLastHorizontalStyle();
+ method public float getLastVerticalBias();
+ method public int getLastVerticalStyle();
+ method public int getMaxElementsWrap();
+ method public int getOrientation();
+ method public int getPaddingBottom();
+ method public int getPaddingLeft();
+ method public int getPaddingRight();
+ method public int getPaddingTop();
+ method protected float getPostMargin(String!);
+ method protected float getPreMargin(String!);
+ method public int getVerticalAlign();
+ method public float getVerticalBias();
+ method public int getVerticalGap();
+ method public int getVerticalStyle();
+ method protected float getWeight(String!);
+ method public int getWrapMode();
+ method public void setFirstHorizontalBias(float);
+ method public void setFirstHorizontalStyle(int);
+ method public void setFirstVerticalBias(float);
+ method public void setFirstVerticalStyle(int);
+ method public void setHorizontalAlign(int);
+ method public void setHorizontalGap(int);
+ method public void setHorizontalStyle(int);
+ method public void setLastHorizontalBias(float);
+ method public void setLastHorizontalStyle(int);
+ method public void setLastVerticalBias(float);
+ method public void setLastVerticalStyle(int);
+ method public void setMaxElementsWrap(int);
+ method public void setOrientation(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingLeft(int);
+ method public void setPaddingRight(int);
+ method public void setPaddingTop(int);
+ method public void setVerticalAlign(int);
+ method public void setVerticalGap(int);
+ method public void setVerticalStyle(int);
+ method public void setWrapMode(int);
+ field protected float mFirstHorizontalBias;
+ field protected int mFirstHorizontalStyle;
+ field protected float mFirstVerticalBias;
+ field protected int mFirstVerticalStyle;
+ field protected androidx.constraintlayout.core.widgets.Flow! mFlow;
+ field protected int mHorizontalAlign;
+ field protected int mHorizontalGap;
+ field protected int mHorizontalStyle;
+ field protected float mLastHorizontalBias;
+ field protected int mLastHorizontalStyle;
+ field protected float mLastVerticalBias;
+ field protected int mLastVerticalStyle;
+ field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPostMargin;
+ field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPreMargin;
+ field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapWeights;
+ field protected int mMaxElementsWrap;
+ field protected int mOrientation;
+ field protected int mPaddingBottom;
+ field protected int mPaddingLeft;
+ field protected int mPaddingRight;
+ field protected int mPaddingTop;
+ field protected int mVerticalAlign;
+ field protected int mVerticalGap;
+ field protected int mVerticalStyle;
+ field protected int mWrapMode;
+ }
+
+ public class GridReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public GridReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+ method public String? getColumnWeights();
+ method public int getColumnsSet();
+ method public int getFlags();
+ method public float getHorizontalGaps();
+ method public int getOrientation();
+ method public int getPaddingBottom();
+ method public int getPaddingEnd();
+ method public int getPaddingStart();
+ method public int getPaddingTop();
+ method public String? getRowWeights();
+ method public int getRowsSet();
+ method public String? getSkips();
+ method public String? getSpans();
+ method public float getVerticalGaps();
+ method public void setColumnWeights(String);
+ method public void setColumnsSet(int);
+ method public void setFlags(int);
+ method public void setFlags(String);
+ method public void setHorizontalGaps(float);
+ method public void setOrientation(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingEnd(int);
+ method public void setPaddingStart(int);
+ method public void setPaddingTop(int);
+ method public void setRowWeights(String);
+ method public void setRowsSet(int);
+ method public void setSkips(String);
+ method public void setSpans(String);
+ method public void setVerticalGaps(float);
+ }
+
+ public class GuidelineReference implements androidx.constraintlayout.core.state.helpers.Facade androidx.constraintlayout.core.state.Reference {
+ ctor public GuidelineReference(androidx.constraintlayout.core.state.State!);
+ method public void apply();
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! end(Object!);
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public androidx.constraintlayout.core.state.helpers.Facade! getFacade();
+ method public Object! getKey();
+ method public int getOrientation();
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! percent(float);
+ method public void setConstraintWidget(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setKey(Object!);
+ method public void setOrientation(int);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! start(Object!);
+ }
+
+ public class HorizontalChainReference extends androidx.constraintlayout.core.state.helpers.ChainReference {
+ ctor public HorizontalChainReference(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class VerticalChainReference extends androidx.constraintlayout.core.state.helpers.ChainReference {
+ ctor public VerticalChainReference(androidx.constraintlayout.core.state.State!);
+ }
+
+}
+
+package androidx.constraintlayout.core.utils {
+
+ public class GridCore extends androidx.constraintlayout.core.widgets.VirtualLayout {
+ ctor public GridCore();
+ ctor public GridCore(int, int);
+ method public String? getColumnWeights();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidgetContainer? getContainer();
+ method public int getFlags();
+ method public float getHorizontalGaps();
+ method public int getOrientation();
+ method public String? getRowWeights();
+ method public float getVerticalGaps();
+ method public void setColumnWeights(String);
+ method public void setColumns(int);
+ method public void setContainer(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer);
+ method public void setFlags(int);
+ method public void setHorizontalGaps(float);
+ method public void setOrientation(int);
+ method public void setRowWeights(String);
+ method public void setRows(int);
+ method public void setSkips(String);
+ method public void setSpans(CharSequence);
+ method public void setVerticalGaps(float);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int SPANS_RESPECT_WIDGET_ORDER = 2; // 0x2
+ field public static final int SUB_GRID_BY_COL_ROW = 1; // 0x1
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+ public class GridEngine {
+ ctor public GridEngine();
+ ctor public GridEngine(int, int);
+ ctor public GridEngine(int, int, int);
+ method public int bottomOfWidget(int);
+ method public int leftOfWidget(int);
+ method public int rightOfWidget(int);
+ method public void setColumns(int);
+ method public void setNumWidgets(int);
+ method public void setOrientation(int);
+ method public void setRows(int);
+ method public void setSkips(String!);
+ method public void setSpans(CharSequence!);
+ method public void setup();
+ method public int topOfWidget(int);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+}
+
+package androidx.constraintlayout.core.widgets {
+
+ public class Barrier extends androidx.constraintlayout.core.widgets.HelperWidget {
+ ctor public Barrier();
+ ctor public Barrier(String!);
+ method public boolean allSolved();
+ method @Deprecated public boolean allowsGoneWidget();
+ method public boolean getAllowsGoneWidget();
+ method public int getBarrierType();
+ method public int getMargin();
+ method public int getOrientation();
+ method protected void markWidgets();
+ method public void setAllowsGoneWidget(boolean);
+ method public void setBarrierType(int);
+ method public void setMargin(int);
+ field public static final int BOTTOM = 3; // 0x3
+ field public static final int LEFT = 0; // 0x0
+ field public static final int RIGHT = 1; // 0x1
+ field public static final int TOP = 2; // 0x2
+ }
+
+ public class Chain {
+ ctor public Chain();
+ method public static void applyChainConstraints(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.LinearSystem!, java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>!, int);
+ field public static final boolean USE_CHAIN_OPTIMIZATION = false;
+ }
+
+ public class ChainHead {
+ ctor public ChainHead(androidx.constraintlayout.core.widgets.ConstraintWidget!, int, boolean);
+ method public void define();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getFirst();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getFirstMatchConstraintWidget();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getFirstVisibleWidget();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getHead();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getLast();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getLastMatchConstraintWidget();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getLastVisibleWidget();
+ method public float getTotalWeight();
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mFirst;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mFirstMatchConstraintWidget;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mFirstVisibleWidget;
+ field protected boolean mHasComplexMatchWeights;
+ field protected boolean mHasDefinedWeights;
+ field protected boolean mHasRatio;
+ field protected boolean mHasUndefinedWeights;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mHead;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mLast;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mLastMatchConstraintWidget;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mLastVisibleWidget;
+ field protected float mTotalWeight;
+ field protected java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>! mWeightedMatchConstraintsWidgets;
+ field protected int mWidgetsCount;
+ field protected int mWidgetsMatchCount;
+ }
+
+ public class ConstraintAnchor {
+ ctor public ConstraintAnchor(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!);
+ method public boolean connect(androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method public boolean connect(androidx.constraintlayout.core.widgets.ConstraintAnchor!, int, int, boolean);
+ method public void copyFrom(androidx.constraintlayout.core.widgets.ConstraintAnchor!, java.util.HashMap<androidx.constraintlayout.core.widgets.ConstraintWidget!,androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void findDependents(int, java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public java.util.HashSet<androidx.constraintlayout.core.widgets.ConstraintAnchor!>! getDependents();
+ method public int getFinalValue();
+ method public int getMargin();
+ method public final androidx.constraintlayout.core.widgets.ConstraintAnchor! getOpposite();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getOwner();
+ method public androidx.constraintlayout.core.SolverVariable! getSolverVariable();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor! getTarget();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor.Type! getType();
+ method public boolean hasCenteredDependents();
+ method public boolean hasDependents();
+ method public boolean hasFinalValue();
+ method public boolean isConnected();
+ method public boolean isConnectionAllowed(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public boolean isConnectionAllowed(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public boolean isSideAnchor();
+ method public boolean isSimilarDimensionConnection(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public boolean isValidConnection(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public boolean isVerticalAnchor();
+ method public void reset();
+ method public void resetFinalResolution();
+ method public void resetSolverVariable(androidx.constraintlayout.core.Cache!);
+ method public void setFinalValue(int);
+ method public void setGoneMargin(int);
+ method public void setMargin(int);
+ field public int mMargin;
+ field public final androidx.constraintlayout.core.widgets.ConstraintWidget! mOwner;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mTarget;
+ field public final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type! mType;
+ }
+
+ public enum ConstraintAnchor.Type {
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type CENTER;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type CENTER_X;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type CENTER_Y;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type LEFT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type NONE;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type TOP;
+ }
+
+ public class ConstraintWidget {
+ ctor public ConstraintWidget();
+ ctor public ConstraintWidget(int, int);
+ ctor public ConstraintWidget(int, int, int, int);
+ ctor public ConstraintWidget(String!);
+ ctor public ConstraintWidget(String!, int, int);
+ ctor public ConstraintWidget(String!, int, int, int, int);
+ method public void addChildrenToSolverByDependency(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.LinearSystem!, java.util.HashSet<androidx.constraintlayout.core.widgets.ConstraintWidget!>!, int, boolean);
+ method public void addToSolver(androidx.constraintlayout.core.LinearSystem!, boolean);
+ method public boolean allowedInBarrier();
+ method public void connect(androidx.constraintlayout.core.widgets.ConstraintAnchor!, androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method public void connect(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!);
+ method public void connect(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, int);
+ method public void connectCircularConstraint(androidx.constraintlayout.core.widgets.ConstraintWidget!, float, int);
+ method public void copy(androidx.constraintlayout.core.widgets.ConstraintWidget!, java.util.HashMap<androidx.constraintlayout.core.widgets.ConstraintWidget!,androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void createObjectVariables(androidx.constraintlayout.core.LinearSystem!);
+ method public void ensureMeasureRequested();
+ method public void ensureWidgetRuns();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor! getAnchor(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!);
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintAnchor!>! getAnchors();
+ method public int getBaselineDistance();
+ method public float getBiasPercent(int);
+ method public int getBottom();
+ method public Object! getCompanionWidget();
+ method public int getContainerItemSkip();
+ method public String! getDebugName();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! getDimensionBehaviour(int);
+ method public float getDimensionRatio();
+ method public int getDimensionRatioSide();
+ method public boolean getHasBaseline();
+ method public int getHeight();
+ method public float getHorizontalBiasPercent();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getHorizontalChainControlWidget();
+ method public int getHorizontalChainStyle();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! getHorizontalDimensionBehaviour();
+ method public int getHorizontalMargin();
+ method public int getLastHorizontalMeasureSpec();
+ method public int getLastVerticalMeasureSpec();
+ method public int getLeft();
+ method public int getLength(int);
+ method public int getMaxHeight();
+ method public int getMaxWidth();
+ method public int getMinHeight();
+ method public int getMinWidth();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getNextChainMember(int);
+ method public int getOptimizerWrapHeight();
+ method public int getOptimizerWrapWidth();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getParent();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getPreviousChainMember(int);
+ method public int getRight();
+ method protected int getRootX();
+ method protected int getRootY();
+ method public androidx.constraintlayout.core.widgets.analyzer.WidgetRun! getRun(int);
+ method public void getSceneString(StringBuilder!);
+ method public int getTop();
+ method public String! getType();
+ method public float getVerticalBiasPercent();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getVerticalChainControlWidget();
+ method public int getVerticalChainStyle();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! getVerticalDimensionBehaviour();
+ method public int getVerticalMargin();
+ method public int getVisibility();
+ method public int getWidth();
+ method public int getWrapBehaviorInParent();
+ method public int getX();
+ method public int getY();
+ method public boolean hasBaseline();
+ method public boolean hasDanglingDimension(int);
+ method public boolean hasDependencies();
+ method public boolean hasDimensionOverride();
+ method public boolean hasResolvedTargets(int, int);
+ method public void immediateConnect(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, int, int);
+ method public boolean isAnimated();
+ method public boolean isHeightWrapContent();
+ method public boolean isHorizontalSolvingPassDone();
+ method public boolean isInBarrier(int);
+ method public boolean isInHorizontalChain();
+ method public boolean isInPlaceholder();
+ method public boolean isInVerticalChain();
+ method public boolean isInVirtualLayout();
+ method public boolean isMeasureRequested();
+ method public boolean isResolvedHorizontally();
+ method public boolean isResolvedVertically();
+ method public boolean isRoot();
+ method public boolean isSpreadHeight();
+ method public boolean isSpreadWidth();
+ method public boolean isVerticalSolvingPassDone();
+ method public boolean isWidthWrapContent();
+ method public void markHorizontalSolvingPassDone();
+ method public void markVerticalSolvingPassDone();
+ method public boolean oppositeDimensionDependsOn(int);
+ method public boolean oppositeDimensionsTied();
+ method public void reset();
+ method public void resetAllConstraints();
+ method public void resetAnchor(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public void resetAnchors();
+ method public void resetFinalResolution();
+ method public void resetSolverVariables(androidx.constraintlayout.core.Cache!);
+ method public void resetSolvingPassFlag();
+ method public StringBuilder! serialize(StringBuilder!);
+ method public void setAnimated(boolean);
+ method public void setBaselineDistance(int);
+ method public void setCompanionWidget(Object!);
+ method public void setContainerItemSkip(int);
+ method public void setDebugName(String!);
+ method public void setDebugSolverName(androidx.constraintlayout.core.LinearSystem!, String!);
+ method public void setDimension(int, int);
+ method public void setDimensionRatio(float, int);
+ method public void setDimensionRatio(String!);
+ method public void setFinalBaseline(int);
+ method public void setFinalFrame(int, int, int, int, int, int);
+ method public void setFinalHorizontal(int, int);
+ method public void setFinalLeft(int);
+ method public void setFinalTop(int);
+ method public void setFinalVertical(int, int);
+ method public void setFrame(int, int, int);
+ method public void setFrame(int, int, int, int);
+ method public void setGoneMargin(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, int);
+ method public void setHasBaseline(boolean);
+ method public void setHeight(int);
+ method public void setHeightWrapContent(boolean);
+ method public void setHorizontalBiasPercent(float);
+ method public void setHorizontalChainStyle(int);
+ method public void setHorizontalDimension(int, int);
+ method public void setHorizontalDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ method public void setHorizontalMatchStyle(int, int, int, float);
+ method public void setHorizontalWeight(float);
+ method protected void setInBarrier(int, boolean);
+ method public void setInPlaceholder(boolean);
+ method public void setInVirtualLayout(boolean);
+ method public void setLastMeasureSpec(int, int);
+ method public void setLength(int, int);
+ method public void setMaxHeight(int);
+ method public void setMaxWidth(int);
+ method public void setMeasureRequested(boolean);
+ method public void setMinHeight(int);
+ method public void setMinWidth(int);
+ method public void setOffset(int, int);
+ method public void setOrigin(int, int);
+ method public void setParent(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setType(String!);
+ method public void setVerticalBiasPercent(float);
+ method public void setVerticalChainStyle(int);
+ method public void setVerticalDimension(int, int);
+ method public void setVerticalDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ method public void setVerticalMatchStyle(int, int, int, float);
+ method public void setVerticalWeight(float);
+ method public void setVisibility(int);
+ method public void setWidth(int);
+ method public void setWidthWrapContent(boolean);
+ method public void setWrapBehaviorInParent(int);
+ method public void setX(int);
+ method public void setY(int);
+ method public void setupDimensionRatio(boolean, boolean, boolean, boolean);
+ method public void updateFromRuns(boolean, boolean);
+ method public void updateFromSolver(androidx.constraintlayout.core.LinearSystem!, boolean);
+ field public static final int ANCHOR_BASELINE = 4; // 0x4
+ field public static final int ANCHOR_BOTTOM = 3; // 0x3
+ field public static final int ANCHOR_LEFT = 0; // 0x0
+ field public static final int ANCHOR_RIGHT = 1; // 0x1
+ field public static final int ANCHOR_TOP = 2; // 0x2
+ field public static final int BOTH = 2; // 0x2
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static float DEFAULT_BIAS;
+ field protected static final int DIRECT = 2; // 0x2
+ field public static final int GONE = 8; // 0x8
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int INVISIBLE = 4; // 0x4
+ field public static final int MATCH_CONSTRAINT_PERCENT = 2; // 0x2
+ field public static final int MATCH_CONSTRAINT_RATIO = 3; // 0x3
+ field public static final int MATCH_CONSTRAINT_RATIO_RESOLVED = 4; // 0x4
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field protected static final int SOLVER = 1; // 0x1
+ field public static final int UNKNOWN = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int VISIBLE = 0; // 0x0
+ field public static final int WRAP_BEHAVIOR_HORIZONTAL_ONLY = 1; // 0x1
+ field public static final int WRAP_BEHAVIOR_INCLUDED = 0; // 0x0
+ field public static final int WRAP_BEHAVIOR_SKIPPED = 3; // 0x3
+ field public static final int WRAP_BEHAVIOR_VERTICAL_ONLY = 2; // 0x2
+ field public androidx.constraintlayout.core.state.WidgetFrame! frame;
+ field public androidx.constraintlayout.core.widgets.analyzer.ChainRun! horizontalChainRun;
+ field public int horizontalGroup;
+ field public boolean[]! isTerminalWidget;
+ field protected java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintAnchor!>! mAnchors;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mBaseline;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mBottom;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mCenter;
+ field public float mCircleConstraintAngle;
+ field public float mDimensionRatio;
+ field protected int mDimensionRatioSide;
+ field public int mHorizontalResolution;
+ field public androidx.constraintlayout.core.widgets.analyzer.HorizontalWidgetRun! mHorizontalRun;
+ field public boolean mIsHeightWrapContent;
+ field public boolean mIsWidthWrapContent;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mLeft;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor![]! mListAnchors;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour![]! mListDimensionBehaviors;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget![]! mListNextMatchConstraintsWidget;
+ field public int mMatchConstraintDefaultHeight;
+ field public int mMatchConstraintDefaultWidth;
+ field public int mMatchConstraintMaxHeight;
+ field public int mMatchConstraintMaxWidth;
+ field public int mMatchConstraintMinHeight;
+ field public int mMatchConstraintMinWidth;
+ field public float mMatchConstraintPercentHeight;
+ field public float mMatchConstraintPercentWidth;
+ field protected int mMinHeight;
+ field protected int mMinWidth;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget![]! mNextChainWidget;
+ field protected int mOffsetX;
+ field protected int mOffsetY;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget! mParent;
+ field public int[]! mResolvedMatchConstraintDefault;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mRight;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mTop;
+ field public int mVerticalResolution;
+ field public androidx.constraintlayout.core.widgets.analyzer.VerticalWidgetRun! mVerticalRun;
+ field public float[]! mWeight;
+ field protected int mX;
+ field protected int mY;
+ field public boolean measured;
+ field public androidx.constraintlayout.core.widgets.analyzer.WidgetRun![]! run;
+ field public String! stringId;
+ field public androidx.constraintlayout.core.widgets.analyzer.ChainRun! verticalChainRun;
+ field public int verticalGroup;
+ }
+
+ public enum ConstraintWidget.DimensionBehaviour {
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour FIXED;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour MATCH_CONSTRAINT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour MATCH_PARENT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour WRAP_CONTENT;
+ }
+
+ public class ConstraintWidgetContainer extends androidx.constraintlayout.core.widgets.WidgetContainer {
+ ctor public ConstraintWidgetContainer();
+ ctor public ConstraintWidgetContainer(int, int);
+ ctor public ConstraintWidgetContainer(int, int, int, int);
+ ctor public ConstraintWidgetContainer(String!, int, int);
+ method public boolean addChildrenToSolver(androidx.constraintlayout.core.LinearSystem!);
+ method public void addHorizontalWrapMaxVariable(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public void addHorizontalWrapMinVariable(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public void defineTerminalWidgets();
+ method public boolean directMeasure(boolean);
+ method public boolean directMeasureSetup(boolean);
+ method public boolean directMeasureWithOrientation(boolean, int);
+ method public void fillMetrics(androidx.constraintlayout.core.Metrics!);
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.Guideline!>! getHorizontalGuidelines();
+ method public androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer! getMeasurer();
+ method public int getOptimizationLevel();
+ method public androidx.constraintlayout.core.LinearSystem! getSystem();
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.Guideline!>! getVerticalGuidelines();
+ method public boolean handlesInternalConstraints();
+ method public void invalidateGraph();
+ method public void invalidateMeasures();
+ method public boolean isHeightMeasuredTooSmall();
+ method public boolean isRtl();
+ method public boolean isWidthMeasuredTooSmall();
+ method public static boolean measure(int, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure!, int);
+ method public long measure(int, int, int, int, int, int, int, int, int);
+ method public boolean optimizeFor(int);
+ method public void setMeasurer(androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ method public void setOptimizationLevel(int);
+ method public void setPadding(int, int, int, int);
+ method public void setPass(int);
+ method public void setRtl(boolean);
+ method public boolean updateChildrenFromSolver(androidx.constraintlayout.core.LinearSystem!, boolean[]!);
+ method public void updateHierarchy();
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyGraph! mDependencyGraph;
+ field public boolean mGroupsWrapOptimized;
+ field public int mHorizontalChainsSize;
+ field public boolean mHorizontalWrapOptimized;
+ field public androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure! mMeasure;
+ field protected androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer! mMeasurer;
+ field public androidx.constraintlayout.core.Metrics! mMetrics;
+ field public boolean mSkipSolver;
+ field protected androidx.constraintlayout.core.LinearSystem! mSystem;
+ field public int mVerticalChainsSize;
+ field public boolean mVerticalWrapOptimized;
+ field public int mWrapFixedHeight;
+ field public int mWrapFixedWidth;
+ }
+
+ public class Flow extends androidx.constraintlayout.core.widgets.VirtualLayout {
+ ctor public Flow();
+ method public float getMaxElementsWrap();
+ method public void setFirstHorizontalBias(float);
+ method public void setFirstHorizontalStyle(int);
+ method public void setFirstVerticalBias(float);
+ method public void setFirstVerticalStyle(int);
+ method public void setHorizontalAlign(int);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalGap(int);
+ method public void setHorizontalStyle(int);
+ method public void setLastHorizontalBias(float);
+ method public void setLastHorizontalStyle(int);
+ method public void setLastVerticalBias(float);
+ method public void setLastVerticalStyle(int);
+ method public void setMaxElementsWrap(int);
+ method public void setOrientation(int);
+ method public void setVerticalAlign(int);
+ method public void setVerticalBias(float);
+ method public void setVerticalGap(int);
+ method public void setVerticalStyle(int);
+ method public void setWrapMode(int);
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_START = 0; // 0x0
+ field public static final int VERTICAL_ALIGN_BASELINE = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 0; // 0x0
+ field public static final int WRAP_ALIGNED = 2; // 0x2
+ field public static final int WRAP_CHAIN = 1; // 0x1
+ field public static final int WRAP_CHAIN_NEW = 3; // 0x3
+ field public static final int WRAP_NONE = 0; // 0x0
+ }
+
+ public class Guideline extends androidx.constraintlayout.core.widgets.ConstraintWidget {
+ ctor public Guideline();
+ method public void cyclePosition();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor! getAnchor();
+ method public int getMinimumPosition();
+ method public int getOrientation();
+ method public int getRelativeBegin();
+ method public int getRelativeBehaviour();
+ method public int getRelativeEnd();
+ method public float getRelativePercent();
+ method public boolean isPercent();
+ method public void setFinalValue(int);
+ method public void setGuideBegin(int);
+ method public void setGuideEnd(int);
+ method public void setGuidePercent(float);
+ method public void setGuidePercent(int);
+ method public void setMinimumPosition(int);
+ method public void setOrientation(int);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int RELATIVE_BEGIN = 1; // 0x1
+ field public static final int RELATIVE_END = 2; // 0x2
+ field public static final int RELATIVE_PERCENT = 0; // 0x0
+ field public static final int RELATIVE_UNKNOWN = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field protected boolean mGuidelineUseRtl;
+ field protected int mRelativeBegin;
+ field protected int mRelativeEnd;
+ field protected float mRelativePercent;
+ }
+
+ public interface Helper {
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void removeAllIds();
+ method public void updateConstraints(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ }
+
+ public class HelperWidget extends androidx.constraintlayout.core.widgets.ConstraintWidget implements androidx.constraintlayout.core.widgets.Helper {
+ ctor public HelperWidget();
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void addDependents(java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!, int, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public int findGroupInDependents(int);
+ method public void removeAllIds();
+ method public void updateConstraints(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget![]! mWidgets;
+ field public int mWidgetsCount;
+ }
+
+ public class Optimizer {
+ ctor public Optimizer();
+ method public static final boolean enabled(int, int);
+ field public static final int OPTIMIZATION_BARRIER = 2; // 0x2
+ field public static final int OPTIMIZATION_CACHE_MEASURES = 256; // 0x100
+ field public static final int OPTIMIZATION_CHAIN = 4; // 0x4
+ field public static final int OPTIMIZATION_DEPENDENCY_ORDERING = 512; // 0x200
+ field public static final int OPTIMIZATION_DIMENSIONS = 8; // 0x8
+ field public static final int OPTIMIZATION_DIRECT = 1; // 0x1
+ field public static final int OPTIMIZATION_GRAPH = 64; // 0x40
+ field public static final int OPTIMIZATION_GRAPH_WRAP = 128; // 0x80
+ field public static final int OPTIMIZATION_GROUPING = 1024; // 0x400
+ field public static final int OPTIMIZATION_GROUPS = 32; // 0x20
+ field public static final int OPTIMIZATION_NONE = 0; // 0x0
+ field public static final int OPTIMIZATION_RATIO = 16; // 0x10
+ field public static final int OPTIMIZATION_STANDARD = 257; // 0x101
+ }
+
+ public class Placeholder extends androidx.constraintlayout.core.widgets.VirtualLayout {
+ ctor public Placeholder();
+ }
+
+ public class Rectangle {
+ ctor public Rectangle();
+ method public boolean contains(int, int);
+ method public int getCenterX();
+ method public int getCenterY();
+ method public void setBounds(int, int, int, int);
+ field public int height;
+ field public int width;
+ field public int x;
+ field public int y;
+ }
+
+ public class VirtualLayout extends androidx.constraintlayout.core.widgets.HelperWidget {
+ ctor public VirtualLayout();
+ method public void applyRtl(boolean);
+ method public void captureWidgets();
+ method public boolean contains(java.util.HashSet<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public int getMeasuredHeight();
+ method public int getMeasuredWidth();
+ method public int getPaddingBottom();
+ method public int getPaddingLeft();
+ method public int getPaddingRight();
+ method public int getPaddingTop();
+ method protected void measure(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, int, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, int);
+ method public void measure(int, int, int, int);
+ method protected boolean measureChildren();
+ method public boolean needSolverPass();
+ method protected void needsCallbackFromSolver(boolean);
+ method public void setMeasure(int, int);
+ method public void setPadding(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingEnd(int);
+ method public void setPaddingLeft(int);
+ method public void setPaddingRight(int);
+ method public void setPaddingStart(int);
+ method public void setPaddingTop(int);
+ field protected androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure! mMeasure;
+ }
+
+ public class WidgetContainer extends androidx.constraintlayout.core.widgets.ConstraintWidget {
+ ctor public WidgetContainer();
+ ctor public WidgetContainer(int, int);
+ ctor public WidgetContainer(int, int, int, int);
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!...!);
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>! getChildren();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! getRootConstraintContainer();
+ method public void layout();
+ method public void remove(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void removeAllChildren();
+ field public java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>! mChildren;
+ }
+
+}
+
+package androidx.constraintlayout.core.widgets.analyzer {
+
+ public class BasicMeasure {
+ ctor public BasicMeasure(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ method public long solverMeasure(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int, int, int, int, int, int);
+ method public void updateHierarchy(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ field public static final int AT_MOST = -2147483648; // 0x80000000
+ field public static final int EXACTLY = 1073741824; // 0x40000000
+ field public static final int FIXED = -3; // 0xfffffffd
+ field public static final int MATCH_PARENT = -1; // 0xffffffff
+ field public static final int UNSPECIFIED = 0; // 0x0
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ }
+
+ public static class BasicMeasure.Measure {
+ ctor public BasicMeasure.Measure();
+ field public static int SELF_DIMENSIONS;
+ field public static int TRY_GIVEN_DIMENSIONS;
+ field public static int USE_GIVEN_DIMENSIONS;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! horizontalBehavior;
+ field public int horizontalDimension;
+ field public int measureStrategy;
+ field public int measuredBaseline;
+ field public boolean measuredHasBaseline;
+ field public int measuredHeight;
+ field public boolean measuredNeedsSolverPass;
+ field public int measuredWidth;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! verticalBehavior;
+ field public int verticalDimension;
+ }
+
+ public static interface BasicMeasure.Measurer {
+ method public void didMeasures();
+ method public void measure(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure!);
+ }
+
+ public class ChainRun extends androidx.constraintlayout.core.widgets.analyzer.WidgetRun {
+ ctor public ChainRun(androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ method public void applyToWidget();
+ }
+
+ public interface Dependency {
+ method public void update(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ }
+
+ public class DependencyGraph {
+ ctor public DependencyGraph(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ method public void buildGraph();
+ method public void buildGraph(java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetRun!>!);
+ method public void defineTerminalWidgets(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ method public boolean directMeasure(boolean);
+ method public boolean directMeasureSetup(boolean);
+ method public boolean directMeasureWithOrientation(boolean, int);
+ method public void invalidateGraph();
+ method public void invalidateMeasures();
+ method public void measureWidgets();
+ method public void setMeasurer(androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ }
+
+ public class DependencyNode implements androidx.constraintlayout.core.widgets.analyzer.Dependency {
+ ctor public DependencyNode(androidx.constraintlayout.core.widgets.analyzer.WidgetRun!);
+ method public void addDependency(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method public void clear();
+ method public String! name();
+ method public void resolve(int);
+ method public void update(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ field public boolean delegateToWidgetRun;
+ field public boolean readyToSolve;
+ field public boolean resolved;
+ field public androidx.constraintlayout.core.widgets.analyzer.Dependency! updateDelegate;
+ field public int value;
+ }
+
+ public class Direct {
+ ctor public Direct();
+ method public static String! ls(int);
+ method public static boolean solveChain(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.LinearSystem!, int, int, androidx.constraintlayout.core.widgets.ChainHead!, boolean, boolean, boolean);
+ method public static void solvingPass(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ }
+
+ public class Grouping {
+ ctor public Grouping();
+ method public static androidx.constraintlayout.core.widgets.analyzer.WidgetGroup! findDependents(androidx.constraintlayout.core.widgets.ConstraintWidget!, int, java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public static boolean simpleSolvingPass(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ method public static boolean validInGroup(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ }
+
+ public class HorizontalWidgetRun extends androidx.constraintlayout.core.widgets.analyzer.WidgetRun {
+ ctor public HorizontalWidgetRun(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void applyToWidget();
+ }
+
+ public class VerticalWidgetRun extends androidx.constraintlayout.core.widgets.analyzer.WidgetRun {
+ ctor public VerticalWidgetRun(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void applyToWidget();
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyNode! baseline;
+ }
+
+ public class WidgetGroup {
+ ctor public WidgetGroup(int);
+ method public boolean add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void apply();
+ method public void cleanup(java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!);
+ method public void clear();
+ method public int getId();
+ method public int getOrientation();
+ method public boolean intersectWith(androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public boolean isAuthoritative();
+ method public int measureWrap(androidx.constraintlayout.core.LinearSystem!, int);
+ method public void moveTo(int, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public void setAuthoritative(boolean);
+ method public void setOrientation(int);
+ method public int size();
+ }
+
+ public abstract class WidgetRun implements androidx.constraintlayout.core.widgets.analyzer.Dependency {
+ ctor public WidgetRun(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method protected final void addTarget(androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, int);
+ method protected final void addTarget(androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, int, androidx.constraintlayout.core.widgets.analyzer.DimensionDependency!);
+ method protected final int getLimitedDimension(int, int);
+ method protected final androidx.constraintlayout.core.widgets.analyzer.DependencyNode! getTarget(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method protected final androidx.constraintlayout.core.widgets.analyzer.DependencyNode! getTarget(androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method public long getWrapDimension();
+ method public boolean isCenterConnection();
+ method public boolean isDimensionResolved();
+ method public boolean isResolved();
+ method public void update(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method protected void updateRunCenter(androidx.constraintlayout.core.widgets.analyzer.Dependency!, androidx.constraintlayout.core.widgets.ConstraintAnchor!, androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method protected void updateRunEnd(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method protected void updateRunStart(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method public long wrapSize(int);
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyNode! end;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! mDimensionBehavior;
+ field protected androidx.constraintlayout.core.widgets.analyzer.WidgetRun.RunType! mRunType;
+ field public int matchConstraintsType;
+ field public int orientation;
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyNode! start;
+ }
+
+}
+
diff --git a/constraintlayout/constraintlayout-core/api/restricted_1.1.0-beta01.txt b/constraintlayout/constraintlayout-core/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..daaf9e5
--- /dev/null
+++ b/constraintlayout/constraintlayout-core/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,3397 @@
+// Signature format: 4.0
+package androidx.constraintlayout.core {
+
+ public class ArrayLinkedVariables implements androidx.constraintlayout.core.ArrayRow.ArrayRowVariables {
+ method public void add(androidx.constraintlayout.core.SolverVariable!, float, boolean);
+ method public final void clear();
+ method public boolean contains(androidx.constraintlayout.core.SolverVariable!);
+ method public void display();
+ method public void divideByAmount(float);
+ method public final float get(androidx.constraintlayout.core.SolverVariable!);
+ method public int getCurrentSize();
+ method public int getHead();
+ method public final int getId(int);
+ method public final int getNextIndice(int);
+ method public final float getValue(int);
+ method public androidx.constraintlayout.core.SolverVariable! getVariable(int);
+ method public float getVariableValue(int);
+ method public int indexOf(androidx.constraintlayout.core.SolverVariable!);
+ method public void invert();
+ method public final void put(androidx.constraintlayout.core.SolverVariable!, float);
+ method public final float remove(androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public int sizeInBytes();
+ method public float use(androidx.constraintlayout.core.ArrayRow!, boolean);
+ field protected final androidx.constraintlayout.core.Cache! mCache;
+ }
+
+ public class ArrayRow {
+ ctor public ArrayRow();
+ ctor public ArrayRow(androidx.constraintlayout.core.Cache!);
+ method public androidx.constraintlayout.core.ArrayRow! addError(androidx.constraintlayout.core.LinearSystem!, int);
+ method public void addError(androidx.constraintlayout.core.SolverVariable!);
+ method public void clear();
+ method public androidx.constraintlayout.core.ArrayRow! createRowDimensionRatio(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEqualDimension(float, float, float, androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEqualMatchDimensions(float, float, float, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEquals(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowEquals(androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowGreaterThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowGreaterThan(androidx.constraintlayout.core.SolverVariable!, int, androidx.constraintlayout.core.SolverVariable!);
+ method public androidx.constraintlayout.core.ArrayRow! createRowLowerThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.ArrayRow! createRowWithAngle(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public androidx.constraintlayout.core.SolverVariable! getKey();
+ method public androidx.constraintlayout.core.SolverVariable! getPivotCandidate(androidx.constraintlayout.core.LinearSystem!, boolean[]!);
+ method public void initFromRow(androidx.constraintlayout.core.LinearSystem.Row!);
+ method public boolean isEmpty();
+ method public androidx.constraintlayout.core.SolverVariable! pickPivot(androidx.constraintlayout.core.SolverVariable!);
+ method public void reset();
+ method public void updateFromFinalVariable(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public void updateFromRow(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.ArrayRow!, boolean);
+ method public void updateFromSynonymVariable(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public void updateFromSystem(androidx.constraintlayout.core.LinearSystem!);
+ field public androidx.constraintlayout.core.ArrayRow.ArrayRowVariables! variables;
+ }
+
+ public static interface ArrayRow.ArrayRowVariables {
+ method public void add(androidx.constraintlayout.core.SolverVariable!, float, boolean);
+ method public void clear();
+ method public boolean contains(androidx.constraintlayout.core.SolverVariable!);
+ method public void display();
+ method public void divideByAmount(float);
+ method public float get(androidx.constraintlayout.core.SolverVariable!);
+ method public int getCurrentSize();
+ method public androidx.constraintlayout.core.SolverVariable! getVariable(int);
+ method public float getVariableValue(int);
+ method public int indexOf(androidx.constraintlayout.core.SolverVariable!);
+ method public void invert();
+ method public void put(androidx.constraintlayout.core.SolverVariable!, float);
+ method public float remove(androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public int sizeInBytes();
+ method public float use(androidx.constraintlayout.core.ArrayRow!, boolean);
+ }
+
+ public class Cache {
+ ctor public Cache();
+ }
+
+ public class GoalRow extends androidx.constraintlayout.core.ArrayRow {
+ ctor public GoalRow(androidx.constraintlayout.core.Cache!);
+ }
+
+ public class LinearSystem {
+ ctor public LinearSystem();
+ method public void addCenterPoint(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintWidget!, float, int);
+ method public void addCentering(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, float, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addConstraint(androidx.constraintlayout.core.ArrayRow!);
+ method public androidx.constraintlayout.core.ArrayRow! addEquality(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addEquality(androidx.constraintlayout.core.SolverVariable!, int);
+ method public void addGreaterBarrier(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, boolean);
+ method public void addGreaterThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addLowerBarrier(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, boolean);
+ method public void addLowerThan(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int, int);
+ method public void addRatio(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float, int);
+ method public void addSynonym(androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, int);
+ method public androidx.constraintlayout.core.SolverVariable! createErrorVariable(int, String!);
+ method public androidx.constraintlayout.core.SolverVariable! createExtraVariable();
+ method public androidx.constraintlayout.core.SolverVariable! createObjectVariable(Object!);
+ method public androidx.constraintlayout.core.ArrayRow! createRow();
+ method public static androidx.constraintlayout.core.ArrayRow! createRowDimensionPercent(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public androidx.constraintlayout.core.SolverVariable! createSlackVariable();
+ method public void displayReadableRows();
+ method public void displayVariablesReadableRows();
+ method public void fillMetrics(androidx.constraintlayout.core.Metrics!);
+ method public androidx.constraintlayout.core.Cache! getCache();
+ method public int getMemoryUsed();
+ method public static androidx.constraintlayout.core.Metrics! getMetrics();
+ method public int getNumEquations();
+ method public int getNumVariables();
+ method public int getObjectVariableValue(Object!);
+ method public void minimize() throws java.lang.Exception;
+ method public void removeRow(androidx.constraintlayout.core.ArrayRow!);
+ method public void reset();
+ field public static long ARRAY_ROW_CREATION;
+ field public static final boolean DEBUG = false;
+ field public static final boolean FULL_DEBUG = false;
+ field public static long OPTIMIZED_ARRAY_ROW_CREATION;
+ field public static boolean OPTIMIZED_ENGINE;
+ field public static boolean SIMPLIFY_SYNONYMS;
+ field public static boolean SKIP_COLUMNS;
+ field public static boolean USE_BASIC_SYNONYMS;
+ field public static boolean USE_DEPENDENCY_ORDERING;
+ field public static boolean USE_SYNONYMS;
+ field public boolean graphOptimizer;
+ field public boolean hasSimpleDefinition;
+ field public boolean newgraphOptimizer;
+ field public static androidx.constraintlayout.core.Metrics! sMetrics;
+ }
+
+ public class Metrics {
+ ctor public Metrics();
+ method public void copy(androidx.constraintlayout.core.Metrics!);
+ method public void reset();
+ field public long additionalMeasures;
+ field public long bfs;
+ field public long constraints;
+ field public long determineGroups;
+ field public long errors;
+ field public long extravariables;
+ field public long fullySolved;
+ field public long graphOptimizer;
+ field public long graphSolved;
+ field public long grouping;
+ field public long infeasibleDetermineGroups;
+ field public long iterations;
+ field public long lastTableSize;
+ field public long layouts;
+ field public long linearSolved;
+ field public long mChildCount;
+ field public long mEquations;
+ field public long mMeasureCalls;
+ field public long mMeasureDuration;
+ field public int mNumberOfLayouts;
+ field public int mNumberOfMeasures;
+ field public long mSimpleEquations;
+ field public long mSolverPasses;
+ field public long mVariables;
+ field public long maxRows;
+ field public long maxTableSize;
+ field public long maxVariables;
+ field public long measuredMatchWidgets;
+ field public long measuredWidgets;
+ field public long measures;
+ field public long measuresLayoutDuration;
+ field public long measuresWidgetsDuration;
+ field public long measuresWrap;
+ field public long measuresWrapInfeasible;
+ field public long minimize;
+ field public long minimizeGoal;
+ field public long nonresolvedWidgets;
+ field public long optimize;
+ field public long pivots;
+ field public java.util.ArrayList<java.lang.String!>! problematicLayouts;
+ field public long resolutions;
+ field public long resolvedWidgets;
+ field public long simpleconstraints;
+ field public long slackvariables;
+ field public long tableSizeIncrease;
+ field public long variables;
+ field public long widgets;
+ }
+
+ public class PriorityGoalRow extends androidx.constraintlayout.core.ArrayRow {
+ ctor public PriorityGoalRow(androidx.constraintlayout.core.Cache!);
+ }
+
+ public class SolverVariable implements java.lang.Comparable<androidx.constraintlayout.core.SolverVariable!> {
+ ctor public SolverVariable(androidx.constraintlayout.core.SolverVariable.Type!, String!);
+ ctor public SolverVariable(String!, androidx.constraintlayout.core.SolverVariable.Type!);
+ method public final void addToRow(androidx.constraintlayout.core.ArrayRow!);
+ method public int compareTo(androidx.constraintlayout.core.SolverVariable!);
+ method public String! getName();
+ method public final void removeFromRow(androidx.constraintlayout.core.ArrayRow!);
+ method public void reset();
+ method public void setFinalValue(androidx.constraintlayout.core.LinearSystem!, float);
+ method public void setName(String!);
+ method public void setSynonym(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.SolverVariable!, float);
+ method public void setType(androidx.constraintlayout.core.SolverVariable.Type!, String!);
+ method public final void updateReferencesWithNewDefinition(androidx.constraintlayout.core.LinearSystem!, androidx.constraintlayout.core.ArrayRow!);
+ field public static final int STRENGTH_BARRIER = 6; // 0x6
+ field public static final int STRENGTH_CENTERING = 7; // 0x7
+ field public static final int STRENGTH_EQUALITY = 5; // 0x5
+ field public static final int STRENGTH_FIXED = 8; // 0x8
+ field public static final int STRENGTH_HIGH = 3; // 0x3
+ field public static final int STRENGTH_HIGHEST = 4; // 0x4
+ field public static final int STRENGTH_LOW = 1; // 0x1
+ field public static final int STRENGTH_MEDIUM = 2; // 0x2
+ field public static final int STRENGTH_NONE = 0; // 0x0
+ field public float computedValue;
+ field public int id;
+ field public boolean inGoal;
+ field public boolean isFinalValue;
+ field public int strength;
+ field public int usageInRowCount;
+ }
+
+ public enum SolverVariable.Type {
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type CONSTANT;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type ERROR;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type SLACK;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type UNKNOWN;
+ enum_constant public static final androidx.constraintlayout.core.SolverVariable.Type UNRESTRICTED;
+ }
+
+ public class SolverVariableValues implements androidx.constraintlayout.core.ArrayRow.ArrayRowVariables {
+ method public void add(androidx.constraintlayout.core.SolverVariable!, float, boolean);
+ method public void clear();
+ method public boolean contains(androidx.constraintlayout.core.SolverVariable!);
+ method public void display();
+ method public void divideByAmount(float);
+ method public float get(androidx.constraintlayout.core.SolverVariable!);
+ method public int getCurrentSize();
+ method public androidx.constraintlayout.core.SolverVariable! getVariable(int);
+ method public float getVariableValue(int);
+ method public int indexOf(androidx.constraintlayout.core.SolverVariable!);
+ method public void invert();
+ method public void put(androidx.constraintlayout.core.SolverVariable!, float);
+ method public float remove(androidx.constraintlayout.core.SolverVariable!, boolean);
+ method public int sizeInBytes();
+ method public float use(androidx.constraintlayout.core.ArrayRow!, boolean);
+ field protected final androidx.constraintlayout.core.Cache! mCache;
+ }
+
+}
+
+package androidx.constraintlayout.core.dsl {
+
+ public class Barrier extends androidx.constraintlayout.core.dsl.Helper {
+ ctor public Barrier(String!);
+ ctor public Barrier(String!, String!);
+ method public androidx.constraintlayout.core.dsl.Barrier! addReference(androidx.constraintlayout.core.dsl.Ref!);
+ method public androidx.constraintlayout.core.dsl.Barrier! addReference(String!);
+ method public androidx.constraintlayout.core.dsl.Constraint.Side! getDirection();
+ method public int getMargin();
+ method public String! referencesToString();
+ method public void setDirection(androidx.constraintlayout.core.dsl.Constraint.Side!);
+ method public void setMargin(int);
+ }
+
+ public abstract class Chain extends androidx.constraintlayout.core.dsl.Helper {
+ ctor public Chain(String!);
+ method public androidx.constraintlayout.core.dsl.Chain! addReference(androidx.constraintlayout.core.dsl.Ref!);
+ method public androidx.constraintlayout.core.dsl.Chain! addReference(String!);
+ method public androidx.constraintlayout.core.dsl.Chain.Style! getStyle();
+ method public String! referencesToString();
+ method public void setStyle(androidx.constraintlayout.core.dsl.Chain.Style!);
+ field protected java.util.ArrayList<androidx.constraintlayout.core.dsl.Ref!>! references;
+ field protected static final java.util.Map<androidx.constraintlayout.core.dsl.Chain.Style!,java.lang.String!>! styleMap;
+ }
+
+ public class Chain.Anchor {
+ method public void build(StringBuilder!);
+ method public String! getId();
+ }
+
+ public enum Chain.Style {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Chain.Style PACKED;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Chain.Style SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Chain.Style SPREAD_INSIDE;
+ }
+
+ public class Constraint {
+ ctor public Constraint(String!);
+ method protected void append(StringBuilder!, String!, float);
+ method public String! convertStringArrayToString(String![]!);
+ method public androidx.constraintlayout.core.dsl.Constraint.VAnchor! getBaseline();
+ method public androidx.constraintlayout.core.dsl.Constraint.VAnchor! getBottom();
+ method public float getCircleAngle();
+ method public String! getCircleConstraint();
+ method public int getCircleRadius();
+ method public String! getDimensionRatio();
+ method public int getEditorAbsoluteX();
+ method public int getEditorAbsoluteY();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getEnd();
+ method public int getHeight();
+ method public androidx.constraintlayout.core.dsl.Constraint.Behaviour! getHeightDefault();
+ method public int getHeightMax();
+ method public int getHeightMin();
+ method public float getHeightPercent();
+ method public float getHorizontalBias();
+ method public androidx.constraintlayout.core.dsl.Constraint.ChainMode! getHorizontalChainStyle();
+ method public float getHorizontalWeight();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getLeft();
+ method public String![]! getReferenceIds();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getRight();
+ method public androidx.constraintlayout.core.dsl.Constraint.HAnchor! getStart();
+ method public androidx.constraintlayout.core.dsl.Constraint.VAnchor! getTop();
+ method public float getVerticalBias();
+ method public androidx.constraintlayout.core.dsl.Constraint.ChainMode! getVerticalChainStyle();
+ method public float getVerticalWeight();
+ method public int getWidth();
+ method public androidx.constraintlayout.core.dsl.Constraint.Behaviour! getWidthDefault();
+ method public int getWidthMax();
+ method public int getWidthMin();
+ method public float getWidthPercent();
+ method public boolean isConstrainedHeight();
+ method public boolean isConstrainedWidth();
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void setCircleAngle(float);
+ method public void setCircleConstraint(String!);
+ method public void setCircleRadius(int);
+ method public void setConstrainedHeight(boolean);
+ method public void setConstrainedWidth(boolean);
+ method public void setDimensionRatio(String!);
+ method public void setEditorAbsoluteX(int);
+ method public void setEditorAbsoluteY(int);
+ method public void setHeight(int);
+ method public void setHeightDefault(androidx.constraintlayout.core.dsl.Constraint.Behaviour!);
+ method public void setHeightMax(int);
+ method public void setHeightMin(int);
+ method public void setHeightPercent(float);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalChainStyle(androidx.constraintlayout.core.dsl.Constraint.ChainMode!);
+ method public void setHorizontalWeight(float);
+ method public void setReferenceIds(String![]!);
+ method public void setVerticalBias(float);
+ method public void setVerticalChainStyle(androidx.constraintlayout.core.dsl.Constraint.ChainMode!);
+ method public void setVerticalWeight(float);
+ method public void setWidth(int);
+ method public void setWidthDefault(androidx.constraintlayout.core.dsl.Constraint.Behaviour!);
+ method public void setWidthMax(int);
+ method public void setWidthMin(int);
+ method public void setWidthPercent(float);
+ field public static final androidx.constraintlayout.core.dsl.Constraint! PARENT;
+ }
+
+ public class Constraint.Anchor {
+ method public void build(StringBuilder!);
+ method public String! getId();
+ }
+
+ public enum Constraint.Behaviour {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour PERCENT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour RATIO;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour RESOLVED;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Behaviour WRAP;
+ }
+
+ public enum Constraint.ChainMode {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.ChainMode PACKED;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.ChainMode SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.ChainMode SPREAD_INSIDE;
+ }
+
+ public class Constraint.HAnchor extends androidx.constraintlayout.core.dsl.Constraint.Anchor {
+ }
+
+ public enum Constraint.HSide {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.HSide START;
+ }
+
+ public enum Constraint.Side {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.Side TOP;
+ }
+
+ public class Constraint.VAnchor extends androidx.constraintlayout.core.dsl.Constraint.Anchor {
+ }
+
+ public enum Constraint.VSide {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.VSide BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.VSide BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Constraint.VSide TOP;
+ }
+
+ public class ConstraintSet {
+ ctor public ConstraintSet(String!);
+ method public void add(androidx.constraintlayout.core.dsl.Constraint!);
+ method public void add(androidx.constraintlayout.core.dsl.Helper!);
+ }
+
+ public abstract class Guideline extends androidx.constraintlayout.core.dsl.Helper {
+ method public int getEnd();
+ method public float getPercent();
+ method public int getStart();
+ method public void setEnd(int);
+ method public void setPercent(float);
+ method public void setStart(int);
+ }
+
+ public class HChain extends androidx.constraintlayout.core.dsl.Chain {
+ ctor public HChain(String!);
+ ctor public HChain(String!, String!);
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getEnd();
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getLeft();
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getRight();
+ method public androidx.constraintlayout.core.dsl.HChain.HAnchor! getStart();
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToEnd(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToLeft(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToRight(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int);
+ method public void linkToStart(androidx.constraintlayout.core.dsl.Constraint.HAnchor!, int, int);
+ }
+
+ public class HChain.HAnchor extends androidx.constraintlayout.core.dsl.Chain.Anchor {
+ }
+
+ public class Helper {
+ ctor public Helper(String!, androidx.constraintlayout.core.dsl.Helper.HelperType!);
+ ctor public Helper(String!, androidx.constraintlayout.core.dsl.Helper.HelperType!, String!);
+ method public void append(java.util.Map<java.lang.String!,java.lang.String!>!, StringBuilder!);
+ method public java.util.Map<java.lang.String!,java.lang.String!>! convertConfigToMap();
+ method public String! getConfig();
+ method public String! getId();
+ method public androidx.constraintlayout.core.dsl.Helper.HelperType! getType();
+ method public static void main(String![]!);
+ field protected String! config;
+ field protected java.util.Map<java.lang.String!,java.lang.String!>! configMap;
+ field protected final String! name;
+ field protected static final java.util.Map<androidx.constraintlayout.core.dsl.Constraint.Side!,java.lang.String!>! sideMap;
+ field protected androidx.constraintlayout.core.dsl.Helper.HelperType! type;
+ field protected static final java.util.Map<androidx.constraintlayout.core.dsl.Helper.Type!,java.lang.String!>! typeMap;
+ }
+
+ public static final class Helper.HelperType {
+ ctor public Helper.HelperType(String!);
+ }
+
+ public enum Helper.Type {
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type BARRIER;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type HORIZONTAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type HORIZONTAL_GUIDELINE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type VERTICAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.Helper.Type VERTICAL_GUIDELINE;
+ }
+
+ public class KeyAttribute extends androidx.constraintlayout.core.dsl.Keys {
+ ctor public KeyAttribute(int, String!);
+ method protected void attributesToString(StringBuilder!);
+ method public float getAlpha();
+ method public androidx.constraintlayout.core.dsl.KeyAttribute.Fit! getCurveFit();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public float getRotation();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public String! getTarget();
+ method public String! getTransitionEasing();
+ method public float getTransitionPathRotate();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public androidx.constraintlayout.core.dsl.KeyAttribute.Visibility! getVisibility();
+ method public void setAlpha(float);
+ method public void setCurveFit(androidx.constraintlayout.core.dsl.KeyAttribute.Fit!);
+ method public void setPivotX(float);
+ method public void setPivotY(float);
+ method public void setRotation(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTarget(String!);
+ method public void setTransitionEasing(String!);
+ method public void setTransitionPathRotate(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ method public void setVisibility(androidx.constraintlayout.core.dsl.KeyAttribute.Visibility!);
+ field protected String! TYPE;
+ }
+
+ public enum KeyAttribute.Fit {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Fit LINEAR;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Fit SPLINE;
+ }
+
+ public enum KeyAttribute.Visibility {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Visibility GONE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Visibility INVISIBLE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttribute.Visibility VISIBLE;
+ }
+
+ public class KeyAttributes extends androidx.constraintlayout.core.dsl.Keys {
+ method protected void attributesToString(StringBuilder!);
+ method public float[]! getAlpha();
+ method public androidx.constraintlayout.core.dsl.KeyAttributes.Fit! getCurveFit();
+ method public float[]! getPivotX();
+ method public float[]! getPivotY();
+ method public float[]! getRotation();
+ method public float[]! getRotationX();
+ method public float[]! getRotationY();
+ method public float[]! getScaleX();
+ method public float[]! getScaleY();
+ method public String![]! getTarget();
+ method public String! getTransitionEasing();
+ method public float[]! getTransitionPathRotate();
+ method public float[]! getTranslationX();
+ method public float[]! getTranslationY();
+ method public float[]! getTranslationZ();
+ method public androidx.constraintlayout.core.dsl.KeyAttributes.Visibility![]! getVisibility();
+ method public void setAlpha(float...!);
+ method public void setCurveFit(androidx.constraintlayout.core.dsl.KeyAttributes.Fit!);
+ method public void setPivotX(float...!);
+ method public void setPivotY(float...!);
+ method public void setRotation(float...!);
+ method public void setRotationX(float...!);
+ method public void setRotationY(float...!);
+ method public void setScaleX(float[]!);
+ method public void setScaleY(float[]!);
+ method public void setTarget(String![]!);
+ method public void setTransitionEasing(String!);
+ method public void setTransitionPathRotate(float...!);
+ method public void setTranslationX(float[]!);
+ method public void setTranslationY(float[]!);
+ method public void setTranslationZ(float[]!);
+ method public void setVisibility(androidx.constraintlayout.core.dsl.KeyAttributes.Visibility!...!);
+ field protected String! TYPE;
+ }
+
+ public enum KeyAttributes.Fit {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Fit LINEAR;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Fit SPLINE;
+ }
+
+ public enum KeyAttributes.Visibility {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Visibility GONE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Visibility INVISIBLE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyAttributes.Visibility VISIBLE;
+ }
+
+ public class KeyCycle extends androidx.constraintlayout.core.dsl.KeyAttribute {
+ method public float getOffset();
+ method public float getPeriod();
+ method public float getPhase();
+ method public androidx.constraintlayout.core.dsl.KeyCycle.Wave! getShape();
+ method public void setOffset(float);
+ method public void setPeriod(float);
+ method public void setPhase(float);
+ method public void setShape(androidx.constraintlayout.core.dsl.KeyCycle.Wave!);
+ }
+
+ public enum KeyCycle.Wave {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave COS;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave REVERSE_SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave SIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave SQUARE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycle.Wave TRIANGLE;
+ }
+
+ public class KeyCycles extends androidx.constraintlayout.core.dsl.KeyAttributes {
+ method public float[]! getWaveOffset();
+ method public float[]! getWavePeriod();
+ method public float[]! getWavePhase();
+ method public androidx.constraintlayout.core.dsl.KeyCycles.Wave! getWaveShape();
+ method public void setWaveOffset(float...!);
+ method public void setWavePeriod(float...!);
+ method public void setWavePhase(float...!);
+ method public void setWaveShape(androidx.constraintlayout.core.dsl.KeyCycles.Wave!);
+ }
+
+ public enum KeyCycles.Wave {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave COS;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave REVERSE_SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave SAW;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave SIN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave SQUARE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyCycles.Wave TRIANGLE;
+ }
+
+ public class KeyFrames {
+ ctor public KeyFrames();
+ method public void add(androidx.constraintlayout.core.dsl.Keys!);
+ }
+
+ public class KeyPosition extends androidx.constraintlayout.core.dsl.Keys {
+ ctor public KeyPosition(String!, int);
+ method public int getFrames();
+ method public float getPercentHeight();
+ method public float getPercentWidth();
+ method public float getPercentX();
+ method public float getPercentY();
+ method public androidx.constraintlayout.core.dsl.KeyPosition.Type! getPositionType();
+ method public String! getTarget();
+ method public String! getTransitionEasing();
+ method public void setFrames(int);
+ method public void setPercentHeight(float);
+ method public void setPercentWidth(float);
+ method public void setPercentX(float);
+ method public void setPercentY(float);
+ method public void setPositionType(androidx.constraintlayout.core.dsl.KeyPosition.Type!);
+ method public void setTarget(String!);
+ method public void setTransitionEasing(String!);
+ }
+
+ public enum KeyPosition.Type {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPosition.Type CARTESIAN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPosition.Type PATH;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPosition.Type SCREEN;
+ }
+
+ public class KeyPositions extends androidx.constraintlayout.core.dsl.Keys {
+ ctor public KeyPositions(int, java.lang.String!...!);
+ method public int[]! getFrames();
+ method public float[]! getPercentHeight();
+ method public float[]! getPercentWidth();
+ method public float[]! getPercentX();
+ method public float[]! getPercentY();
+ method public androidx.constraintlayout.core.dsl.KeyPositions.Type! getPositionType();
+ method public String![]! getTarget();
+ method public String! getTransitionEasing();
+ method public void setFrames(int...!);
+ method public void setPercentHeight(float...!);
+ method public void setPercentWidth(float...!);
+ method public void setPercentX(float...!);
+ method public void setPercentY(float...!);
+ method public void setPositionType(androidx.constraintlayout.core.dsl.KeyPositions.Type!);
+ method public void setTransitionEasing(String!);
+ }
+
+ public enum KeyPositions.Type {
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPositions.Type CARTESIAN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPositions.Type PATH;
+ enum_constant public static final androidx.constraintlayout.core.dsl.KeyPositions.Type SCREEN;
+ }
+
+ public class Keys {
+ ctor public Keys();
+ method protected void append(StringBuilder!, String!, float);
+ method protected void append(StringBuilder!, String!, float[]!);
+ method protected void append(StringBuilder!, String!, int);
+ method protected void append(StringBuilder!, String!, String!);
+ method protected void append(StringBuilder!, String!, String![]!);
+ method protected String! unpack(String![]!);
+ }
+
+ public class MotionScene {
+ ctor public MotionScene();
+ method public void addConstraintSet(androidx.constraintlayout.core.dsl.ConstraintSet!);
+ method public void addTransition(androidx.constraintlayout.core.dsl.Transition!);
+ }
+
+ public class OnSwipe {
+ ctor public OnSwipe();
+ ctor public OnSwipe(String!, androidx.constraintlayout.core.dsl.OnSwipe.Side!, androidx.constraintlayout.core.dsl.OnSwipe.Drag!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Mode! getAutoCompleteMode();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Drag! getDragDirection();
+ method public float getDragScale();
+ method public float getDragThreshold();
+ method public String! getLimitBoundsTo();
+ method public float getMaxAcceleration();
+ method public float getMaxVelocity();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.TouchUp! getOnTouchUp();
+ method public String! getRotationCenterId();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Boundary! getSpringBoundary();
+ method public float getSpringDamping();
+ method public float getSpringMass();
+ method public float getSpringStiffness();
+ method public float getSpringStopThreshold();
+ method public String! getTouchAnchorId();
+ method public androidx.constraintlayout.core.dsl.OnSwipe.Side! getTouchAnchorSide();
+ method public void setAutoCompleteMode(androidx.constraintlayout.core.dsl.OnSwipe.Mode!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setDragDirection(androidx.constraintlayout.core.dsl.OnSwipe.Drag!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setDragScale(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setDragThreshold(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setLimitBoundsTo(String!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setMaxAcceleration(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setMaxVelocity(int);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setOnTouchUp(androidx.constraintlayout.core.dsl.OnSwipe.TouchUp!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setRotateCenter(String!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringBoundary(androidx.constraintlayout.core.dsl.OnSwipe.Boundary!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringDamping(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringMass(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringStiffness(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setSpringStopThreshold(float);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setTouchAnchorId(String!);
+ method public androidx.constraintlayout.core.dsl.OnSwipe! setTouchAnchorSide(androidx.constraintlayout.core.dsl.OnSwipe.Side!);
+ field public static final int FLAG_DISABLE_POST_SCROLL = 1; // 0x1
+ field public static final int FLAG_DISABLE_SCROLL = 2; // 0x2
+ }
+
+ public enum OnSwipe.Boundary {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary BOUNCE_BOTH;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary BOUNCE_END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary BOUNCE_START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Boundary OVERSHOOT;
+ }
+
+ public enum OnSwipe.Drag {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag ANTICLOCKWISE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag CLOCKWISE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag DOWN;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Drag UP;
+ }
+
+ public enum OnSwipe.Mode {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Mode SPRING;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Mode VELOCITY;
+ }
+
+ public enum OnSwipe.Side {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side LEFT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side MIDDLE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.Side TOP;
+ }
+
+ public enum OnSwipe.TouchUp {
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp AUTOCOMPLETE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp DECELERATE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp DECELERATE_COMPLETE;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp NEVER_COMPLETE_END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp NEVER_COMPLETE_START;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp STOP;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp TO_END;
+ enum_constant public static final androidx.constraintlayout.core.dsl.OnSwipe.TouchUp TO_START;
+ }
+
+ public class Ref {
+ method public static void addStringToReferences(String!, java.util.ArrayList<androidx.constraintlayout.core.dsl.Ref!>!);
+ method public String! getId();
+ method public float getPostMargin();
+ method public float getPreMargin();
+ method public float getWeight();
+ method public static float parseFloat(Object!);
+ method public static androidx.constraintlayout.core.dsl.Ref! parseStringToRef(String!);
+ method public void setId(String!);
+ method public void setPostMargin(float);
+ method public void setPreMargin(float);
+ method public void setWeight(float);
+ }
+
+ public class Transition {
+ ctor public Transition(String!, String!);
+ ctor public Transition(String!, String!, String!);
+ method public String! getId();
+ method public void setDuration(int);
+ method public void setFrom(String!);
+ method public void setId(String!);
+ method public void setKeyFrames(androidx.constraintlayout.core.dsl.Keys!);
+ method public void setOnSwipe(androidx.constraintlayout.core.dsl.OnSwipe!);
+ method public void setStagger(float);
+ method public void setTo(String!);
+ }
+
+ public class VChain extends androidx.constraintlayout.core.dsl.Chain {
+ ctor public VChain(String!);
+ ctor public VChain(String!, String!);
+ method public androidx.constraintlayout.core.dsl.VChain.VAnchor! getBaseline();
+ method public androidx.constraintlayout.core.dsl.VChain.VAnchor! getBottom();
+ method public androidx.constraintlayout.core.dsl.VChain.VAnchor! getTop();
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBaseline(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToBottom(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int);
+ method public void linkToTop(androidx.constraintlayout.core.dsl.Constraint.VAnchor!, int, int);
+ }
+
+ public class VChain.VAnchor extends androidx.constraintlayout.core.dsl.Chain.Anchor {
+ }
+
+ public class VGuideline extends androidx.constraintlayout.core.dsl.Guideline {
+ ctor public VGuideline(String!);
+ ctor public VGuideline(String!, String!);
+ }
+
+}
+
+package androidx.constraintlayout.core.motion {
+
+ public class CustomAttribute {
+ ctor public CustomAttribute(androidx.constraintlayout.core.motion.CustomAttribute!, Object!);
+ ctor public CustomAttribute(String!, androidx.constraintlayout.core.motion.CustomAttribute.AttributeType!);
+ ctor public CustomAttribute(String!, androidx.constraintlayout.core.motion.CustomAttribute.AttributeType!, Object!, boolean);
+ method public boolean diff(androidx.constraintlayout.core.motion.CustomAttribute!);
+ method public androidx.constraintlayout.core.motion.CustomAttribute.AttributeType! getType();
+ method public float getValueToInterpolate();
+ method public void getValuesToInterpolate(float[]!);
+ method public static int hsvToRgb(float, float, float);
+ method public boolean isContinuous();
+ method public int numberOfInterpolatedValues();
+ method public void setColorValue(int);
+ method public void setFloatValue(float);
+ method public void setIntValue(int);
+ method public void setStringValue(String!);
+ method public void setValue(float[]!);
+ method public void setValue(Object!);
+ }
+
+ public enum CustomAttribute.AttributeType {
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType BOOLEAN_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType COLOR_DRAWABLE_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType COLOR_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType DIMENSION_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType FLOAT_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType INT_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType REFERENCE_TYPE;
+ enum_constant public static final androidx.constraintlayout.core.motion.CustomAttribute.AttributeType STRING_TYPE;
+ }
+
+ public class CustomVariable {
+ ctor public CustomVariable(androidx.constraintlayout.core.motion.CustomVariable!);
+ ctor public CustomVariable(androidx.constraintlayout.core.motion.CustomVariable!, Object!);
+ ctor public CustomVariable(String!, int);
+ ctor public CustomVariable(String!, int, boolean);
+ ctor public CustomVariable(String!, int, float);
+ ctor public CustomVariable(String!, int, int);
+ ctor public CustomVariable(String!, int, Object!);
+ ctor public CustomVariable(String!, int, String!);
+ method public void applyToWidget(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public static String! colorString(int);
+ method public androidx.constraintlayout.core.motion.CustomVariable! copy();
+ method public boolean diff(androidx.constraintlayout.core.motion.CustomVariable!);
+ method public boolean getBooleanValue();
+ method public int getColorValue();
+ method public float getFloatValue();
+ method public int getIntegerValue();
+ method public int getInterpolatedColor(float[]!);
+ method public String! getName();
+ method public String! getStringValue();
+ method public int getType();
+ method public float getValueToInterpolate();
+ method public void getValuesToInterpolate(float[]!);
+ method public static int hsvToRgb(float, float, float);
+ method public boolean isContinuous();
+ method public int numberOfInterpolatedValues();
+ method public static int rgbaTocColor(float, float, float, float);
+ method public void setBooleanValue(boolean);
+ method public void setFloatValue(float);
+ method public void setIntValue(int);
+ method public void setInterpolatedValue(androidx.constraintlayout.core.motion.MotionWidget!, float[]!);
+ method public void setStringValue(String!);
+ method public void setValue(float[]!);
+ method public void setValue(Object!);
+ }
+
+ public class Motion implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public Motion(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void addKey(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public int buildKeyFrames(float[]!, int[]!, int[]!);
+ method public void buildPath(float[]!, int);
+ method public void buildRect(float, float[]!, int);
+ method public String! getAnimateRelativeTo();
+ method public void getCenter(double, float[]!, float[]!);
+ method public float getCenterX();
+ method public float getCenterY();
+ method public void getDpDt(float, float, float, float[]!);
+ method public int getDrawPath();
+ method public float getFinalHeight();
+ method public float getFinalWidth();
+ method public float getFinalX();
+ method public float getFinalY();
+ method public int getId(String!);
+ method public androidx.constraintlayout.core.motion.MotionPaths! getKeyFrame(int);
+ method public int getKeyFrameInfo(int, int[]!);
+ method public int getKeyFramePositions(int[]!, float[]!);
+ method public float getMotionStagger();
+ method public float getStartHeight();
+ method public float getStartWidth();
+ method public float getStartX();
+ method public float getStartY();
+ method public int getTransformPivotTarget();
+ method public androidx.constraintlayout.core.motion.MotionWidget! getView();
+ method public boolean interpolate(androidx.constraintlayout.core.motion.MotionWidget!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ method public void setDrawPath(int);
+ method public void setEnd(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void setIdString(String!);
+ method public void setPathMotionArc(int);
+ method public void setStaggerOffset(float);
+ method public void setStaggerScale(float);
+ method public void setStart(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void setStartState(androidx.constraintlayout.core.motion.utils.ViewState!, androidx.constraintlayout.core.motion.MotionWidget!, int, int, int);
+ method public void setTransformPivotTarget(int);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public void setView(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public void setup(int, int, float, long);
+ method public void setupRelative(androidx.constraintlayout.core.motion.Motion!);
+ field public static final int DRAW_PATH_AS_CONFIGURED = 4; // 0x4
+ field public static final int DRAW_PATH_BASIC = 1; // 0x1
+ field public static final int DRAW_PATH_CARTESIAN = 3; // 0x3
+ field public static final int DRAW_PATH_NONE = 0; // 0x0
+ field public static final int DRAW_PATH_RECTANGLE = 5; // 0x5
+ field public static final int DRAW_PATH_RELATIVE = 2; // 0x2
+ field public static final int DRAW_PATH_SCREEN = 6; // 0x6
+ field public static final int HORIZONTAL_PATH_X = 2; // 0x2
+ field public static final int HORIZONTAL_PATH_Y = 3; // 0x3
+ field public static final int PATH_PERCENT = 0; // 0x0
+ field public static final int PATH_PERPENDICULAR = 1; // 0x1
+ field public static final int ROTATION_LEFT = 2; // 0x2
+ field public static final int ROTATION_RIGHT = 1; // 0x1
+ field public static final int VERTICAL_PATH_X = 4; // 0x4
+ field public static final int VERTICAL_PATH_Y = 5; // 0x5
+ field public String! mId;
+ }
+
+ public class MotionPaths implements java.lang.Comparable<androidx.constraintlayout.core.motion.MotionPaths!> {
+ ctor public MotionPaths();
+ ctor public MotionPaths(int, int, androidx.constraintlayout.core.motion.key.MotionKeyPosition!, androidx.constraintlayout.core.motion.MotionPaths!, androidx.constraintlayout.core.motion.MotionPaths!);
+ method public void applyParameters(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public int compareTo(androidx.constraintlayout.core.motion.MotionPaths!);
+ method public void configureRelativeTo(androidx.constraintlayout.core.motion.Motion!);
+ method public void setupRelative(androidx.constraintlayout.core.motion.Motion!, androidx.constraintlayout.core.motion.MotionPaths!);
+ field public static final int CARTESIAN = 0; // 0x0
+ field public static final boolean DEBUG = false;
+ field public static final boolean OLD_WAY = false;
+ field public static final int PERPENDICULAR = 1; // 0x1
+ field public static final int SCREEN = 2; // 0x2
+ field public static final String TAG = "MotionPaths";
+ field public String! mId;
+ }
+
+ public class MotionWidget implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public MotionWidget();
+ ctor public MotionWidget(androidx.constraintlayout.core.state.WidgetFrame!);
+ method public androidx.constraintlayout.core.motion.MotionWidget! findViewById(int);
+ method public float getAlpha();
+ method public int getBottom();
+ method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
+ method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
+ method public int getHeight();
+ method public int getId(String!);
+ method public int getLeft();
+ method public String! getName();
+ method public androidx.constraintlayout.core.motion.MotionWidget! getParent();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public int getRight();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public int getTop();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public float getValueAttributes(int);
+ method public int getVisibility();
+ method public androidx.constraintlayout.core.state.WidgetFrame! getWidgetFrame();
+ method public int getWidth();
+ method public int getX();
+ method public int getY();
+ method public void layout(int, int, int, int);
+ method public void setBounds(int, int, int, int);
+ method public void setCustomAttribute(String!, int, boolean);
+ method public void setCustomAttribute(String!, int, float);
+ method public void setCustomAttribute(String!, int, int);
+ method public void setCustomAttribute(String!, int, String!);
+ method public void setInterpolatedValue(androidx.constraintlayout.core.motion.CustomAttribute!, float[]!);
+ method public void setPivotX(float);
+ method public void setPivotY(float);
+ method public void setRotationX(float);
+ method public void setRotationY(float);
+ method public void setRotationZ(float);
+ method public void setScaleX(float);
+ method public void setScaleY(float);
+ method public void setTranslationX(float);
+ method public void setTranslationY(float);
+ method public void setTranslationZ(float);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public boolean setValueAttributes(int, float);
+ method public boolean setValueMotion(int, float);
+ method public boolean setValueMotion(int, int);
+ method public boolean setValueMotion(int, String!);
+ method public void setVisibility(int);
+ method public void updateMotion(androidx.constraintlayout.core.motion.utils.TypedValues!);
+ field public static final int FILL_PARENT = -1; // 0xffffffff
+ field public static final int GONE_UNSET = -2147483648; // 0x80000000
+ field public static final int INVISIBLE = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int MATCH_PARENT = -1; // 0xffffffff
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int ROTATE_LEFT_OF_PORTRATE = 4; // 0x4
+ field public static final int ROTATE_NONE = 0; // 0x0
+ field public static final int ROTATE_PORTRATE_OF_LEFT = 2; // 0x2
+ field public static final int ROTATE_PORTRATE_OF_RIGHT = 1; // 0x1
+ field public static final int ROTATE_RIGHT_OF_PORTRATE = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int VISIBILITY_MODE_IGNORE = 1; // 0x1
+ field public static final int VISIBILITY_MODE_NORMAL = 0; // 0x0
+ field public static final int VISIBLE = 4; // 0x4
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ }
+
+ public static class MotionWidget.Motion {
+ ctor public MotionWidget.Motion();
+ field public int mAnimateCircleAngleTo;
+ field public String! mAnimateRelativeTo;
+ field public int mDrawPath;
+ field public float mMotionStagger;
+ field public int mPathMotionArc;
+ field public float mPathRotate;
+ field public int mPolarRelativeTo;
+ field public int mQuantizeInterpolatorID;
+ field public String! mQuantizeInterpolatorString;
+ field public int mQuantizeInterpolatorType;
+ field public float mQuantizeMotionPhase;
+ field public int mQuantizeMotionSteps;
+ field public String! mTransitionEasing;
+ }
+
+ public static class MotionWidget.PropertySet {
+ ctor public MotionWidget.PropertySet();
+ field public float alpha;
+ field public float mProgress;
+ field public int mVisibilityMode;
+ field public int visibility;
+ }
+
+}
+
+package androidx.constraintlayout.core.motion.key {
+
+ public class MotionConstraintSet {
+ ctor public MotionConstraintSet();
+ field public static final int ROTATE_LEFT_OF_PORTRATE = 4; // 0x4
+ field public static final int ROTATE_NONE = 0; // 0x0
+ field public static final int ROTATE_PORTRATE_OF_LEFT = 2; // 0x2
+ field public static final int ROTATE_PORTRATE_OF_RIGHT = 1; // 0x1
+ field public static final int ROTATE_RIGHT_OF_PORTRATE = 3; // 0x3
+ field public String! mIdString;
+ field public int mRotate;
+ }
+
+ public abstract class MotionKey implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public MotionKey();
+ method public abstract void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public abstract androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public androidx.constraintlayout.core.motion.key.MotionKey! copy(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public abstract void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getFramePosition();
+ method public void setCustomAttribute(String!, int, boolean);
+ method public void setCustomAttribute(String!, int, float);
+ method public void setCustomAttribute(String!, int, int);
+ method public void setCustomAttribute(String!, int, String!);
+ method public void setFramePosition(int);
+ method public void setInterpolation(java.util.HashMap<java.lang.String!,java.lang.Integer!>!);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! setViewId(int);
+ field public static final String ALPHA = "alpha";
+ field public static final String CUSTOM = "CUSTOM";
+ field public static final String ELEVATION = "elevation";
+ field public static final String ROTATION = "rotationZ";
+ field public static final String ROTATION_X = "rotationX";
+ field public static final String SCALE_X = "scaleX";
+ field public static final String SCALE_Y = "scaleY";
+ field public static final String TRANSITION_PATH_ROTATE = "transitionPathRotate";
+ field public static final String TRANSLATION_X = "translationX";
+ field public static final String TRANSLATION_Y = "translationY";
+ field public static int UNSET;
+ field public static final String VISIBILITY = "visibility";
+ field public java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.CustomVariable!>! mCustom;
+ field public int mFramePosition;
+ field public int mType;
+ }
+
+ public class MotionKeyAttributes extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyAttributes();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getCurveFit();
+ method public int getId(String!);
+ method public void printAttributes();
+ field public static final int KEY_TYPE = 1; // 0x1
+ }
+
+ public class MotionKeyCycle extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyCycle();
+ method public void addCycleValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void dump();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ method public float getValue(String!);
+ method public void printAttributes();
+ field public static final int KEY_TYPE = 4; // 0x4
+ field public static final int SHAPE_BOUNCE = 6; // 0x6
+ field public static final int SHAPE_COS_WAVE = 5; // 0x5
+ field public static final int SHAPE_REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SHAPE_SAW_WAVE = 3; // 0x3
+ field public static final int SHAPE_SIN_WAVE = 0; // 0x0
+ field public static final int SHAPE_SQUARE_WAVE = 1; // 0x1
+ field public static final int SHAPE_TRIANGLE_WAVE = 2; // 0x2
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_PHASE = "wavePhase";
+ field public static final String WAVE_SHAPE = "waveShape";
+ }
+
+ public class MotionKeyPosition extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyPosition();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ method public boolean intersects(int, int, androidx.constraintlayout.core.motion.utils.FloatRect!, androidx.constraintlayout.core.motion.utils.FloatRect!, float, float);
+ method public void positionAttributes(androidx.constraintlayout.core.motion.MotionWidget!, androidx.constraintlayout.core.motion.utils.FloatRect!, androidx.constraintlayout.core.motion.utils.FloatRect!, float, float, String![]!, float[]!);
+ field protected static final float SELECTION_SLOPE = 20.0f;
+ field public static final int TYPE_CARTESIAN = 0; // 0x0
+ field public static final int TYPE_PATH = 1; // 0x1
+ field public static final int TYPE_SCREEN = 2; // 0x2
+ field public float mAltPercentX;
+ field public float mAltPercentY;
+ field public int mCurveFit;
+ field public int mDrawPath;
+ field public int mPathMotionArc;
+ field public float mPercentHeight;
+ field public float mPercentWidth;
+ field public float mPercentX;
+ field public float mPercentY;
+ field public int mPositionType;
+ field public String! mTransitionEasing;
+ }
+
+ public class MotionKeyTimeCycle extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyTimeCycle();
+ method public void addTimeValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public androidx.constraintlayout.core.motion.key.MotionKeyTimeCycle! copy(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ field public static final int KEY_TYPE = 3; // 0x3
+ }
+
+ public class MotionKeyTrigger extends androidx.constraintlayout.core.motion.key.MotionKey {
+ ctor public MotionKeyTrigger();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.core.motion.utils.SplineSet!>!);
+ method public androidx.constraintlayout.core.motion.key.MotionKey! clone();
+ method public void conditionallyFire(float, androidx.constraintlayout.core.motion.MotionWidget!);
+ method public androidx.constraintlayout.core.motion.key.MotionKeyTrigger! copy(androidx.constraintlayout.core.motion.key.MotionKey!);
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public int getId(String!);
+ field public static final String CROSS = "CROSS";
+ field public static final int KEY_TYPE = 5; // 0x5
+ field public static final String NEGATIVE_CROSS = "negativeCross";
+ field public static final String POSITIVE_CROSS = "positiveCross";
+ field public static final String POST_LAYOUT = "postLayout";
+ field public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
+ field public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
+ field public static final String TRIGGER_ID = "triggerID";
+ field public static final String TRIGGER_RECEIVER = "triggerReceiver";
+ field public static final String TRIGGER_SLACK = "triggerSlack";
+ field public static final int TYPE_CROSS = 312; // 0x138
+ field public static final int TYPE_NEGATIVE_CROSS = 310; // 0x136
+ field public static final int TYPE_POSITIVE_CROSS = 309; // 0x135
+ field public static final int TYPE_POST_LAYOUT = 304; // 0x130
+ field public static final int TYPE_TRIGGER_COLLISION_ID = 307; // 0x133
+ field public static final int TYPE_TRIGGER_COLLISION_VIEW = 306; // 0x132
+ field public static final int TYPE_TRIGGER_ID = 308; // 0x134
+ field public static final int TYPE_TRIGGER_RECEIVER = 311; // 0x137
+ field public static final int TYPE_TRIGGER_SLACK = 305; // 0x131
+ field public static final int TYPE_VIEW_TRANSITION_ON_CROSS = 301; // 0x12d
+ field public static final int TYPE_VIEW_TRANSITION_ON_NEGATIVE_CROSS = 303; // 0x12f
+ field public static final int TYPE_VIEW_TRANSITION_ON_POSITIVE_CROSS = 302; // 0x12e
+ field public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
+ field public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
+ field public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
+ }
+
+}
+
+package androidx.constraintlayout.core.motion.parse {
+
+ public class KeyParser {
+ ctor public KeyParser();
+ method public static void main(String![]!);
+ method public static androidx.constraintlayout.core.motion.utils.TypedBundle! parseAttributes(String!);
+ }
+
+}
+
+package androidx.constraintlayout.core.motion.utils {
+
+ public class ArcCurveFit extends androidx.constraintlayout.core.motion.utils.CurveFit {
+ ctor public ArcCurveFit(int[]!, double[]!, double[]![]!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getSlope(double, double[]!);
+ method public double getSlope(double, int);
+ method public double[]! getTimePoints();
+ field public static final int ARC_ABOVE = 5; // 0x5
+ field public static final int ARC_BELOW = 4; // 0x4
+ field public static final int ARC_START_FLIP = 3; // 0x3
+ field public static final int ARC_START_HORIZONTAL = 2; // 0x2
+ field public static final int ARC_START_LINEAR = 0; // 0x0
+ field public static final int ARC_START_VERTICAL = 1; // 0x1
+ }
+
+ public abstract class CurveFit {
+ ctor public CurveFit();
+ method public static androidx.constraintlayout.core.motion.utils.CurveFit! get(int, double[]!, double[]![]!);
+ method public static androidx.constraintlayout.core.motion.utils.CurveFit! getArc(int[]!, double[]!, double[]![]!);
+ method public abstract void getPos(double, double[]!);
+ method public abstract void getPos(double, float[]!);
+ method public abstract double getPos(double, int);
+ method public abstract void getSlope(double, double[]!);
+ method public abstract double getSlope(double, int);
+ method public abstract double[]! getTimePoints();
+ field public static final int CONSTANT = 2; // 0x2
+ field public static final int LINEAR = 1; // 0x1
+ field public static final int SPLINE = 0; // 0x0
+ }
+
+ public interface DifferentialInterpolator {
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ }
+
+ public class Easing {
+ ctor public Easing();
+ method public double get(double);
+ method public double getDiff(double);
+ method public static androidx.constraintlayout.core.motion.utils.Easing! getInterpolator(String!);
+ field public static String![]! NAMED_EASING;
+ }
+
+ public class FloatRect {
+ ctor public FloatRect();
+ method public final float centerX();
+ method public final float centerY();
+ field public float bottom;
+ field public float left;
+ field public float right;
+ field public float top;
+ }
+
+ public class HyperSpline {
+ ctor public HyperSpline();
+ ctor public HyperSpline(double[]![]!);
+ method public double approxLength(androidx.constraintlayout.core.motion.utils.HyperSpline.Cubic![]!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getVelocity(double, double[]!);
+ method public void setup(double[]![]!);
+ }
+
+ public static class HyperSpline.Cubic {
+ ctor public HyperSpline.Cubic(double, double, double, double);
+ method public double eval(double);
+ method public double vel(double);
+ }
+
+ public class KeyCache {
+ ctor public KeyCache();
+ method public float getFloatValue(Object!, String!, int);
+ method public void setFloatValue(Object!, String!, int, float);
+ }
+
+ public abstract class KeyCycleOscillator {
+ ctor public KeyCycleOscillator();
+ method public float get(float);
+ method public androidx.constraintlayout.core.motion.utils.CurveFit! getCurveFit();
+ method public float getSlope(float);
+ method public static androidx.constraintlayout.core.motion.utils.KeyCycleOscillator! makeWidgetCycle(String!);
+ method protected void setCustom(Object!);
+ method public void setPoint(int, int, String!, int, float, float, float, float);
+ method public void setPoint(int, int, String!, int, float, float, float, float, Object!);
+ method public void setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float);
+ method public void setType(String!);
+ method public void setup(float);
+ method public boolean variesByPath();
+ field public int mVariesBy;
+ }
+
+ public static class KeyCycleOscillator.PathRotateSet extends androidx.constraintlayout.core.motion.utils.KeyCycleOscillator {
+ ctor public KeyCycleOscillator.PathRotateSet(String!);
+ method public void setPathRotate(androidx.constraintlayout.core.motion.MotionWidget!, float, double, double);
+ }
+
+ public class KeyFrameArray {
+ ctor public KeyFrameArray();
+ }
+
+ public static class KeyFrameArray.CustomArray {
+ ctor public KeyFrameArray.CustomArray();
+ method public void append(int, androidx.constraintlayout.core.motion.CustomAttribute!);
+ method public void clear();
+ method public void dump();
+ method public int keyAt(int);
+ method public void remove(int);
+ method public int size();
+ method public androidx.constraintlayout.core.motion.CustomAttribute! valueAt(int);
+ }
+
+ public static class KeyFrameArray.CustomVar {
+ ctor public KeyFrameArray.CustomVar();
+ method public void append(int, androidx.constraintlayout.core.motion.CustomVariable!);
+ method public void clear();
+ method public void dump();
+ method public int keyAt(int);
+ method public void remove(int);
+ method public int size();
+ method public androidx.constraintlayout.core.motion.CustomVariable! valueAt(int);
+ }
+
+ public class LinearCurveFit extends androidx.constraintlayout.core.motion.utils.CurveFit {
+ ctor public LinearCurveFit(double[]!, double[]![]!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getSlope(double, double[]!);
+ method public double getSlope(double, int);
+ method public double[]! getTimePoints();
+ }
+
+ public class MonotonicCurveFit extends androidx.constraintlayout.core.motion.utils.CurveFit {
+ ctor public MonotonicCurveFit(double[]!, double[]![]!);
+ method public static androidx.constraintlayout.core.motion.utils.MonotonicCurveFit! buildWave(String!);
+ method public void getPos(double, double[]!);
+ method public void getPos(double, float[]!);
+ method public double getPos(double, int);
+ method public void getSlope(double, double[]!);
+ method public double getSlope(double, int);
+ method public double[]! getTimePoints();
+ }
+
+ public class Oscillator {
+ ctor public Oscillator();
+ method public void addPoint(double, float);
+ method public double getSlope(double, double, double);
+ method public double getValue(double, double);
+ method public void normalize();
+ method public void setType(int, String!);
+ field public static final int BOUNCE = 6; // 0x6
+ field public static final int COS_WAVE = 5; // 0x5
+ field public static final int CUSTOM = 7; // 0x7
+ field public static final int REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SAW_WAVE = 3; // 0x3
+ field public static final int SIN_WAVE = 0; // 0x0
+ field public static final int SQUARE_WAVE = 1; // 0x1
+ field public static String! TAG;
+ field public static final int TRIANGLE_WAVE = 2; // 0x2
+ }
+
+ public class Rect {
+ ctor public Rect();
+ method public int height();
+ method public int width();
+ field public int bottom;
+ field public int left;
+ field public int right;
+ field public int top;
+ }
+
+ public class Schlick extends androidx.constraintlayout.core.motion.utils.Easing {
+ }
+
+ public abstract class SplineSet {
+ ctor public SplineSet();
+ method public float get(float);
+ method public androidx.constraintlayout.core.motion.utils.CurveFit! getCurveFit();
+ method public float getSlope(float);
+ method public static androidx.constraintlayout.core.motion.utils.SplineSet! makeCustomSpline(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomArray!);
+ method public static androidx.constraintlayout.core.motion.utils.SplineSet! makeCustomSplineSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomVar!);
+ method public static androidx.constraintlayout.core.motion.utils.SplineSet! makeSpline(String!, long);
+ method public void setPoint(int, float);
+ method public void setProperty(androidx.constraintlayout.core.motion.utils.TypedValues!, float);
+ method public void setType(String!);
+ method public void setup(int);
+ field protected androidx.constraintlayout.core.motion.utils.CurveFit! mCurveFit;
+ field protected int[]! mTimePoints;
+ field protected float[]! mValues;
+ }
+
+ public static class SplineSet.CustomSet extends androidx.constraintlayout.core.motion.utils.SplineSet {
+ ctor public SplineSet.CustomSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomArray!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomAttribute!);
+ method public void setProperty(androidx.constraintlayout.core.state.WidgetFrame!, float);
+ }
+
+ public static class SplineSet.CustomSpline extends androidx.constraintlayout.core.motion.utils.SplineSet {
+ ctor public SplineSet.CustomSpline(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomVar!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomVariable!);
+ method public void setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float);
+ }
+
+ public class SpringStopEngine implements androidx.constraintlayout.core.motion.utils.StopEngine {
+ ctor public SpringStopEngine();
+ method public String! debug(String!, float);
+ method public float getAcceleration();
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ method public void springConfig(float, float, float, float, float, float, float, int);
+ }
+
+ public class StepCurve extends androidx.constraintlayout.core.motion.utils.Easing {
+ }
+
+ public interface StopEngine {
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ }
+
+ public class StopLogicEngine implements androidx.constraintlayout.core.motion.utils.StopEngine {
+ ctor public StopLogicEngine();
+ method public void config(float, float, float, float, float, float);
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ }
+
+ public static class StopLogicEngine.Decelerate implements androidx.constraintlayout.core.motion.utils.StopEngine {
+ ctor public StopLogicEngine.Decelerate();
+ method public void config(float, float, float);
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ }
+
+ public abstract class TimeCycleSplineSet {
+ ctor public TimeCycleSplineSet();
+ method protected float calcWave(float);
+ method public androidx.constraintlayout.core.motion.utils.CurveFit! getCurveFit();
+ method public void setPoint(int, float, float, int, float);
+ method protected void setStartTime(long);
+ method public void setType(String!);
+ method public void setup(int);
+ field protected static final int CURVE_OFFSET = 2; // 0x2
+ field protected static final int CURVE_PERIOD = 1; // 0x1
+ field protected static final int CURVE_VALUE = 0; // 0x0
+ field protected float[]! mCache;
+ field protected boolean mContinue;
+ field protected int mCount;
+ field protected androidx.constraintlayout.core.motion.utils.CurveFit! mCurveFit;
+ field protected float mLastCycle;
+ field protected long mLastTime;
+ field protected int[]! mTimePoints;
+ field protected String! mType;
+ field protected float[]![]! mValues;
+ field protected int mWaveShape;
+ field protected static float sVal2PI;
+ }
+
+ public static class TimeCycleSplineSet.CustomSet extends androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet {
+ ctor public TimeCycleSplineSet.CustomSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomArray!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomAttribute!, float, int, float);
+ method public boolean setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ public static class TimeCycleSplineSet.CustomVarSet extends androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet {
+ ctor public TimeCycleSplineSet.CustomVarSet(String!, androidx.constraintlayout.core.motion.utils.KeyFrameArray.CustomVar!);
+ method public void setPoint(int, androidx.constraintlayout.core.motion.CustomVariable!, float, int, float);
+ method public boolean setProperty(androidx.constraintlayout.core.motion.MotionWidget!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ protected static class TimeCycleSplineSet.Sort {
+ ctor protected TimeCycleSplineSet.Sort();
+ }
+
+ public class TypedBundle {
+ ctor public TypedBundle();
+ method public void add(int, boolean);
+ method public void add(int, float);
+ method public void add(int, int);
+ method public void add(int, String!);
+ method public void addIfNotNull(int, String!);
+ method public void applyDelta(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void applyDelta(androidx.constraintlayout.core.motion.utils.TypedValues!);
+ method public void clear();
+ method public int getInteger(int);
+ }
+
+ public interface TypedValues {
+ method public int getId(String!);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ field public static final int BOOLEAN_MASK = 1; // 0x1
+ field public static final int FLOAT_MASK = 4; // 0x4
+ field public static final int INT_MASK = 2; // 0x2
+ field public static final int STRING_MASK = 8; // 0x8
+ field public static final String S_CUSTOM = "CUSTOM";
+ field public static final int TYPE_FRAME_POSITION = 100; // 0x64
+ field public static final int TYPE_TARGET = 101; // 0x65
+ }
+
+ public static interface TypedValues.AttributesType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyAttributes";
+ field public static final String S_ALPHA = "alpha";
+ field public static final String S_CURVE_FIT = "curveFit";
+ field public static final String S_CUSTOM = "CUSTOM";
+ field public static final String S_EASING = "easing";
+ field public static final String S_ELEVATION = "elevation";
+ field public static final String S_FRAME = "frame";
+ field public static final String S_PATH_ROTATE = "pathRotate";
+ field public static final String S_PIVOT_TARGET = "pivotTarget";
+ field public static final String S_PIVOT_X = "pivotX";
+ field public static final String S_PIVOT_Y = "pivotY";
+ field public static final String S_PROGRESS = "progress";
+ field public static final String S_ROTATION_X = "rotationX";
+ field public static final String S_ROTATION_Y = "rotationY";
+ field public static final String S_ROTATION_Z = "rotationZ";
+ field public static final String S_SCALE_X = "scaleX";
+ field public static final String S_SCALE_Y = "scaleY";
+ field public static final String S_TARGET = "target";
+ field public static final String S_TRANSLATION_X = "translationX";
+ field public static final String S_TRANSLATION_Y = "translationY";
+ field public static final String S_TRANSLATION_Z = "translationZ";
+ field public static final String S_VISIBILITY = "visibility";
+ field public static final int TYPE_ALPHA = 303; // 0x12f
+ field public static final int TYPE_CURVE_FIT = 301; // 0x12d
+ field public static final int TYPE_EASING = 317; // 0x13d
+ field public static final int TYPE_ELEVATION = 307; // 0x133
+ field public static final int TYPE_PATH_ROTATE = 316; // 0x13c
+ field public static final int TYPE_PIVOT_TARGET = 318; // 0x13e
+ field public static final int TYPE_PIVOT_X = 313; // 0x139
+ field public static final int TYPE_PIVOT_Y = 314; // 0x13a
+ field public static final int TYPE_PROGRESS = 315; // 0x13b
+ field public static final int TYPE_ROTATION_X = 308; // 0x134
+ field public static final int TYPE_ROTATION_Y = 309; // 0x135
+ field public static final int TYPE_ROTATION_Z = 310; // 0x136
+ field public static final int TYPE_SCALE_X = 311; // 0x137
+ field public static final int TYPE_SCALE_Y = 312; // 0x138
+ field public static final int TYPE_TRANSLATION_X = 304; // 0x130
+ field public static final int TYPE_TRANSLATION_Y = 305; // 0x131
+ field public static final int TYPE_TRANSLATION_Z = 306; // 0x132
+ field public static final int TYPE_VISIBILITY = 302; // 0x12e
+ }
+
+ public static interface TypedValues.Custom {
+ method public static int getId(String!);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "Custom";
+ field public static final String S_BOOLEAN = "boolean";
+ field public static final String S_COLOR = "color";
+ field public static final String S_DIMENSION = "dimension";
+ field public static final String S_FLOAT = "float";
+ field public static final String S_INT = "integer";
+ field public static final String S_REFERENCE = "reference";
+ field public static final String S_STRING = "string";
+ field public static final int TYPE_BOOLEAN = 904; // 0x388
+ field public static final int TYPE_COLOR = 902; // 0x386
+ field public static final int TYPE_DIMENSION = 905; // 0x389
+ field public static final int TYPE_FLOAT = 901; // 0x385
+ field public static final int TYPE_INT = 900; // 0x384
+ field public static final int TYPE_REFERENCE = 906; // 0x38a
+ field public static final int TYPE_STRING = 903; // 0x387
+ }
+
+ public static interface TypedValues.CycleType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyCycle";
+ field public static final String S_ALPHA = "alpha";
+ field public static final String S_CURVE_FIT = "curveFit";
+ field public static final String S_CUSTOM_WAVE_SHAPE = "customWave";
+ field public static final String S_EASING = "easing";
+ field public static final String S_ELEVATION = "elevation";
+ field public static final String S_PATH_ROTATE = "pathRotate";
+ field public static final String S_PIVOT_X = "pivotX";
+ field public static final String S_PIVOT_Y = "pivotY";
+ field public static final String S_PROGRESS = "progress";
+ field public static final String S_ROTATION_X = "rotationX";
+ field public static final String S_ROTATION_Y = "rotationY";
+ field public static final String S_ROTATION_Z = "rotationZ";
+ field public static final String S_SCALE_X = "scaleX";
+ field public static final String S_SCALE_Y = "scaleY";
+ field public static final String S_TRANSLATION_X = "translationX";
+ field public static final String S_TRANSLATION_Y = "translationY";
+ field public static final String S_TRANSLATION_Z = "translationZ";
+ field public static final String S_VISIBILITY = "visibility";
+ field public static final String S_WAVE_OFFSET = "offset";
+ field public static final String S_WAVE_PERIOD = "period";
+ field public static final String S_WAVE_PHASE = "phase";
+ field public static final String S_WAVE_SHAPE = "waveShape";
+ field public static final int TYPE_ALPHA = 403; // 0x193
+ field public static final int TYPE_CURVE_FIT = 401; // 0x191
+ field public static final int TYPE_CUSTOM_WAVE_SHAPE = 422; // 0x1a6
+ field public static final int TYPE_EASING = 420; // 0x1a4
+ field public static final int TYPE_ELEVATION = 307; // 0x133
+ field public static final int TYPE_PATH_ROTATE = 416; // 0x1a0
+ field public static final int TYPE_PIVOT_X = 313; // 0x139
+ field public static final int TYPE_PIVOT_Y = 314; // 0x13a
+ field public static final int TYPE_PROGRESS = 315; // 0x13b
+ field public static final int TYPE_ROTATION_X = 308; // 0x134
+ field public static final int TYPE_ROTATION_Y = 309; // 0x135
+ field public static final int TYPE_ROTATION_Z = 310; // 0x136
+ field public static final int TYPE_SCALE_X = 311; // 0x137
+ field public static final int TYPE_SCALE_Y = 312; // 0x138
+ field public static final int TYPE_TRANSLATION_X = 304; // 0x130
+ field public static final int TYPE_TRANSLATION_Y = 305; // 0x131
+ field public static final int TYPE_TRANSLATION_Z = 306; // 0x132
+ field public static final int TYPE_VISIBILITY = 402; // 0x192
+ field public static final int TYPE_WAVE_OFFSET = 424; // 0x1a8
+ field public static final int TYPE_WAVE_PERIOD = 423; // 0x1a7
+ field public static final int TYPE_WAVE_PHASE = 425; // 0x1a9
+ field public static final int TYPE_WAVE_SHAPE = 421; // 0x1a5
+ }
+
+ public static interface TypedValues.MotionScene {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "MotionScene";
+ field public static final String S_DEFAULT_DURATION = "defaultDuration";
+ field public static final String S_LAYOUT_DURING_TRANSITION = "layoutDuringTransition";
+ field public static final int TYPE_DEFAULT_DURATION = 600; // 0x258
+ field public static final int TYPE_LAYOUT_DURING_TRANSITION = 601; // 0x259
+ }
+
+ public static interface TypedValues.MotionType {
+ method public static int getId(String!);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "Motion";
+ field public static final String S_ANIMATE_CIRCLEANGLE_TO = "AnimateCircleAngleTo";
+ field public static final String S_ANIMATE_RELATIVE_TO = "AnimateRelativeTo";
+ field public static final String S_DRAW_PATH = "DrawPath";
+ field public static final String S_EASING = "TransitionEasing";
+ field public static final String S_PATHMOTION_ARC = "PathMotionArc";
+ field public static final String S_PATH_ROTATE = "PathRotate";
+ field public static final String S_POLAR_RELATIVETO = "PolarRelativeTo";
+ field public static final String S_QUANTIZE_INTERPOLATOR = "QuantizeInterpolator";
+ field public static final String S_QUANTIZE_INTERPOLATOR_ID = "QuantizeInterpolatorID";
+ field public static final String S_QUANTIZE_INTERPOLATOR_TYPE = "QuantizeInterpolatorType";
+ field public static final String S_QUANTIZE_MOTIONSTEPS = "QuantizeMotionSteps";
+ field public static final String S_QUANTIZE_MOTION_PHASE = "QuantizeMotionPhase";
+ field public static final String S_STAGGER = "Stagger";
+ field public static final int TYPE_ANIMATE_CIRCLEANGLE_TO = 606; // 0x25e
+ field public static final int TYPE_ANIMATE_RELATIVE_TO = 605; // 0x25d
+ field public static final int TYPE_DRAW_PATH = 608; // 0x260
+ field public static final int TYPE_EASING = 603; // 0x25b
+ field public static final int TYPE_PATHMOTION_ARC = 607; // 0x25f
+ field public static final int TYPE_PATH_ROTATE = 601; // 0x259
+ field public static final int TYPE_POLAR_RELATIVETO = 609; // 0x261
+ field public static final int TYPE_QUANTIZE_INTERPOLATOR = 604; // 0x25c
+ field public static final int TYPE_QUANTIZE_INTERPOLATOR_ID = 612; // 0x264
+ field public static final int TYPE_QUANTIZE_INTERPOLATOR_TYPE = 611; // 0x263
+ field public static final int TYPE_QUANTIZE_MOTIONSTEPS = 610; // 0x262
+ field public static final int TYPE_QUANTIZE_MOTION_PHASE = 602; // 0x25a
+ field public static final int TYPE_STAGGER = 600; // 0x258
+ }
+
+ public static interface TypedValues.OnSwipe {
+ field public static final String AUTOCOMPLETE_MODE = "autocompletemode";
+ field public static final String![]! AUTOCOMPLETE_MODE_ENUM;
+ field public static final String DRAG_DIRECTION = "dragdirection";
+ field public static final String DRAG_SCALE = "dragscale";
+ field public static final String DRAG_THRESHOLD = "dragthreshold";
+ field public static final String LIMIT_BOUNDS_TO = "limitboundsto";
+ field public static final String MAX_ACCELERATION = "maxacceleration";
+ field public static final String MAX_VELOCITY = "maxvelocity";
+ field public static final String MOVE_WHEN_SCROLLAT_TOP = "movewhenscrollattop";
+ field public static final String NESTED_SCROLL_FLAGS = "nestedscrollflags";
+ field public static final String![]! NESTED_SCROLL_FLAGS_ENUM;
+ field public static final String ON_TOUCH_UP = "ontouchup";
+ field public static final String![]! ON_TOUCH_UP_ENUM;
+ field public static final String ROTATION_CENTER_ID = "rotationcenterid";
+ field public static final String SPRINGS_TOP_THRESHOLD = "springstopthreshold";
+ field public static final String SPRING_BOUNDARY = "springboundary";
+ field public static final String![]! SPRING_BOUNDARY_ENUM;
+ field public static final String SPRING_DAMPING = "springdamping";
+ field public static final String SPRING_MASS = "springmass";
+ field public static final String SPRING_STIFFNESS = "springstiffness";
+ field public static final String TOUCH_ANCHOR_ID = "touchanchorid";
+ field public static final String TOUCH_ANCHOR_SIDE = "touchanchorside";
+ field public static final String TOUCH_REGION_ID = "touchregionid";
+ }
+
+ public static interface TypedValues.PositionType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyPosition";
+ field public static final String S_DRAWPATH = "drawPath";
+ field public static final String S_PERCENT_HEIGHT = "percentHeight";
+ field public static final String S_PERCENT_WIDTH = "percentWidth";
+ field public static final String S_PERCENT_X = "percentX";
+ field public static final String S_PERCENT_Y = "percentY";
+ field public static final String S_SIZE_PERCENT = "sizePercent";
+ field public static final String S_TRANSITION_EASING = "transitionEasing";
+ field public static final int TYPE_CURVE_FIT = 508; // 0x1fc
+ field public static final int TYPE_DRAWPATH = 502; // 0x1f6
+ field public static final int TYPE_PATH_MOTION_ARC = 509; // 0x1fd
+ field public static final int TYPE_PERCENT_HEIGHT = 504; // 0x1f8
+ field public static final int TYPE_PERCENT_WIDTH = 503; // 0x1f7
+ field public static final int TYPE_PERCENT_X = 506; // 0x1fa
+ field public static final int TYPE_PERCENT_Y = 507; // 0x1fb
+ field public static final int TYPE_POSITION_TYPE = 510; // 0x1fe
+ field public static final int TYPE_SIZE_PERCENT = 505; // 0x1f9
+ field public static final int TYPE_TRANSITION_EASING = 501; // 0x1f5
+ }
+
+ public static interface TypedValues.TransitionType {
+ method public static int getId(String!);
+ method public static int getType(int);
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "Transitions";
+ field public static final String S_AUTO_TRANSITION = "autoTransition";
+ field public static final String S_DURATION = "duration";
+ field public static final String S_FROM = "from";
+ field public static final String S_INTERPOLATOR = "motionInterpolator";
+ field public static final String S_PATH_MOTION_ARC = "pathMotionArc";
+ field public static final String S_STAGGERED = "staggered";
+ field public static final String S_TO = "to";
+ field public static final String S_TRANSITION_FLAGS = "transitionFlags";
+ field public static final int TYPE_AUTO_TRANSITION = 704; // 0x2c0
+ field public static final int TYPE_DURATION = 700; // 0x2bc
+ field public static final int TYPE_FROM = 701; // 0x2bd
+ field public static final int TYPE_INTERPOLATOR = 705; // 0x2c1
+ field public static final int TYPE_PATH_MOTION_ARC = 509; // 0x1fd
+ field public static final int TYPE_STAGGERED = 706; // 0x2c2
+ field public static final int TYPE_TO = 702; // 0x2be
+ field public static final int TYPE_TRANSITION_FLAGS = 707; // 0x2c3
+ }
+
+ public static interface TypedValues.TriggerType {
+ method public static int getId(String!);
+ field public static final String CROSS = "CROSS";
+ field public static final String![]! KEY_WORDS;
+ field public static final String NAME = "KeyTrigger";
+ field public static final String NEGATIVE_CROSS = "negativeCross";
+ field public static final String POSITIVE_CROSS = "positiveCross";
+ field public static final String POST_LAYOUT = "postLayout";
+ field public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
+ field public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
+ field public static final String TRIGGER_ID = "triggerID";
+ field public static final String TRIGGER_RECEIVER = "triggerReceiver";
+ field public static final String TRIGGER_SLACK = "triggerSlack";
+ field public static final int TYPE_CROSS = 312; // 0x138
+ field public static final int TYPE_NEGATIVE_CROSS = 310; // 0x136
+ field public static final int TYPE_POSITIVE_CROSS = 309; // 0x135
+ field public static final int TYPE_POST_LAYOUT = 304; // 0x130
+ field public static final int TYPE_TRIGGER_COLLISION_ID = 307; // 0x133
+ field public static final int TYPE_TRIGGER_COLLISION_VIEW = 306; // 0x132
+ field public static final int TYPE_TRIGGER_ID = 308; // 0x134
+ field public static final int TYPE_TRIGGER_RECEIVER = 311; // 0x137
+ field public static final int TYPE_TRIGGER_SLACK = 305; // 0x131
+ field public static final int TYPE_VIEW_TRANSITION_ON_CROSS = 301; // 0x12d
+ field public static final int TYPE_VIEW_TRANSITION_ON_NEGATIVE_CROSS = 303; // 0x12f
+ field public static final int TYPE_VIEW_TRANSITION_ON_POSITIVE_CROSS = 302; // 0x12e
+ field public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
+ field public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
+ field public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
+ }
+
+ public class Utils {
+ ctor public Utils();
+ method public int getInterpolatedColor(float[]!);
+ method public static void log(String!);
+ method public static void log(String!, String!);
+ method public static void logStack(String!, int);
+ method public static void loge(String!, String!);
+ method public static int rgbaTocColor(float, float, float, float);
+ method public static void setDebugHandle(androidx.constraintlayout.core.motion.utils.Utils.DebugHandle!);
+ method public static void socketSend(String!);
+ }
+
+ public static interface Utils.DebugHandle {
+ method public void message(String!);
+ }
+
+ public class VelocityMatrix {
+ ctor public VelocityMatrix();
+ method public void applyTransform(float, float, int, int, float[]!);
+ method public void clear();
+ method public void setRotationVelocity(androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, float);
+ method public void setRotationVelocity(androidx.constraintlayout.core.motion.utils.SplineSet!, float);
+ method public void setScaleVelocity(androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, float);
+ method public void setScaleVelocity(androidx.constraintlayout.core.motion.utils.SplineSet!, androidx.constraintlayout.core.motion.utils.SplineSet!, float);
+ method public void setTranslationVelocity(androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, androidx.constraintlayout.core.motion.utils.KeyCycleOscillator!, float);
+ method public void setTranslationVelocity(androidx.constraintlayout.core.motion.utils.SplineSet!, androidx.constraintlayout.core.motion.utils.SplineSet!, float);
+ }
+
+ public class ViewState {
+ ctor public ViewState();
+ method public void getState(androidx.constraintlayout.core.motion.MotionWidget!);
+ method public int height();
+ method public int width();
+ field public int bottom;
+ field public int left;
+ field public int right;
+ field public float rotation;
+ field public int top;
+ }
+
+}
+
+package androidx.constraintlayout.core.parser {
+
+ public class CLArray extends androidx.constraintlayout.core.parser.CLContainer {
+ ctor public CLArray(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ }
+
+ public class CLContainer extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLContainer(char[]!);
+ method public void add(androidx.constraintlayout.core.parser.CLElement!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public void clear();
+ method public androidx.constraintlayout.core.parser.CLContainer clone();
+ method public androidx.constraintlayout.core.parser.CLElement! get(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLElement! get(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLArray! getArray(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLArray! getArray(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLArray! getArrayOrCreate(String!);
+ method public androidx.constraintlayout.core.parser.CLArray! getArrayOrNull(String!);
+ method public boolean getBoolean(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public boolean getBoolean(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public float getFloat(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public float getFloat(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public float getFloatOrNaN(String!);
+ method public int getInt(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public int getInt(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLObject! getObject(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLObject! getObject(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLObject! getObjectOrNull(String!);
+ method public androidx.constraintlayout.core.parser.CLElement! getOrNull(int);
+ method public androidx.constraintlayout.core.parser.CLElement! getOrNull(String!);
+ method public String! getString(int) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public String! getString(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public String! getStringOrNull(int);
+ method public String! getStringOrNull(String!);
+ method public boolean has(String!);
+ method public java.util.ArrayList<java.lang.String!>! names();
+ method public void put(String!, androidx.constraintlayout.core.parser.CLElement!);
+ method public void putNumber(String!, float);
+ method public void putString(String!, String!);
+ method public void remove(String!);
+ method public int size();
+ }
+
+ public class CLElement implements java.lang.Cloneable {
+ ctor public CLElement(char[]!);
+ method protected void addIndent(StringBuilder!, int);
+ method public androidx.constraintlayout.core.parser.CLElement clone();
+ method public String! content();
+ method public androidx.constraintlayout.core.parser.CLElement! getContainer();
+ method protected String! getDebugName();
+ method public long getEnd();
+ method public float getFloat();
+ method public int getInt();
+ method public int getLine();
+ method public long getStart();
+ method protected String! getStrClass();
+ method public boolean hasContent();
+ method public boolean isDone();
+ method public boolean isStarted();
+ method public boolean notStarted();
+ method public void setContainer(androidx.constraintlayout.core.parser.CLContainer!);
+ method public void setEnd(long);
+ method public void setLine(int);
+ method public void setStart(long);
+ method protected String! toFormattedJSON(int, int);
+ method protected String! toJSON();
+ field protected androidx.constraintlayout.core.parser.CLContainer! mContainer;
+ field protected long mEnd;
+ field protected long mStart;
+ field protected static int sBaseIndent;
+ field protected static int sMaxLine;
+ }
+
+ public class CLKey extends androidx.constraintlayout.core.parser.CLContainer {
+ ctor public CLKey(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(String!, androidx.constraintlayout.core.parser.CLElement!);
+ method public String! getName();
+ method public androidx.constraintlayout.core.parser.CLElement! getValue();
+ method public void set(androidx.constraintlayout.core.parser.CLElement!);
+ }
+
+ public class CLNumber extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLNumber(char[]!);
+ ctor public CLNumber(float);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public boolean isInt();
+ method public void putValue(float);
+ }
+
+ public class CLObject extends androidx.constraintlayout.core.parser.CLContainer implements java.lang.Iterable<androidx.constraintlayout.core.parser.CLKey!> {
+ ctor public CLObject(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLObject! allocate(char[]!);
+ method public androidx.constraintlayout.core.parser.CLObject clone();
+ method public java.util.Iterator<androidx.constraintlayout.core.parser.CLKey!>! iterator();
+ method public String! toFormattedJSON();
+ method public String! toFormattedJSON(int, int);
+ method public String! toJSON();
+ }
+
+ public class CLParser {
+ ctor public CLParser(String!);
+ method public androidx.constraintlayout.core.parser.CLObject! parse() throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static androidx.constraintlayout.core.parser.CLObject! parse(String!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ }
+
+ public class CLParsingException extends java.lang.Exception {
+ ctor public CLParsingException(String!, androidx.constraintlayout.core.parser.CLElement!);
+ method public String! reason();
+ }
+
+ public class CLString extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLString(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLString from(String);
+ }
+
+ public class CLToken extends androidx.constraintlayout.core.parser.CLElement {
+ ctor public CLToken(char[]!);
+ method public static androidx.constraintlayout.core.parser.CLElement! allocate(char[]!);
+ method public boolean getBoolean() throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.parser.CLToken.Type! getType();
+ method public boolean isNull() throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public boolean validate(char, long);
+ }
+
+}
+
+package androidx.constraintlayout.core.state {
+
+ public class ConstraintReference implements androidx.constraintlayout.core.state.Reference {
+ ctor public ConstraintReference(androidx.constraintlayout.core.state.State!);
+ method public void addCustomColor(String!, int);
+ method public void addCustomFloat(String!, float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! alpha(float);
+ method public void apply();
+ method public void applyWidgetConstraints();
+ method public androidx.constraintlayout.core.state.ConstraintReference! baseline();
+ method public androidx.constraintlayout.core.state.ConstraintReference! baselineToBaseline(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! baselineToBottom(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! baselineToTop(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! bias(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! bottom();
+ method public androidx.constraintlayout.core.state.ConstraintReference! bottomToBottom(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! bottomToTop(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! centerHorizontally(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! centerVertically(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! circularConstraint(Object!, float, float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! clear();
+ method public androidx.constraintlayout.core.state.ConstraintReference! clearAll();
+ method public androidx.constraintlayout.core.state.ConstraintReference! clearHorizontal();
+ method public androidx.constraintlayout.core.state.ConstraintReference! clearVertical();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! createConstraintWidget();
+ method public androidx.constraintlayout.core.state.ConstraintReference! end();
+ method public androidx.constraintlayout.core.state.ConstraintReference! endToEnd(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! endToStart(Object!);
+ method public float getAlpha();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public androidx.constraintlayout.core.state.helpers.Facade! getFacade();
+ method public androidx.constraintlayout.core.state.Dimension! getHeight();
+ method public int getHorizontalChainStyle();
+ method public float getHorizontalChainWeight();
+ method public Object! getKey();
+ method public float getPivotX();
+ method public float getPivotY();
+ method public float getRotationX();
+ method public float getRotationY();
+ method public float getRotationZ();
+ method public float getScaleX();
+ method public float getScaleY();
+ method public String! getTag();
+ method public float getTranslationX();
+ method public float getTranslationY();
+ method public float getTranslationZ();
+ method public int getVerticalChainStyle(int);
+ method public float getVerticalChainWeight();
+ method public Object! getView();
+ method public androidx.constraintlayout.core.state.Dimension! getWidth();
+ method public androidx.constraintlayout.core.state.ConstraintReference! height(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! horizontalBias(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! left();
+ method public androidx.constraintlayout.core.state.ConstraintReference! leftToLeft(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! leftToRight(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! margin(int);
+ method public androidx.constraintlayout.core.state.ConstraintReference! margin(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! marginGone(int);
+ method public androidx.constraintlayout.core.state.ConstraintReference! marginGone(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! pivotX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! pivotY(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! right();
+ method public androidx.constraintlayout.core.state.ConstraintReference! rightToLeft(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rightToRight(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rotationX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rotationY(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! rotationZ(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! scaleX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! scaleY(float);
+ method public void setConstraintWidget(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setFacade(androidx.constraintlayout.core.state.helpers.Facade!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! setHeight(androidx.constraintlayout.core.state.Dimension!);
+ method public void setHorizontalChainStyle(int);
+ method public void setHorizontalChainWeight(float);
+ method public void setKey(Object!);
+ method public void setTag(String!);
+ method public void setVerticalChainStyle(int);
+ method public void setVerticalChainWeight(float);
+ method public void setView(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! setWidth(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! start();
+ method public androidx.constraintlayout.core.state.ConstraintReference! startToEnd(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! startToStart(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! top();
+ method public androidx.constraintlayout.core.state.ConstraintReference! topToBottom(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! topToTop(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! translationX(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! translationY(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! translationZ(float);
+ method public void validate() throws java.lang.Exception;
+ method public androidx.constraintlayout.core.state.ConstraintReference! verticalBias(float);
+ method public androidx.constraintlayout.core.state.ConstraintReference! visibility(int);
+ method public androidx.constraintlayout.core.state.ConstraintReference! width(androidx.constraintlayout.core.state.Dimension!);
+ field protected Object! mBottomToBottom;
+ field protected Object! mBottomToTop;
+ field protected Object! mEndToEnd;
+ field protected Object! mEndToStart;
+ field protected float mHorizontalBias;
+ field protected Object! mLeftToLeft;
+ field protected Object! mLeftToRight;
+ field protected int mMarginBottom;
+ field protected int mMarginBottomGone;
+ field protected int mMarginEnd;
+ field protected int mMarginEndGone;
+ field protected int mMarginLeft;
+ field protected int mMarginLeftGone;
+ field protected int mMarginRight;
+ field protected int mMarginRightGone;
+ field protected int mMarginStart;
+ field protected int mMarginStartGone;
+ field protected int mMarginTop;
+ field protected int mMarginTopGone;
+ field protected Object! mRightToLeft;
+ field protected Object! mRightToRight;
+ field protected Object! mStartToEnd;
+ field protected Object! mStartToStart;
+ field protected Object! mTopToBottom;
+ field protected Object! mTopToTop;
+ field protected float mVerticalBias;
+ }
+
+ public static interface ConstraintReference.ConstraintReferenceFactory {
+ method public androidx.constraintlayout.core.state.ConstraintReference! create(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class ConstraintSetParser {
+ ctor public ConstraintSetParser();
+ method public static void parseDesignElementsJSON(String!, java.util.ArrayList<androidx.constraintlayout.core.state.ConstraintSetParser.DesignElement!>!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static void parseJSON(String!, androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.ConstraintSetParser.LayoutVariables!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static void parseJSON(String!, androidx.constraintlayout.core.state.Transition!, int);
+ method public static void parseMotionSceneJSON(androidx.constraintlayout.core.state.CoreMotionScene!, String!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static void populateState(androidx.constraintlayout.core.parser.CLObject, androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.ConstraintSetParser.LayoutVariables) throws androidx.constraintlayout.core.parser.CLParsingException;
+ }
+
+ public static class ConstraintSetParser.DesignElement {
+ method public String! getId();
+ method public java.util.HashMap<java.lang.String!,java.lang.String!>! getParams();
+ method public String! getType();
+ }
+
+ public static class ConstraintSetParser.LayoutVariables {
+ ctor public ConstraintSetParser.LayoutVariables();
+ method public void putOverride(String!, float);
+ }
+
+ public enum ConstraintSetParser.MotionLayoutDebugFlags {
+ enum_constant public static final androidx.constraintlayout.core.state.ConstraintSetParser.MotionLayoutDebugFlags NONE;
+ enum_constant public static final androidx.constraintlayout.core.state.ConstraintSetParser.MotionLayoutDebugFlags SHOW_ALL;
+ enum_constant public static final androidx.constraintlayout.core.state.ConstraintSetParser.MotionLayoutDebugFlags UNKNOWN;
+ }
+
+ public interface CoreMotionScene {
+ method public String! getConstraintSet(int);
+ method public String! getConstraintSet(String!);
+ method public String! getTransition(String!);
+ method public void setConstraintSetContent(String!, String!);
+ method public void setDebugName(String!);
+ method public void setTransitionContent(String!, String!);
+ }
+
+ public interface CorePixelDp {
+ method public float toPixels(float);
+ }
+
+ public class Dimension {
+ method public void apply(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ method public static androidx.constraintlayout.core.state.Dimension! createFixed(int);
+ method public static androidx.constraintlayout.core.state.Dimension! createFixed(Object!);
+ method public static androidx.constraintlayout.core.state.Dimension! createParent();
+ method public static androidx.constraintlayout.core.state.Dimension! createPercent(Object!, float);
+ method public static androidx.constraintlayout.core.state.Dimension! createRatio(String!);
+ method public static androidx.constraintlayout.core.state.Dimension! createSpread();
+ method public static androidx.constraintlayout.core.state.Dimension! createSuggested(int);
+ method public static androidx.constraintlayout.core.state.Dimension! createSuggested(Object!);
+ method public static androidx.constraintlayout.core.state.Dimension! createWrap();
+ method public boolean equalsFixedValue(int);
+ method public androidx.constraintlayout.core.state.Dimension! fixed(int);
+ method public androidx.constraintlayout.core.state.Dimension! fixed(Object!);
+ method public androidx.constraintlayout.core.state.Dimension! max(int);
+ method public androidx.constraintlayout.core.state.Dimension! max(Object!);
+ method public androidx.constraintlayout.core.state.Dimension! min(int);
+ method public androidx.constraintlayout.core.state.Dimension! min(Object!);
+ method public androidx.constraintlayout.core.state.Dimension! percent(Object!, float);
+ method public androidx.constraintlayout.core.state.Dimension! ratio(String!);
+ method public androidx.constraintlayout.core.state.Dimension! suggested(int);
+ method public androidx.constraintlayout.core.state.Dimension! suggested(Object!);
+ field public static final Object! FIXED_DIMENSION;
+ field public static final Object! PARENT_DIMENSION;
+ field public static final Object! PERCENT_DIMENSION;
+ field public static final Object! RATIO_DIMENSION;
+ field public static final Object! SPREAD_DIMENSION;
+ field public static final Object! WRAP_DIMENSION;
+ }
+
+ public enum Dimension.Type {
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type FIXED;
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type MATCH_CONSTRAINT;
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type MATCH_PARENT;
+ enum_constant public static final androidx.constraintlayout.core.state.Dimension.Type WRAP;
+ }
+
+ public class HelperReference extends androidx.constraintlayout.core.state.ConstraintReference implements androidx.constraintlayout.core.state.helpers.Facade {
+ ctor public HelperReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
+ method public androidx.constraintlayout.core.state.HelperReference! add(java.lang.Object!...!);
+ method public void applyBase();
+ method public androidx.constraintlayout.core.widgets.HelperWidget! getHelperWidget();
+ method public androidx.constraintlayout.core.state.State.Helper! getType();
+ method public void setHelperWidget(androidx.constraintlayout.core.widgets.HelperWidget!);
+ field protected final androidx.constraintlayout.core.state.State! mHelperState;
+ field protected java.util.ArrayList<java.lang.Object!>! mReferences;
+ }
+
+ public interface Interpolator {
+ method public float getInterpolation(float);
+ }
+
+ public interface Reference {
+ method public void apply();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public androidx.constraintlayout.core.state.helpers.Facade! getFacade();
+ method public Object! getKey();
+ method public void setConstraintWidget(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setKey(Object!);
+ }
+
+ public class Registry {
+ ctor public Registry();
+ method public String! currentContent(String!);
+ method public String! currentLayoutInformation(String!);
+ method public static androidx.constraintlayout.core.state.Registry! getInstance();
+ method public long getLastModified(String!);
+ method public java.util.Set<java.lang.String!>! getLayoutList();
+ method public void register(String!, androidx.constraintlayout.core.state.RegistryCallback!);
+ method public void setDrawDebug(String!, int);
+ method public void setLayoutInformationMode(String!, int);
+ method public void unregister(String!, androidx.constraintlayout.core.state.RegistryCallback!);
+ method public void updateContent(String!, String!);
+ method public void updateDimensions(String!, int, int);
+ method public void updateProgress(String!, float);
+ }
+
+ public interface RegistryCallback {
+ method public String! currentLayoutInformation();
+ method public String! currentMotionScene();
+ method public long getLastModified();
+ method public void onDimensions(int, int);
+ method public void onNewMotionScene(String!);
+ method public void onProgress(float);
+ method public void setDrawDebug(int);
+ method public void setLayoutInformationMode(int);
+ }
+
+ public class State {
+ ctor public State();
+ method public void apply(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ method public androidx.constraintlayout.core.state.helpers.BarrierReference! barrier(Object!, androidx.constraintlayout.core.state.State.Direction!);
+ method public void baselineNeededFor(Object!);
+ method public androidx.constraintlayout.core.state.helpers.AlignHorizontallyReference! centerHorizontally(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.AlignVerticallyReference! centerVertically(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! constraints(Object!);
+ method public int convertDimension(Object!);
+ method public androidx.constraintlayout.core.state.ConstraintReference! createConstraintReference(Object!);
+ method public void directMapping();
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getFlow(Object!, boolean);
+ method public androidx.constraintlayout.core.state.helpers.GridReference getGrid(Object, String);
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getHorizontalFlow();
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getHorizontalFlow(java.lang.Object!...!);
+ method public java.util.ArrayList<java.lang.String!>! getIdsForTag(String!);
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getVerticalFlow();
+ method public androidx.constraintlayout.core.state.helpers.FlowReference! getVerticalFlow(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! guideline(Object!, int);
+ method public androidx.constraintlayout.core.state.State! height(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.HelperReference! helper(Object!, androidx.constraintlayout.core.state.State.Helper!);
+ method public androidx.constraintlayout.core.state.helpers.HorizontalChainReference! horizontalChain();
+ method public androidx.constraintlayout.core.state.helpers.HorizontalChainReference! horizontalChain(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! horizontalGuideline(Object!);
+ method public boolean isBaselineNeeded(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method @Deprecated public boolean isLtr();
+ method public boolean isRtl();
+ method public void map(Object!, Object!);
+ method public void reset();
+ method public boolean sameFixedHeight(int);
+ method public boolean sameFixedWidth(int);
+ method public void setDpToPixel(androidx.constraintlayout.core.state.CorePixelDp!);
+ method public androidx.constraintlayout.core.state.State! setHeight(androidx.constraintlayout.core.state.Dimension!);
+ method @Deprecated public void setLtr(boolean);
+ method public void setRtl(boolean);
+ method public void setTag(String!, String!);
+ method public androidx.constraintlayout.core.state.State! setWidth(androidx.constraintlayout.core.state.Dimension!);
+ method public androidx.constraintlayout.core.state.helpers.VerticalChainReference! verticalChain();
+ method public androidx.constraintlayout.core.state.helpers.VerticalChainReference! verticalChain(java.lang.Object!...!);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! verticalGuideline(Object!);
+ method public androidx.constraintlayout.core.state.State! width(androidx.constraintlayout.core.state.Dimension!);
+ field public static final Integer PARENT;
+ field protected java.util.HashMap<java.lang.Object!,androidx.constraintlayout.core.state.HelperReference!>! mHelperReferences;
+ field public final androidx.constraintlayout.core.state.ConstraintReference! mParent;
+ field protected java.util.HashMap<java.lang.Object!,androidx.constraintlayout.core.state.Reference!>! mReferences;
+ }
+
+ public enum State.Chain {
+ method public static androidx.constraintlayout.core.state.State.Chain! getChainByString(String!);
+ method public static int getValueByString(String!);
+ enum_constant public static final androidx.constraintlayout.core.state.State.Chain PACKED;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Chain SPREAD;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Chain SPREAD_INSIDE;
+ field public static java.util.Map<java.lang.String!,androidx.constraintlayout.core.state.State.Chain!>! chainMap;
+ field public static java.util.Map<java.lang.String!,java.lang.Integer!>! valueMap;
+ }
+
+ public enum State.Constraint {
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_TOP;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_TOP;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_HORIZONTALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_VERTICALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CIRCULAR_CONSTRAINT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint END_TO_END;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint END_TO_START;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint LEFT_TO_LEFT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint LEFT_TO_RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_LEFT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_END;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_START;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_TOP;
+ }
+
+ public enum State.Direction {
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction END;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction LEFT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction START;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Direction TOP;
+ }
+
+ public enum State.Helper {
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper ALIGN_HORIZONTALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper ALIGN_VERTICALLY;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper BARRIER;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper COLUMN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper FLOW;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper GRID;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper HORIZONTAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper HORIZONTAL_FLOW;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper LAYER;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper ROW;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper VERTICAL_CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Helper VERTICAL_FLOW;
+ }
+
+ public enum State.Wrap {
+ method public static androidx.constraintlayout.core.state.State.Wrap! getChainByString(String!);
+ method public static int getValueByString(String!);
+ enum_constant public static final androidx.constraintlayout.core.state.State.Wrap ALIGNED;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Wrap CHAIN;
+ enum_constant public static final androidx.constraintlayout.core.state.State.Wrap NONE;
+ field public static java.util.Map<java.lang.String!,java.lang.Integer!>! valueMap;
+ field public static java.util.Map<java.lang.String!,androidx.constraintlayout.core.state.State.Wrap!>! wrapMap;
+ }
+
+ public class Transition implements androidx.constraintlayout.core.motion.utils.TypedValues {
+ ctor public Transition(androidx.constraintlayout.core.state.CorePixelDp);
+ method public void addCustomColor(int, String!, String!, int);
+ method public void addCustomFloat(int, String!, String!, float);
+ method public void addKeyAttribute(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void addKeyAttribute(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!, androidx.constraintlayout.core.motion.CustomVariable![]!);
+ method public void addKeyCycle(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void addKeyPosition(String!, androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void addKeyPosition(String!, int, int, float, float);
+ method public void calcStagger();
+ method public void clear();
+ method public boolean contains(String!);
+ method public float dragToProgress(float, int, int, float, float);
+ method public void fillKeyPositions(androidx.constraintlayout.core.state.WidgetFrame!, float[]!, float[]!, float[]!);
+ method public androidx.constraintlayout.core.state.Transition.KeyPosition! findNextPosition(String!, int);
+ method public androidx.constraintlayout.core.state.Transition.KeyPosition! findPreviousPosition(String!, int);
+ method public int getAutoTransition();
+ method public androidx.constraintlayout.core.state.WidgetFrame! getEnd(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getEnd(String!);
+ method public int getId(String!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getInterpolated(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getInterpolated(String!);
+ method public int getInterpolatedHeight();
+ method public int getInterpolatedWidth();
+ method public androidx.constraintlayout.core.state.Interpolator! getInterpolator();
+ method public static androidx.constraintlayout.core.state.Interpolator! getInterpolator(int, String!);
+ method public int getKeyFrames(String!, float[]!, int[]!, int[]!);
+ method public androidx.constraintlayout.core.motion.Motion! getMotion(String!);
+ method public int getNumberKeyPositions(androidx.constraintlayout.core.state.WidgetFrame!);
+ method public float[]! getPath(String!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getStart(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public androidx.constraintlayout.core.state.WidgetFrame! getStart(String!);
+ method public float getTouchUpProgress(long);
+ method public androidx.constraintlayout.core.state.Transition.WidgetState! getWidgetState(String!, androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ method public boolean hasOnSwipe();
+ method public boolean hasPositionKeyframes();
+ method public void interpolate(int, int, float);
+ method public boolean isEmpty();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public boolean isFirstDownAccepted(float, float);
+ method public boolean isTouchNotDone(float);
+ method public void setTouchUp(float, long, float, float);
+ method public void setTransitionProperties(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public boolean setValue(int, boolean);
+ method public boolean setValue(int, float);
+ method public boolean setValue(int, int);
+ method public boolean setValue(int, String!);
+ method public void updateFrom(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int);
+ field public static final int END = 1; // 0x1
+ field public static final int INTERPOLATED = 2; // 0x2
+ field public static final int START = 0; // 0x0
+ }
+
+ public static class Transition.WidgetState {
+ ctor public Transition.WidgetState();
+ method public androidx.constraintlayout.core.state.WidgetFrame! getFrame(int);
+ method public void interpolate(int, int, float, androidx.constraintlayout.core.state.Transition!);
+ method public void setKeyAttribute(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void setKeyAttribute(androidx.constraintlayout.core.motion.utils.TypedBundle!, androidx.constraintlayout.core.motion.CustomVariable![]!);
+ method public void setKeyCycle(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void setKeyPosition(androidx.constraintlayout.core.motion.utils.TypedBundle!);
+ method public void setPathRelative(androidx.constraintlayout.core.state.Transition.WidgetState!);
+ method public void update(androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ }
+
+ public class TransitionParser {
+ ctor public TransitionParser();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static void parse(androidx.constraintlayout.core.parser.CLObject, androidx.constraintlayout.core.state.Transition) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method @Deprecated public static void parse(androidx.constraintlayout.core.parser.CLObject!, androidx.constraintlayout.core.state.Transition!, androidx.constraintlayout.core.state.CorePixelDp!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public static void parseKeyFrames(androidx.constraintlayout.core.parser.CLObject!, androidx.constraintlayout.core.state.Transition!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ }
+
+ public class WidgetFrame {
+ ctor public WidgetFrame();
+ ctor public WidgetFrame(androidx.constraintlayout.core.state.WidgetFrame!);
+ ctor public WidgetFrame(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void addCustomColor(String!, int);
+ method public void addCustomFloat(String!, float);
+ method public float centerX();
+ method public float centerY();
+ method public boolean containsCustom(String);
+ method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
+ method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
+ method public int getCustomColor(String!);
+ method public float getCustomFloat(String!);
+ method public String! getId();
+ method public androidx.constraintlayout.core.motion.utils.TypedBundle! getMotionProperties();
+ method public int height();
+ method public static void interpolate(int, int, androidx.constraintlayout.core.state.WidgetFrame!, androidx.constraintlayout.core.state.WidgetFrame!, androidx.constraintlayout.core.state.WidgetFrame!, androidx.constraintlayout.core.state.Transition!, float);
+ method public boolean isDefaultTransform();
+ method public StringBuilder! serialize(StringBuilder!);
+ method public StringBuilder! serialize(StringBuilder!, boolean);
+ method public void setCustomAttribute(String!, int, boolean);
+ method public void setCustomAttribute(String!, int, float);
+ method public void setCustomAttribute(String!, int, int);
+ method public void setCustomAttribute(String!, int, String!);
+ method public void setCustomValue(androidx.constraintlayout.core.motion.CustomAttribute!, float[]!);
+ method public boolean setValue(String!, androidx.constraintlayout.core.parser.CLElement!) throws androidx.constraintlayout.core.parser.CLParsingException;
+ method public androidx.constraintlayout.core.state.WidgetFrame! update();
+ method public androidx.constraintlayout.core.state.WidgetFrame! update(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void updateAttributes(androidx.constraintlayout.core.state.WidgetFrame!);
+ method public int width();
+ field public float alpha;
+ field public int bottom;
+ field public float interpolatedPos;
+ field public int left;
+ field public String! name;
+ field public static float phone_orientation;
+ field public float pivotX;
+ field public float pivotY;
+ field public int right;
+ field public float rotationX;
+ field public float rotationY;
+ field public float rotationZ;
+ field public float scaleX;
+ field public float scaleY;
+ field public int top;
+ field public float translationX;
+ field public float translationY;
+ field public float translationZ;
+ field public int visibility;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget! widget;
+ }
+
+}
+
+package androidx.constraintlayout.core.state.helpers {
+
+ public class AlignHorizontallyReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public AlignHorizontallyReference(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class AlignVerticallyReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public AlignVerticallyReference(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class BarrierReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public BarrierReference(androidx.constraintlayout.core.state.State!);
+ method public void setBarrierDirection(androidx.constraintlayout.core.state.State.Direction!);
+ }
+
+ public class ChainReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public ChainReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void addChainElement(Object, float, float, float, float, float);
+ method public void addChainElement(String, float, float, float);
+ method public androidx.constraintlayout.core.state.helpers.ChainReference bias(float);
+ method public float getBias();
+ method protected float getPostMargin(String);
+ method protected float getPreMargin(String);
+ method public androidx.constraintlayout.core.state.State.Chain getStyle();
+ method protected float getWeight(String);
+ method public androidx.constraintlayout.core.state.helpers.ChainReference style(androidx.constraintlayout.core.state.State.Chain);
+ field protected float mBias;
+ field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPostMargin;
+ field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPreMargin;
+ field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapWeights;
+ field protected androidx.constraintlayout.core.state.State.Chain mStyle;
+ }
+
+ public interface Facade {
+ method public void apply();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ }
+
+ public class FlowReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public FlowReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
+ method public void addFlowElement(String!, float, float, float);
+ method public float getFirstHorizontalBias();
+ method public int getFirstHorizontalStyle();
+ method public float getFirstVerticalBias();
+ method public int getFirstVerticalStyle();
+ method public int getHorizontalAlign();
+ method public float getHorizontalBias();
+ method public int getHorizontalGap();
+ method public int getHorizontalStyle();
+ method public float getLastHorizontalBias();
+ method public int getLastHorizontalStyle();
+ method public float getLastVerticalBias();
+ method public int getLastVerticalStyle();
+ method public int getMaxElementsWrap();
+ method public int getOrientation();
+ method public int getPaddingBottom();
+ method public int getPaddingLeft();
+ method public int getPaddingRight();
+ method public int getPaddingTop();
+ method protected float getPostMargin(String!);
+ method protected float getPreMargin(String!);
+ method public int getVerticalAlign();
+ method public float getVerticalBias();
+ method public int getVerticalGap();
+ method public int getVerticalStyle();
+ method protected float getWeight(String!);
+ method public int getWrapMode();
+ method public void setFirstHorizontalBias(float);
+ method public void setFirstHorizontalStyle(int);
+ method public void setFirstVerticalBias(float);
+ method public void setFirstVerticalStyle(int);
+ method public void setHorizontalAlign(int);
+ method public void setHorizontalGap(int);
+ method public void setHorizontalStyle(int);
+ method public void setLastHorizontalBias(float);
+ method public void setLastHorizontalStyle(int);
+ method public void setLastVerticalBias(float);
+ method public void setLastVerticalStyle(int);
+ method public void setMaxElementsWrap(int);
+ method public void setOrientation(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingLeft(int);
+ method public void setPaddingRight(int);
+ method public void setPaddingTop(int);
+ method public void setVerticalAlign(int);
+ method public void setVerticalGap(int);
+ method public void setVerticalStyle(int);
+ method public void setWrapMode(int);
+ field protected float mFirstHorizontalBias;
+ field protected int mFirstHorizontalStyle;
+ field protected float mFirstVerticalBias;
+ field protected int mFirstVerticalStyle;
+ field protected androidx.constraintlayout.core.widgets.Flow! mFlow;
+ field protected int mHorizontalAlign;
+ field protected int mHorizontalGap;
+ field protected int mHorizontalStyle;
+ field protected float mLastHorizontalBias;
+ field protected int mLastHorizontalStyle;
+ field protected float mLastVerticalBias;
+ field protected int mLastVerticalStyle;
+ field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPostMargin;
+ field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPreMargin;
+ field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapWeights;
+ field protected int mMaxElementsWrap;
+ field protected int mOrientation;
+ field protected int mPaddingBottom;
+ field protected int mPaddingLeft;
+ field protected int mPaddingRight;
+ field protected int mPaddingTop;
+ field protected int mVerticalAlign;
+ field protected int mVerticalGap;
+ field protected int mVerticalStyle;
+ field protected int mWrapMode;
+ }
+
+ public class GridReference extends androidx.constraintlayout.core.state.HelperReference {
+ ctor public GridReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+ method public String? getColumnWeights();
+ method public int getColumnsSet();
+ method public int getFlags();
+ method public float getHorizontalGaps();
+ method public int getOrientation();
+ method public int getPaddingBottom();
+ method public int getPaddingEnd();
+ method public int getPaddingStart();
+ method public int getPaddingTop();
+ method public String? getRowWeights();
+ method public int getRowsSet();
+ method public String? getSkips();
+ method public String? getSpans();
+ method public float getVerticalGaps();
+ method public void setColumnWeights(String);
+ method public void setColumnsSet(int);
+ method public void setFlags(int);
+ method public void setFlags(String);
+ method public void setHorizontalGaps(float);
+ method public void setOrientation(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingEnd(int);
+ method public void setPaddingStart(int);
+ method public void setPaddingTop(int);
+ method public void setRowWeights(String);
+ method public void setRowsSet(int);
+ method public void setSkips(String);
+ method public void setSpans(String);
+ method public void setVerticalGaps(float);
+ }
+
+ public class GuidelineReference implements androidx.constraintlayout.core.state.helpers.Facade androidx.constraintlayout.core.state.Reference {
+ ctor public GuidelineReference(androidx.constraintlayout.core.state.State!);
+ method public void apply();
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! end(Object!);
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public androidx.constraintlayout.core.state.helpers.Facade! getFacade();
+ method public Object! getKey();
+ method public int getOrientation();
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! percent(float);
+ method public void setConstraintWidget(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setKey(Object!);
+ method public void setOrientation(int);
+ method public androidx.constraintlayout.core.state.helpers.GuidelineReference! start(Object!);
+ }
+
+ public class HorizontalChainReference extends androidx.constraintlayout.core.state.helpers.ChainReference {
+ ctor public HorizontalChainReference(androidx.constraintlayout.core.state.State!);
+ }
+
+ public class VerticalChainReference extends androidx.constraintlayout.core.state.helpers.ChainReference {
+ ctor public VerticalChainReference(androidx.constraintlayout.core.state.State!);
+ }
+
+}
+
+package androidx.constraintlayout.core.utils {
+
+ public class GridCore extends androidx.constraintlayout.core.widgets.VirtualLayout {
+ ctor public GridCore();
+ ctor public GridCore(int, int);
+ method public String? getColumnWeights();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidgetContainer? getContainer();
+ method public int getFlags();
+ method public float getHorizontalGaps();
+ method public int getOrientation();
+ method public String? getRowWeights();
+ method public float getVerticalGaps();
+ method public void setColumnWeights(String);
+ method public void setColumns(int);
+ method public void setContainer(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer);
+ method public void setFlags(int);
+ method public void setHorizontalGaps(float);
+ method public void setOrientation(int);
+ method public void setRowWeights(String);
+ method public void setRows(int);
+ method public void setSkips(String);
+ method public void setSpans(CharSequence);
+ method public void setVerticalGaps(float);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int SPANS_RESPECT_WIDGET_ORDER = 2; // 0x2
+ field public static final int SUB_GRID_BY_COL_ROW = 1; // 0x1
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+ public class GridEngine {
+ ctor public GridEngine();
+ ctor public GridEngine(int, int);
+ ctor public GridEngine(int, int, int);
+ method public int bottomOfWidget(int);
+ method public int leftOfWidget(int);
+ method public int rightOfWidget(int);
+ method public void setColumns(int);
+ method public void setNumWidgets(int);
+ method public void setOrientation(int);
+ method public void setRows(int);
+ method public void setSkips(String!);
+ method public void setSpans(CharSequence!);
+ method public void setup();
+ method public int topOfWidget(int);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+}
+
+package androidx.constraintlayout.core.widgets {
+
+ public class Barrier extends androidx.constraintlayout.core.widgets.HelperWidget {
+ ctor public Barrier();
+ ctor public Barrier(String!);
+ method public boolean allSolved();
+ method @Deprecated public boolean allowsGoneWidget();
+ method public boolean getAllowsGoneWidget();
+ method public int getBarrierType();
+ method public int getMargin();
+ method public int getOrientation();
+ method protected void markWidgets();
+ method public void setAllowsGoneWidget(boolean);
+ method public void setBarrierType(int);
+ method public void setMargin(int);
+ field public static final int BOTTOM = 3; // 0x3
+ field public static final int LEFT = 0; // 0x0
+ field public static final int RIGHT = 1; // 0x1
+ field public static final int TOP = 2; // 0x2
+ }
+
+ public class Chain {
+ ctor public Chain();
+ method public static void applyChainConstraints(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.LinearSystem!, java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>!, int);
+ field public static final boolean USE_CHAIN_OPTIMIZATION = false;
+ }
+
+ public class ChainHead {
+ ctor public ChainHead(androidx.constraintlayout.core.widgets.ConstraintWidget!, int, boolean);
+ method public void define();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getFirst();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getFirstMatchConstraintWidget();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getFirstVisibleWidget();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getHead();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getLast();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getLastMatchConstraintWidget();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getLastVisibleWidget();
+ method public float getTotalWeight();
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mFirst;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mFirstMatchConstraintWidget;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mFirstVisibleWidget;
+ field protected boolean mHasComplexMatchWeights;
+ field protected boolean mHasDefinedWeights;
+ field protected boolean mHasRatio;
+ field protected boolean mHasUndefinedWeights;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mHead;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mLast;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mLastMatchConstraintWidget;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget! mLastVisibleWidget;
+ field protected float mTotalWeight;
+ field protected java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>! mWeightedMatchConstraintsWidgets;
+ field protected int mWidgetsCount;
+ field protected int mWidgetsMatchCount;
+ }
+
+ public class ConstraintAnchor {
+ ctor public ConstraintAnchor(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!);
+ method public boolean connect(androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method public boolean connect(androidx.constraintlayout.core.widgets.ConstraintAnchor!, int, int, boolean);
+ method public void copyFrom(androidx.constraintlayout.core.widgets.ConstraintAnchor!, java.util.HashMap<androidx.constraintlayout.core.widgets.ConstraintWidget!,androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void findDependents(int, java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public java.util.HashSet<androidx.constraintlayout.core.widgets.ConstraintAnchor!>! getDependents();
+ method public int getFinalValue();
+ method public int getMargin();
+ method public final androidx.constraintlayout.core.widgets.ConstraintAnchor! getOpposite();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getOwner();
+ method public androidx.constraintlayout.core.SolverVariable! getSolverVariable();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor! getTarget();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor.Type! getType();
+ method public boolean hasCenteredDependents();
+ method public boolean hasDependents();
+ method public boolean hasFinalValue();
+ method public boolean isConnected();
+ method public boolean isConnectionAllowed(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public boolean isConnectionAllowed(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public boolean isSideAnchor();
+ method public boolean isSimilarDimensionConnection(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public boolean isValidConnection(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public boolean isVerticalAnchor();
+ method public void reset();
+ method public void resetFinalResolution();
+ method public void resetSolverVariable(androidx.constraintlayout.core.Cache!);
+ method public void setFinalValue(int);
+ method public void setGoneMargin(int);
+ method public void setMargin(int);
+ field public int mMargin;
+ field public final androidx.constraintlayout.core.widgets.ConstraintWidget! mOwner;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mTarget;
+ field public final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type! mType;
+ }
+
+ public enum ConstraintAnchor.Type {
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type BASELINE;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type BOTTOM;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type CENTER;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type CENTER_X;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type CENTER_Y;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type LEFT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type NONE;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type RIGHT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintAnchor.Type TOP;
+ }
+
+ public class ConstraintWidget {
+ ctor public ConstraintWidget();
+ ctor public ConstraintWidget(int, int);
+ ctor public ConstraintWidget(int, int, int, int);
+ ctor public ConstraintWidget(String!);
+ ctor public ConstraintWidget(String!, int, int);
+ ctor public ConstraintWidget(String!, int, int, int, int);
+ method public void addChildrenToSolverByDependency(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.LinearSystem!, java.util.HashSet<androidx.constraintlayout.core.widgets.ConstraintWidget!>!, int, boolean);
+ method public void addToSolver(androidx.constraintlayout.core.LinearSystem!, boolean);
+ method public boolean allowedInBarrier();
+ method public void connect(androidx.constraintlayout.core.widgets.ConstraintAnchor!, androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method public void connect(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!);
+ method public void connect(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, int);
+ method public void connectCircularConstraint(androidx.constraintlayout.core.widgets.ConstraintWidget!, float, int);
+ method public void copy(androidx.constraintlayout.core.widgets.ConstraintWidget!, java.util.HashMap<androidx.constraintlayout.core.widgets.ConstraintWidget!,androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void createObjectVariables(androidx.constraintlayout.core.LinearSystem!);
+ method public void ensureMeasureRequested();
+ method public void ensureWidgetRuns();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor! getAnchor(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!);
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintAnchor!>! getAnchors();
+ method public int getBaselineDistance();
+ method public float getBiasPercent(int);
+ method public int getBottom();
+ method public Object! getCompanionWidget();
+ method public int getContainerItemSkip();
+ method public String! getDebugName();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! getDimensionBehaviour(int);
+ method public float getDimensionRatio();
+ method public int getDimensionRatioSide();
+ method public boolean getHasBaseline();
+ method public int getHeight();
+ method public float getHorizontalBiasPercent();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getHorizontalChainControlWidget();
+ method public int getHorizontalChainStyle();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! getHorizontalDimensionBehaviour();
+ method public int getHorizontalMargin();
+ method public int getLastHorizontalMeasureSpec();
+ method public int getLastVerticalMeasureSpec();
+ method public int getLeft();
+ method public int getLength(int);
+ method public int getMaxHeight();
+ method public int getMaxWidth();
+ method public int getMinHeight();
+ method public int getMinWidth();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getNextChainMember(int);
+ method public int getOptimizerWrapHeight();
+ method public int getOptimizerWrapWidth();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getParent();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getPreviousChainMember(int);
+ method public int getRight();
+ method protected int getRootX();
+ method protected int getRootY();
+ method public androidx.constraintlayout.core.widgets.analyzer.WidgetRun! getRun(int);
+ method public void getSceneString(StringBuilder!);
+ method public int getTop();
+ method public String! getType();
+ method public float getVerticalBiasPercent();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getVerticalChainControlWidget();
+ method public int getVerticalChainStyle();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! getVerticalDimensionBehaviour();
+ method public int getVerticalMargin();
+ method public int getVisibility();
+ method public int getWidth();
+ method public int getWrapBehaviorInParent();
+ method public int getX();
+ method public int getY();
+ method public boolean hasBaseline();
+ method public boolean hasDanglingDimension(int);
+ method public boolean hasDependencies();
+ method public boolean hasDimensionOverride();
+ method public boolean hasResolvedTargets(int, int);
+ method public void immediateConnect(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, int, int);
+ method public boolean isAnimated();
+ method public boolean isHeightWrapContent();
+ method public boolean isHorizontalSolvingPassDone();
+ method public boolean isInBarrier(int);
+ method public boolean isInHorizontalChain();
+ method public boolean isInPlaceholder();
+ method public boolean isInVerticalChain();
+ method public boolean isInVirtualLayout();
+ method public boolean isMeasureRequested();
+ method public boolean isResolvedHorizontally();
+ method public boolean isResolvedVertically();
+ method public boolean isRoot();
+ method public boolean isSpreadHeight();
+ method public boolean isSpreadWidth();
+ method public boolean isVerticalSolvingPassDone();
+ method public boolean isWidthWrapContent();
+ method public void markHorizontalSolvingPassDone();
+ method public void markVerticalSolvingPassDone();
+ method public boolean oppositeDimensionDependsOn(int);
+ method public boolean oppositeDimensionsTied();
+ method public void reset();
+ method public void resetAllConstraints();
+ method public void resetAnchor(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public void resetAnchors();
+ method public void resetFinalResolution();
+ method public void resetSolverVariables(androidx.constraintlayout.core.Cache!);
+ method public void resetSolvingPassFlag();
+ method public StringBuilder! serialize(StringBuilder!);
+ method public void setAnimated(boolean);
+ method public void setBaselineDistance(int);
+ method public void setCompanionWidget(Object!);
+ method public void setContainerItemSkip(int);
+ method public void setDebugName(String!);
+ method public void setDebugSolverName(androidx.constraintlayout.core.LinearSystem!, String!);
+ method public void setDimension(int, int);
+ method public void setDimensionRatio(float, int);
+ method public void setDimensionRatio(String!);
+ method public void setFinalBaseline(int);
+ method public void setFinalFrame(int, int, int, int, int, int);
+ method public void setFinalHorizontal(int, int);
+ method public void setFinalLeft(int);
+ method public void setFinalTop(int);
+ method public void setFinalVertical(int, int);
+ method public void setFrame(int, int, int);
+ method public void setFrame(int, int, int, int);
+ method public void setGoneMargin(androidx.constraintlayout.core.widgets.ConstraintAnchor.Type!, int);
+ method public void setHasBaseline(boolean);
+ method public void setHeight(int);
+ method public void setHeightWrapContent(boolean);
+ method public void setHorizontalBiasPercent(float);
+ method public void setHorizontalChainStyle(int);
+ method public void setHorizontalDimension(int, int);
+ method public void setHorizontalDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ method public void setHorizontalMatchStyle(int, int, int, float);
+ method public void setHorizontalWeight(float);
+ method protected void setInBarrier(int, boolean);
+ method public void setInPlaceholder(boolean);
+ method public void setInVirtualLayout(boolean);
+ method public void setLastMeasureSpec(int, int);
+ method public void setLength(int, int);
+ method public void setMaxHeight(int);
+ method public void setMaxWidth(int);
+ method public void setMeasureRequested(boolean);
+ method public void setMinHeight(int);
+ method public void setMinWidth(int);
+ method public void setOffset(int, int);
+ method public void setOrigin(int, int);
+ method public void setParent(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void setType(String!);
+ method public void setVerticalBiasPercent(float);
+ method public void setVerticalChainStyle(int);
+ method public void setVerticalDimension(int, int);
+ method public void setVerticalDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ method public void setVerticalMatchStyle(int, int, int, float);
+ method public void setVerticalWeight(float);
+ method public void setVisibility(int);
+ method public void setWidth(int);
+ method public void setWidthWrapContent(boolean);
+ method public void setWrapBehaviorInParent(int);
+ method public void setX(int);
+ method public void setY(int);
+ method public void setupDimensionRatio(boolean, boolean, boolean, boolean);
+ method public void updateFromRuns(boolean, boolean);
+ method public void updateFromSolver(androidx.constraintlayout.core.LinearSystem!, boolean);
+ field public static final int ANCHOR_BASELINE = 4; // 0x4
+ field public static final int ANCHOR_BOTTOM = 3; // 0x3
+ field public static final int ANCHOR_LEFT = 0; // 0x0
+ field public static final int ANCHOR_RIGHT = 1; // 0x1
+ field public static final int ANCHOR_TOP = 2; // 0x2
+ field public static final int BOTH = 2; // 0x2
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static float DEFAULT_BIAS;
+ field protected static final int DIRECT = 2; // 0x2
+ field public static final int GONE = 8; // 0x8
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int INVISIBLE = 4; // 0x4
+ field public static final int MATCH_CONSTRAINT_PERCENT = 2; // 0x2
+ field public static final int MATCH_CONSTRAINT_RATIO = 3; // 0x3
+ field public static final int MATCH_CONSTRAINT_RATIO_RESOLVED = 4; // 0x4
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field protected static final int SOLVER = 1; // 0x1
+ field public static final int UNKNOWN = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int VISIBLE = 0; // 0x0
+ field public static final int WRAP_BEHAVIOR_HORIZONTAL_ONLY = 1; // 0x1
+ field public static final int WRAP_BEHAVIOR_INCLUDED = 0; // 0x0
+ field public static final int WRAP_BEHAVIOR_SKIPPED = 3; // 0x3
+ field public static final int WRAP_BEHAVIOR_VERTICAL_ONLY = 2; // 0x2
+ field public androidx.constraintlayout.core.state.WidgetFrame! frame;
+ field public androidx.constraintlayout.core.widgets.analyzer.ChainRun! horizontalChainRun;
+ field public int horizontalGroup;
+ field public boolean[]! isTerminalWidget;
+ field protected java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintAnchor!>! mAnchors;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mBaseline;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mBottom;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mCenter;
+ field public float mCircleConstraintAngle;
+ field public float mDimensionRatio;
+ field protected int mDimensionRatioSide;
+ field public int mHorizontalResolution;
+ field public androidx.constraintlayout.core.widgets.analyzer.HorizontalWidgetRun! mHorizontalRun;
+ field public boolean mIsHeightWrapContent;
+ field public boolean mIsWidthWrapContent;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mLeft;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor![]! mListAnchors;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour![]! mListDimensionBehaviors;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget![]! mListNextMatchConstraintsWidget;
+ field public int mMatchConstraintDefaultHeight;
+ field public int mMatchConstraintDefaultWidth;
+ field public int mMatchConstraintMaxHeight;
+ field public int mMatchConstraintMaxWidth;
+ field public int mMatchConstraintMinHeight;
+ field public int mMatchConstraintMinWidth;
+ field public float mMatchConstraintPercentHeight;
+ field public float mMatchConstraintPercentWidth;
+ field protected int mMinHeight;
+ field protected int mMinWidth;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget![]! mNextChainWidget;
+ field protected int mOffsetX;
+ field protected int mOffsetY;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget! mParent;
+ field public int[]! mResolvedMatchConstraintDefault;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mRight;
+ field public androidx.constraintlayout.core.widgets.ConstraintAnchor! mTop;
+ field public int mVerticalResolution;
+ field public androidx.constraintlayout.core.widgets.analyzer.VerticalWidgetRun! mVerticalRun;
+ field public float[]! mWeight;
+ field protected int mX;
+ field protected int mY;
+ field public boolean measured;
+ field public androidx.constraintlayout.core.widgets.analyzer.WidgetRun![]! run;
+ field public String! stringId;
+ field public androidx.constraintlayout.core.widgets.analyzer.ChainRun! verticalChainRun;
+ field public int verticalGroup;
+ }
+
+ public enum ConstraintWidget.DimensionBehaviour {
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour FIXED;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour MATCH_CONSTRAINT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour MATCH_PARENT;
+ enum_constant public static final androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour WRAP_CONTENT;
+ }
+
+ public class ConstraintWidgetContainer extends androidx.constraintlayout.core.widgets.WidgetContainer {
+ ctor public ConstraintWidgetContainer();
+ ctor public ConstraintWidgetContainer(int, int);
+ ctor public ConstraintWidgetContainer(int, int, int, int);
+ ctor public ConstraintWidgetContainer(String!, int, int);
+ method public boolean addChildrenToSolver(androidx.constraintlayout.core.LinearSystem!);
+ method public void addHorizontalWrapMaxVariable(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public void addHorizontalWrapMinVariable(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method public void defineTerminalWidgets();
+ method public boolean directMeasure(boolean);
+ method public boolean directMeasureSetup(boolean);
+ method public boolean directMeasureWithOrientation(boolean, int);
+ method public void fillMetrics(androidx.constraintlayout.core.Metrics!);
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.Guideline!>! getHorizontalGuidelines();
+ method public androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer! getMeasurer();
+ method public int getOptimizationLevel();
+ method public androidx.constraintlayout.core.LinearSystem! getSystem();
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.Guideline!>! getVerticalGuidelines();
+ method public boolean handlesInternalConstraints();
+ method public void invalidateGraph();
+ method public void invalidateMeasures();
+ method public boolean isHeightMeasuredTooSmall();
+ method public boolean isRtl();
+ method public boolean isWidthMeasuredTooSmall();
+ method public static boolean measure(int, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure!, int);
+ method public long measure(int, int, int, int, int, int, int, int, int);
+ method public boolean optimizeFor(int);
+ method public void setMeasurer(androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ method public void setOptimizationLevel(int);
+ method public void setPadding(int, int, int, int);
+ method public void setPass(int);
+ method public void setRtl(boolean);
+ method public boolean updateChildrenFromSolver(androidx.constraintlayout.core.LinearSystem!, boolean[]!);
+ method public void updateHierarchy();
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyGraph! mDependencyGraph;
+ field public boolean mGroupsWrapOptimized;
+ field public int mHorizontalChainsSize;
+ field public boolean mHorizontalWrapOptimized;
+ field public androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure! mMeasure;
+ field protected androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer! mMeasurer;
+ field public androidx.constraintlayout.core.Metrics! mMetrics;
+ field public boolean mSkipSolver;
+ field protected androidx.constraintlayout.core.LinearSystem! mSystem;
+ field public int mVerticalChainsSize;
+ field public boolean mVerticalWrapOptimized;
+ field public int mWrapFixedHeight;
+ field public int mWrapFixedWidth;
+ }
+
+ public class Flow extends androidx.constraintlayout.core.widgets.VirtualLayout {
+ ctor public Flow();
+ method public float getMaxElementsWrap();
+ method public void setFirstHorizontalBias(float);
+ method public void setFirstHorizontalStyle(int);
+ method public void setFirstVerticalBias(float);
+ method public void setFirstVerticalStyle(int);
+ method public void setHorizontalAlign(int);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalGap(int);
+ method public void setHorizontalStyle(int);
+ method public void setLastHorizontalBias(float);
+ method public void setLastHorizontalStyle(int);
+ method public void setLastVerticalBias(float);
+ method public void setLastVerticalStyle(int);
+ method public void setMaxElementsWrap(int);
+ method public void setOrientation(int);
+ method public void setVerticalAlign(int);
+ method public void setVerticalBias(float);
+ method public void setVerticalGap(int);
+ method public void setVerticalStyle(int);
+ method public void setWrapMode(int);
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_START = 0; // 0x0
+ field public static final int VERTICAL_ALIGN_BASELINE = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 0; // 0x0
+ field public static final int WRAP_ALIGNED = 2; // 0x2
+ field public static final int WRAP_CHAIN = 1; // 0x1
+ field public static final int WRAP_CHAIN_NEW = 3; // 0x3
+ field public static final int WRAP_NONE = 0; // 0x0
+ }
+
+ public class Guideline extends androidx.constraintlayout.core.widgets.ConstraintWidget {
+ ctor public Guideline();
+ method public void cyclePosition();
+ method public androidx.constraintlayout.core.widgets.ConstraintAnchor! getAnchor();
+ method public int getMinimumPosition();
+ method public int getOrientation();
+ method public int getRelativeBegin();
+ method public int getRelativeBehaviour();
+ method public int getRelativeEnd();
+ method public float getRelativePercent();
+ method public boolean isPercent();
+ method public void setFinalValue(int);
+ method public void setGuideBegin(int);
+ method public void setGuideEnd(int);
+ method public void setGuidePercent(float);
+ method public void setGuidePercent(int);
+ method public void setMinimumPosition(int);
+ method public void setOrientation(int);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int RELATIVE_BEGIN = 1; // 0x1
+ field public static final int RELATIVE_END = 2; // 0x2
+ field public static final int RELATIVE_PERCENT = 0; // 0x0
+ field public static final int RELATIVE_UNKNOWN = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field protected boolean mGuidelineUseRtl;
+ field protected int mRelativeBegin;
+ field protected int mRelativeEnd;
+ field protected float mRelativePercent;
+ }
+
+ public interface Helper {
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void removeAllIds();
+ method public void updateConstraints(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ }
+
+ public class HelperWidget extends androidx.constraintlayout.core.widgets.ConstraintWidget implements androidx.constraintlayout.core.widgets.Helper {
+ ctor public HelperWidget();
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void addDependents(java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!, int, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public int findGroupInDependents(int);
+ method public void removeAllIds();
+ method public void updateConstraints(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget![]! mWidgets;
+ field public int mWidgetsCount;
+ }
+
+ public class Optimizer {
+ ctor public Optimizer();
+ method public static final boolean enabled(int, int);
+ field public static final int OPTIMIZATION_BARRIER = 2; // 0x2
+ field public static final int OPTIMIZATION_CACHE_MEASURES = 256; // 0x100
+ field public static final int OPTIMIZATION_CHAIN = 4; // 0x4
+ field public static final int OPTIMIZATION_DEPENDENCY_ORDERING = 512; // 0x200
+ field public static final int OPTIMIZATION_DIMENSIONS = 8; // 0x8
+ field public static final int OPTIMIZATION_DIRECT = 1; // 0x1
+ field public static final int OPTIMIZATION_GRAPH = 64; // 0x40
+ field public static final int OPTIMIZATION_GRAPH_WRAP = 128; // 0x80
+ field public static final int OPTIMIZATION_GROUPING = 1024; // 0x400
+ field public static final int OPTIMIZATION_GROUPS = 32; // 0x20
+ field public static final int OPTIMIZATION_NONE = 0; // 0x0
+ field public static final int OPTIMIZATION_RATIO = 16; // 0x10
+ field public static final int OPTIMIZATION_STANDARD = 257; // 0x101
+ }
+
+ public class Placeholder extends androidx.constraintlayout.core.widgets.VirtualLayout {
+ ctor public Placeholder();
+ }
+
+ public class Rectangle {
+ ctor public Rectangle();
+ method public boolean contains(int, int);
+ method public int getCenterX();
+ method public int getCenterY();
+ method public void setBounds(int, int, int, int);
+ field public int height;
+ field public int width;
+ field public int x;
+ field public int y;
+ }
+
+ public class VirtualLayout extends androidx.constraintlayout.core.widgets.HelperWidget {
+ ctor public VirtualLayout();
+ method public void applyRtl(boolean);
+ method public void captureWidgets();
+ method public boolean contains(java.util.HashSet<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public int getMeasuredHeight();
+ method public int getMeasuredWidth();
+ method public int getPaddingBottom();
+ method public int getPaddingLeft();
+ method public int getPaddingRight();
+ method public int getPaddingTop();
+ method protected void measure(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, int, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, int);
+ method public void measure(int, int, int, int);
+ method protected boolean measureChildren();
+ method public boolean needSolverPass();
+ method protected void needsCallbackFromSolver(boolean);
+ method public void setMeasure(int, int);
+ method public void setPadding(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingEnd(int);
+ method public void setPaddingLeft(int);
+ method public void setPaddingRight(int);
+ method public void setPaddingStart(int);
+ method public void setPaddingTop(int);
+ field protected androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure! mMeasure;
+ }
+
+ public class WidgetContainer extends androidx.constraintlayout.core.widgets.ConstraintWidget {
+ ctor public WidgetContainer();
+ ctor public WidgetContainer(int, int);
+ ctor public WidgetContainer(int, int, int, int);
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void add(androidx.constraintlayout.core.widgets.ConstraintWidget!...!);
+ method public java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>! getChildren();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! getRootConstraintContainer();
+ method public void layout();
+ method public void remove(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void removeAllChildren();
+ field public java.util.ArrayList<androidx.constraintlayout.core.widgets.ConstraintWidget!>! mChildren;
+ }
+
+}
+
+package androidx.constraintlayout.core.widgets.analyzer {
+
+ public class BasicMeasure {
+ ctor public BasicMeasure(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ method public long solverMeasure(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int, int, int, int, int, int);
+ method public void updateHierarchy(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ field public static final int AT_MOST = -2147483648; // 0x80000000
+ field public static final int EXACTLY = 1073741824; // 0x40000000
+ field public static final int FIXED = -3; // 0xfffffffd
+ field public static final int MATCH_PARENT = -1; // 0xffffffff
+ field public static final int UNSPECIFIED = 0; // 0x0
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ }
+
+ public static class BasicMeasure.Measure {
+ ctor public BasicMeasure.Measure();
+ field public static int SELF_DIMENSIONS;
+ field public static int TRY_GIVEN_DIMENSIONS;
+ field public static int USE_GIVEN_DIMENSIONS;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! horizontalBehavior;
+ field public int horizontalDimension;
+ field public int measureStrategy;
+ field public int measuredBaseline;
+ field public boolean measuredHasBaseline;
+ field public int measuredHeight;
+ field public boolean measuredNeedsSolverPass;
+ field public int measuredWidth;
+ field public androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! verticalBehavior;
+ field public int verticalDimension;
+ }
+
+ public static interface BasicMeasure.Measurer {
+ method public void didMeasures();
+ method public void measure(androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure!);
+ }
+
+ public class ChainRun extends androidx.constraintlayout.core.widgets.analyzer.WidgetRun {
+ ctor public ChainRun(androidx.constraintlayout.core.widgets.ConstraintWidget!, int);
+ method public void applyToWidget();
+ }
+
+ public interface Dependency {
+ method public void update(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ }
+
+ public class DependencyGraph {
+ ctor public DependencyGraph(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!);
+ method public void buildGraph();
+ method public void buildGraph(java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetRun!>!);
+ method public void defineTerminalWidgets(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ method public boolean directMeasure(boolean);
+ method public boolean directMeasureSetup(boolean);
+ method public boolean directMeasureWithOrientation(boolean, int);
+ method public void invalidateGraph();
+ method public void invalidateMeasures();
+ method public void measureWidgets();
+ method public void setMeasurer(androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ }
+
+ public class DependencyNode implements androidx.constraintlayout.core.widgets.analyzer.Dependency {
+ ctor public DependencyNode(androidx.constraintlayout.core.widgets.analyzer.WidgetRun!);
+ method public void addDependency(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method public void clear();
+ method public String! name();
+ method public void resolve(int);
+ method public void update(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ field public boolean delegateToWidgetRun;
+ field public boolean readyToSolve;
+ field public boolean resolved;
+ field public androidx.constraintlayout.core.widgets.analyzer.Dependency! updateDelegate;
+ field public int value;
+ }
+
+ public class Direct {
+ ctor public Direct();
+ method public static String! ls(int);
+ method public static boolean solveChain(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.LinearSystem!, int, int, androidx.constraintlayout.core.widgets.ChainHead!, boolean, boolean, boolean);
+ method public static void solvingPass(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ }
+
+ public class Grouping {
+ ctor public Grouping();
+ method public static androidx.constraintlayout.core.widgets.analyzer.WidgetGroup! findDependents(androidx.constraintlayout.core.widgets.ConstraintWidget!, int, java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public static boolean simpleSolvingPass(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measurer!);
+ method public static boolean validInGroup(androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!, androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour!);
+ }
+
+ public class HorizontalWidgetRun extends androidx.constraintlayout.core.widgets.analyzer.WidgetRun {
+ ctor public HorizontalWidgetRun(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void applyToWidget();
+ }
+
+ public class VerticalWidgetRun extends androidx.constraintlayout.core.widgets.analyzer.WidgetRun {
+ ctor public VerticalWidgetRun(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void applyToWidget();
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyNode! baseline;
+ }
+
+ public class WidgetGroup {
+ ctor public WidgetGroup(int);
+ method public boolean add(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method public void apply();
+ method public void cleanup(java.util.ArrayList<androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!>!);
+ method public void clear();
+ method public int getId();
+ method public int getOrientation();
+ method public boolean intersectWith(androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public boolean isAuthoritative();
+ method public int measureWrap(androidx.constraintlayout.core.LinearSystem!, int);
+ method public void moveTo(int, androidx.constraintlayout.core.widgets.analyzer.WidgetGroup!);
+ method public void setAuthoritative(boolean);
+ method public void setOrientation(int);
+ method public int size();
+ }
+
+ public abstract class WidgetRun implements androidx.constraintlayout.core.widgets.analyzer.Dependency {
+ ctor public WidgetRun(androidx.constraintlayout.core.widgets.ConstraintWidget!);
+ method protected final void addTarget(androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, int);
+ method protected final void addTarget(androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, androidx.constraintlayout.core.widgets.analyzer.DependencyNode!, int, androidx.constraintlayout.core.widgets.analyzer.DimensionDependency!);
+ method protected final int getLimitedDimension(int, int);
+ method protected final androidx.constraintlayout.core.widgets.analyzer.DependencyNode! getTarget(androidx.constraintlayout.core.widgets.ConstraintAnchor!);
+ method protected final androidx.constraintlayout.core.widgets.analyzer.DependencyNode! getTarget(androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method public long getWrapDimension();
+ method public boolean isCenterConnection();
+ method public boolean isDimensionResolved();
+ method public boolean isResolved();
+ method public void update(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method protected void updateRunCenter(androidx.constraintlayout.core.widgets.analyzer.Dependency!, androidx.constraintlayout.core.widgets.ConstraintAnchor!, androidx.constraintlayout.core.widgets.ConstraintAnchor!, int);
+ method protected void updateRunEnd(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method protected void updateRunStart(androidx.constraintlayout.core.widgets.analyzer.Dependency!);
+ method public long wrapSize(int);
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyNode! end;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour! mDimensionBehavior;
+ field protected androidx.constraintlayout.core.widgets.analyzer.WidgetRun.RunType! mRunType;
+ field public int matchConstraintsType;
+ field public int orientation;
+ field public androidx.constraintlayout.core.widgets.analyzer.DependencyNode! start;
+ }
+
+}
+
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java
index 7df5ef9..69e03f2 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java
@@ -399,10 +399,12 @@
return mY1 + t * (mY2 - mY1);
}
+ @SuppressWarnings("UnusedVariable")
public double getLinearDX(double t) {
return mEllipseCenterX;
}
+ @SuppressWarnings("UnusedVariable")
public double getLinearDY(double t) {
return mEllipseCenterY;
}
diff --git a/constraintlayout/constraintlayout/api/2.2.0-beta01.txt b/constraintlayout/constraintlayout/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..2d187a8
--- /dev/null
+++ b/constraintlayout/constraintlayout/api/2.2.0-beta01.txt
@@ -0,0 +1,1714 @@
+// Signature format: 4.0
+package androidx.constraintlayout.helper.widget {
+
+ public class Carousel extends androidx.constraintlayout.motion.widget.MotionHelper {
+ ctor public Carousel(android.content.Context!);
+ ctor public Carousel(android.content.Context!, android.util.AttributeSet!);
+ ctor public Carousel(android.content.Context!, android.util.AttributeSet!, int);
+ method public int getCount();
+ method public int getCurrentIndex();
+ method public boolean isInfinite();
+ method public void jumpToIndex(int);
+ method public void refresh();
+ method public void setAdapter(androidx.constraintlayout.helper.widget.Carousel.Adapter!);
+ method public void setInfinite(boolean);
+ method public void transitionToIndex(int, int);
+ field public static final int TOUCH_UP_CARRY_ON = 2; // 0x2
+ field public static final int TOUCH_UP_IMMEDIATE_STOP = 1; // 0x1
+ }
+
+ public static interface Carousel.Adapter {
+ method public int count();
+ method public void onNewItem(int);
+ method public void populate(android.view.View!, int);
+ }
+
+ public class CircularFlow extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public CircularFlow(android.content.Context!);
+ ctor public CircularFlow(android.content.Context!, android.util.AttributeSet!);
+ ctor public CircularFlow(android.content.Context!, android.util.AttributeSet!, int);
+ method public void addViewToCircularFlow(android.view.View!, int, float);
+ method public float[]! getAngles();
+ method public int[]! getRadius();
+ method public boolean isUpdatable(android.view.View!);
+ method public void setDefaultAngle(float);
+ method public void setDefaultRadius(int);
+ method public void updateAngle(android.view.View!, float);
+ method public void updateRadius(android.view.View!, int);
+ method public void updateReference(android.view.View!, int, float);
+ }
+
+ public class Flow extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public Flow(android.content.Context!);
+ ctor public Flow(android.content.Context!, android.util.AttributeSet!);
+ ctor public Flow(android.content.Context!, android.util.AttributeSet!, int);
+ method public void setFirstHorizontalBias(float);
+ method public void setFirstHorizontalStyle(int);
+ method public void setFirstVerticalBias(float);
+ method public void setFirstVerticalStyle(int);
+ method public void setHorizontalAlign(int);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalGap(int);
+ method public void setHorizontalStyle(int);
+ method public void setLastHorizontalBias(float);
+ method public void setLastHorizontalStyle(int);
+ method public void setLastVerticalBias(float);
+ method public void setLastVerticalStyle(int);
+ method public void setMaxElementsWrap(int);
+ method public void setOrientation(int);
+ method public void setPadding(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingLeft(int);
+ method public void setPaddingRight(int);
+ method public void setPaddingTop(int);
+ method public void setVerticalAlign(int);
+ method public void setVerticalBias(float);
+ method public void setVerticalGap(int);
+ method public void setVerticalStyle(int);
+ method public void setWrapMode(int);
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_START = 0; // 0x0
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_BASELINE = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 0; // 0x0
+ field public static final int WRAP_ALIGNED = 2; // 0x2
+ field public static final int WRAP_CHAIN = 1; // 0x1
+ field public static final int WRAP_NONE = 0; // 0x0
+ }
+
+ public class Grid extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public Grid(android.content.Context!);
+ ctor public Grid(android.content.Context!, android.util.AttributeSet!);
+ ctor public Grid(android.content.Context!, android.util.AttributeSet!, int);
+ method public String! getColumnWeights();
+ method public int getColumns();
+ method public float getHorizontalGaps();
+ method public int getOrientation();
+ method public String! getRowWeights();
+ method public int getRows();
+ method public String! getSkips();
+ method public String! getSpans();
+ method public float getVerticalGaps();
+ method public void setColumnWeights(String!);
+ method public void setColumns(int);
+ method public void setHorizontalGaps(float);
+ method public void setOrientation(int);
+ method public void setRowWeights(String!);
+ method public void setRows(int);
+ method public void setSkips(String!);
+ method public void setSpans(CharSequence!);
+ method public void setVerticalGaps(float);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+ public class Layer extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public Layer(android.content.Context!);
+ ctor public Layer(android.content.Context!, android.util.AttributeSet!);
+ ctor public Layer(android.content.Context!, android.util.AttributeSet!, int);
+ method protected void calcCenters();
+ field protected float mComputedCenterX;
+ field protected float mComputedCenterY;
+ field protected float mComputedMaxX;
+ field protected float mComputedMaxY;
+ field protected float mComputedMinX;
+ field protected float mComputedMinY;
+ }
+
+ public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
+ ctor public MotionEffect(android.content.Context!);
+ ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!, int);
+ field public static final int AUTO = -1; // 0xffffffff
+ field public static final int EAST = 2; // 0x2
+ field public static final int NORTH = 0; // 0x0
+ field public static final int SOUTH = 1; // 0x1
+ field public static final String TAG = "FadeMove";
+ field public static final int WEST = 3; // 0x3
+ }
+
+ public class MotionPlaceholder extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public MotionPlaceholder(android.content.Context!);
+ ctor public MotionPlaceholder(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionPlaceholder(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public MotionPlaceholder(android.content.Context!, android.util.AttributeSet!, int, int);
+ }
+
+}
+
+package androidx.constraintlayout.motion.utils {
+
+ public class CustomSupport {
+ ctor public CustomSupport();
+ method public static void setInterpolatedValue(androidx.constraintlayout.widget.ConstraintAttribute!, android.view.View!, float[]!);
+ }
+
+ public class StopLogic extends androidx.constraintlayout.motion.widget.MotionInterpolator {
+ ctor public StopLogic();
+ method public void config(float, float, float, float, float, float);
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ method public void springConfig(float, float, float, float, float, float, float, int);
+ }
+
+ public abstract class ViewOscillator extends androidx.constraintlayout.core.motion.utils.KeyCycleOscillator {
+ ctor public ViewOscillator();
+ method public static androidx.constraintlayout.motion.utils.ViewOscillator! makeSpline(String!);
+ method public abstract void setProperty(android.view.View!, float);
+ }
+
+ public static class ViewOscillator.PathRotateSet extends androidx.constraintlayout.motion.utils.ViewOscillator {
+ ctor public ViewOscillator.PathRotateSet();
+ method public void setPathRotate(android.view.View!, float, double, double);
+ method public void setProperty(android.view.View!, float);
+ }
+
+ public abstract class ViewSpline extends androidx.constraintlayout.core.motion.utils.SplineSet {
+ ctor public ViewSpline();
+ method public static androidx.constraintlayout.motion.utils.ViewSpline! makeCustomSpline(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public static androidx.constraintlayout.motion.utils.ViewSpline! makeSpline(String!);
+ method public abstract void setProperty(android.view.View!, float);
+ }
+
+ public static class ViewSpline.CustomSet extends androidx.constraintlayout.motion.utils.ViewSpline {
+ ctor public ViewSpline.CustomSet(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public void setPoint(int, androidx.constraintlayout.widget.ConstraintAttribute!);
+ method public void setProperty(android.view.View!, float);
+ }
+
+ public static class ViewSpline.PathRotate extends androidx.constraintlayout.motion.utils.ViewSpline {
+ ctor public ViewSpline.PathRotate();
+ method public void setPathRotate(android.view.View!, float, double, double);
+ method public void setProperty(android.view.View!, float);
+ }
+
+ public class ViewState {
+ ctor public ViewState();
+ method public void getState(android.view.View!);
+ method public int height();
+ method public int width();
+ field public int bottom;
+ field public int left;
+ field public int right;
+ field public float rotation;
+ field public int top;
+ }
+
+ public abstract class ViewTimeCycle extends androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet {
+ ctor public ViewTimeCycle();
+ method public float get(float, long, android.view.View!, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ method public static androidx.constraintlayout.motion.utils.ViewTimeCycle! makeCustomSpline(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public static androidx.constraintlayout.motion.utils.ViewTimeCycle! makeSpline(String!, long);
+ method public abstract boolean setProperty(android.view.View!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ public static class ViewTimeCycle.CustomSet extends androidx.constraintlayout.motion.utils.ViewTimeCycle {
+ ctor public ViewTimeCycle.CustomSet(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public void setPoint(int, androidx.constraintlayout.widget.ConstraintAttribute!, float, int, float);
+ method public boolean setProperty(android.view.View!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ public static class ViewTimeCycle.PathRotate extends androidx.constraintlayout.motion.utils.ViewTimeCycle {
+ ctor public ViewTimeCycle.PathRotate();
+ method public boolean setPathRotate(android.view.View!, androidx.constraintlayout.core.motion.utils.KeyCache!, float, long, double, double);
+ method public boolean setProperty(android.view.View!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+}
+
+package androidx.constraintlayout.motion.widget {
+
+ public interface Animatable {
+ method public float getProgress();
+ method public void setProgress(float);
+ }
+
+ public interface CustomFloatAttributes {
+ method public float get(String!);
+ method public String![]! getListOfAttributes();
+ method public void set(String!, float);
+ }
+
+ public class Debug {
+ ctor public Debug();
+ method public static void dumpLayoutParams(android.view.ViewGroup!, String!);
+ method public static void dumpLayoutParams(android.view.ViewGroup.LayoutParams!, String!);
+ method public static void dumpPoc(Object!);
+ method public static String! getActionType(android.view.MotionEvent!);
+ method public static String! getCallFrom(int);
+ method public static String! getLoc();
+ method public static String! getLocation();
+ method public static String! getLocation2();
+ method public static String! getName(android.content.Context!, int);
+ method public static String! getName(android.content.Context!, int[]!);
+ method public static String! getName(android.view.View!);
+ method public static String! getState(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public static String! getState(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public static void logStack(String!, String!, int);
+ method public static void printStack(String!, int);
+ }
+
+ public class DesignTool {
+ ctor public DesignTool(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public int designAccess(int, String!, Object!, float[]!, int, float[]!, int);
+ method public void disableAutoTransition(boolean);
+ method public void dumpConstraintSet(String!);
+ method public int getAnimationKeyFrames(Object!, float[]!);
+ method public int getAnimationPath(Object!, float[]!, int);
+ method public void getAnimationRectangles(Object!, float[]!);
+ method public String! getEndState();
+ method public int getKeyFrameInfo(Object!, int, int[]!);
+ method public float getKeyFramePosition(Object!, int, float, float);
+ method public int getKeyFramePositions(Object!, int[]!, float[]!);
+ method public Object! getKeyframe(int, int, int);
+ method public Object! getKeyframe(Object!, int, int);
+ method public Object! getKeyframeAtLocation(Object!, float, float);
+ method public Boolean! getPositionKeyframe(Object!, Object!, float, float, String![]!, float[]!);
+ method public float getProgress();
+ method public String! getStartState();
+ method public String! getState();
+ method public long getTransitionTimeMs();
+ method public boolean isInTransition();
+ method public void setAttributes(int, String!, Object!, Object!);
+ method public void setKeyFrame(Object!, int, String!, Object!);
+ method public boolean setKeyFramePosition(Object!, int, int, float, float);
+ method public void setKeyframe(Object!, String!, Object!);
+ method public void setState(String!);
+ method public void setToolPosition(float);
+ method public void setTransition(String!, String!);
+ method public void setViewDebug(Object!, int);
+ }
+
+ public interface FloatLayout {
+ method public void layout(float, float, float, float);
+ }
+
+ public abstract class Key {
+ ctor public Key();
+ method public abstract void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public abstract androidx.constraintlayout.motion.widget.Key! clone();
+ method public androidx.constraintlayout.motion.widget.Key! copy(androidx.constraintlayout.motion.widget.Key!);
+ method public int getFramePosition();
+ method public void setFramePosition(int);
+ method public void setInterpolation(java.util.HashMap<java.lang.String!,java.lang.Integer!>!);
+ method public abstract void setValue(String!, Object!);
+ method public androidx.constraintlayout.motion.widget.Key! setViewId(int);
+ field public static final String ALPHA = "alpha";
+ field public static final String CURVEFIT = "curveFit";
+ field public static final String CUSTOM = "CUSTOM";
+ field public static final String ELEVATION = "elevation";
+ field public static final String MOTIONPROGRESS = "motionProgress";
+ field public static final String PIVOT_X = "transformPivotX";
+ field public static final String PIVOT_Y = "transformPivotY";
+ field public static final String PROGRESS = "progress";
+ field public static final String ROTATION = "rotation";
+ field public static final String ROTATION_X = "rotationX";
+ field public static final String ROTATION_Y = "rotationY";
+ field public static final String SCALE_X = "scaleX";
+ field public static final String SCALE_Y = "scaleY";
+ field public static final String TRANSITIONEASING = "transitionEasing";
+ field public static final String TRANSITION_PATH_ROTATE = "transitionPathRotate";
+ field public static final String TRANSLATION_X = "translationX";
+ field public static final String TRANSLATION_Y = "translationY";
+ field public static final String TRANSLATION_Z = "translationZ";
+ field public static int UNSET;
+ field public static final String VISIBILITY = "visibility";
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_PHASE = "wavePhase";
+ field public static final String WAVE_VARIES_BY = "waveVariesBy";
+ field protected int mType;
+ }
+
+ public class KeyAttributes extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyAttributes();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final int KEY_TYPE = 1; // 0x1
+ }
+
+ public class KeyCycle extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyCycle();
+ method public void addCycleValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewOscillator!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public float getValue(String!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final int KEY_TYPE = 4; // 0x4
+ field public static final int SHAPE_BOUNCE = 6; // 0x6
+ field public static final int SHAPE_COS_WAVE = 5; // 0x5
+ field public static final int SHAPE_REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SHAPE_SAW_WAVE = 3; // 0x3
+ field public static final int SHAPE_SIN_WAVE = 0; // 0x0
+ field public static final int SHAPE_SQUARE_WAVE = 1; // 0x1
+ field public static final int SHAPE_TRIANGLE_WAVE = 2; // 0x2
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_PHASE = "wavePhase";
+ field public static final String WAVE_SHAPE = "waveShape";
+ }
+
+ public class KeyFrames {
+ ctor public KeyFrames();
+ ctor public KeyFrames(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public void addAllFrames(androidx.constraintlayout.motion.widget.MotionController!);
+ method public void addFrames(androidx.constraintlayout.motion.widget.MotionController!);
+ method public void addKey(androidx.constraintlayout.motion.widget.Key!);
+ method public java.util.ArrayList<androidx.constraintlayout.motion.widget.Key!>! getKeyFramesForView(int);
+ method public java.util.Set<java.lang.Integer!>! getKeys();
+ field public static final int UNSET = -1; // 0xffffffff
+ }
+
+ public class KeyPosition extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyPosition();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public boolean intersects(int, int, android.graphics.RectF!, android.graphics.RectF!, float, float);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void positionAttributes(android.view.View!, android.graphics.RectF!, android.graphics.RectF!, float, float, String![]!, float[]!);
+ method public void setType(int);
+ method public void setValue(String!, Object!);
+ field public static final String DRAWPATH = "drawPath";
+ field public static final String PERCENT_HEIGHT = "percentHeight";
+ field public static final String PERCENT_WIDTH = "percentWidth";
+ field public static final String PERCENT_X = "percentX";
+ field public static final String PERCENT_Y = "percentY";
+ field public static final String SIZE_PERCENT = "sizePercent";
+ field public static final String TRANSITION_EASING = "transitionEasing";
+ field public static final int TYPE_AXIS = 3; // 0x3
+ field public static final int TYPE_CARTESIAN = 0; // 0x0
+ field public static final int TYPE_PATH = 1; // 0x1
+ field public static final int TYPE_SCREEN = 2; // 0x2
+ }
+
+ public class KeyTimeCycle extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyTimeCycle();
+ method public void addTimeValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewTimeCycle!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final int KEY_TYPE = 3; // 0x3
+ field public static final int SHAPE_BOUNCE = 6; // 0x6
+ field public static final int SHAPE_COS_WAVE = 5; // 0x5
+ field public static final int SHAPE_REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SHAPE_SAW_WAVE = 3; // 0x3
+ field public static final int SHAPE_SIN_WAVE = 0; // 0x0
+ field public static final int SHAPE_SQUARE_WAVE = 1; // 0x1
+ field public static final int SHAPE_TRIANGLE_WAVE = 2; // 0x2
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_SHAPE = "waveShape";
+ }
+
+ public class KeyTrigger extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyTrigger();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void conditionallyFire(float, android.view.View!);
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final String CROSS = "CROSS";
+ field public static final int KEY_TYPE = 5; // 0x5
+ field public static final String NEGATIVE_CROSS = "negativeCross";
+ field public static final String POSITIVE_CROSS = "positiveCross";
+ field public static final String POST_LAYOUT = "postLayout";
+ field public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
+ field public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
+ field public static final String TRIGGER_ID = "triggerID";
+ field public static final String TRIGGER_RECEIVER = "triggerReceiver";
+ field public static final String TRIGGER_SLACK = "triggerSlack";
+ field public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
+ field public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
+ field public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
+ }
+
+ public class MotionController {
+ method public void addKey(androidx.constraintlayout.motion.widget.Key!);
+ method public int getAnimateRelativeTo();
+ method public void getCenter(double, float[]!, float[]!);
+ method public float getCenterX();
+ method public float getCenterY();
+ method public int getDrawPath();
+ method public float getFinalHeight();
+ method public float getFinalWidth();
+ method public float getFinalX();
+ method public float getFinalY();
+ method public int getKeyFrameInfo(int, int[]!);
+ method public int getKeyFramePositions(int[]!, float[]!);
+ method public float getStartHeight();
+ method public float getStartWidth();
+ method public float getStartX();
+ method public float getStartY();
+ method public int getTransformPivotTarget();
+ method public android.view.View! getView();
+ method public void remeasure();
+ method public void setDrawPath(int);
+ method public void setPathMotionArc(int);
+ method public void setStartState(androidx.constraintlayout.motion.utils.ViewState!, android.view.View!, int, int, int);
+ method public void setTransformPivotTarget(int);
+ method public void setView(android.view.View!);
+ method public void setup(int, int, float, long);
+ method public void setupRelative(androidx.constraintlayout.motion.widget.MotionController!);
+ field public static final int DRAW_PATH_AS_CONFIGURED = 4; // 0x4
+ field public static final int DRAW_PATH_BASIC = 1; // 0x1
+ field public static final int DRAW_PATH_CARTESIAN = 3; // 0x3
+ field public static final int DRAW_PATH_NONE = 0; // 0x0
+ field public static final int DRAW_PATH_RECTANGLE = 5; // 0x5
+ field public static final int DRAW_PATH_RELATIVE = 2; // 0x2
+ field public static final int DRAW_PATH_SCREEN = 6; // 0x6
+ field public static final int HORIZONTAL_PATH_X = 2; // 0x2
+ field public static final int HORIZONTAL_PATH_Y = 3; // 0x3
+ field public static final int PATH_PERCENT = 0; // 0x0
+ field public static final int PATH_PERPENDICULAR = 1; // 0x1
+ field public static final int ROTATION_LEFT = 2; // 0x2
+ field public static final int ROTATION_RIGHT = 1; // 0x1
+ field public static final int VERTICAL_PATH_X = 4; // 0x4
+ field public static final int VERTICAL_PATH_Y = 5; // 0x5
+ }
+
+ public class MotionHelper extends androidx.constraintlayout.widget.ConstraintHelper implements androidx.constraintlayout.motion.widget.MotionHelperInterface {
+ ctor public MotionHelper(android.content.Context!);
+ ctor public MotionHelper(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionHelper(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getProgress();
+ method public boolean isDecorator();
+ method public boolean isUseOnHide();
+ method public boolean isUsedOnShow();
+ method public void onFinishedMotionScene(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void onPostDraw(android.graphics.Canvas!);
+ method public void onPreDraw(android.graphics.Canvas!);
+ method public void onPreSetup(androidx.constraintlayout.motion.widget.MotionLayout!, java.util.HashMap<android.view.View!,androidx.constraintlayout.motion.widget.MotionController!>!);
+ method public void onTransitionChange(androidx.constraintlayout.motion.widget.MotionLayout!, int, int, float);
+ method public void onTransitionCompleted(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void onTransitionStarted(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public void onTransitionTrigger(androidx.constraintlayout.motion.widget.MotionLayout!, int, boolean, float);
+ method public void setProgress(android.view.View!, float);
+ method public void setProgress(float);
+ field protected android.view.View![]! views;
+ }
+
+ public interface MotionHelperInterface extends androidx.constraintlayout.motion.widget.Animatable androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener {
+ method public boolean isDecorator();
+ method public boolean isUseOnHide();
+ method public boolean isUsedOnShow();
+ method public void onFinishedMotionScene(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void onPostDraw(android.graphics.Canvas!);
+ method public void onPreDraw(android.graphics.Canvas!);
+ method public void onPreSetup(androidx.constraintlayout.motion.widget.MotionLayout!, java.util.HashMap<android.view.View!,androidx.constraintlayout.motion.widget.MotionController!>!);
+ }
+
+ public abstract class MotionInterpolator implements android.view.animation.Interpolator {
+ ctor public MotionInterpolator();
+ method public abstract float getVelocity();
+ }
+
+ public class MotionLayout extends androidx.constraintlayout.widget.ConstraintLayout implements androidx.core.view.NestedScrollingParent3 {
+ ctor public MotionLayout(android.content.Context);
+ ctor public MotionLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public MotionLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public void addTransitionListener(androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener!);
+ method public boolean applyViewTransition(int, androidx.constraintlayout.motion.widget.MotionController!);
+ method public androidx.constraintlayout.widget.ConstraintSet! cloneConstraintSet(int);
+ method public void enableTransition(int, boolean);
+ method public void enableViewTransition(int, boolean);
+ method protected void fireTransitionCompleted();
+ method public void fireTrigger(int, boolean, float);
+ method public androidx.constraintlayout.widget.ConstraintSet! getConstraintSet(int);
+ method @IdRes public int[]! getConstraintSetIds();
+ method public int getCurrentState();
+ method public java.util.ArrayList<androidx.constraintlayout.motion.widget.MotionScene.Transition!>! getDefinedTransitions();
+ method public androidx.constraintlayout.motion.widget.DesignTool! getDesignTool();
+ method public int getEndState();
+ method public int[]! getMatchingConstraintSetIds(java.lang.String!...!);
+ method protected long getNanoTime();
+ method public float getProgress();
+ method public androidx.constraintlayout.motion.widget.MotionScene! getScene();
+ method public int getStartState();
+ method public float getTargetPosition();
+ method public androidx.constraintlayout.motion.widget.MotionScene.Transition! getTransition(int);
+ method public android.os.Bundle! getTransitionState();
+ method public long getTransitionTimeMs();
+ method public float getVelocity();
+ method public void getViewVelocity(android.view.View!, float, float, float[]!, int);
+ method public boolean isDelayedApplicationOfInitialState();
+ method public boolean isInRotation();
+ method public boolean isInteractionEnabled();
+ method public boolean isViewTransitionEnabled(int);
+ method public void jumpToState(int);
+ method protected androidx.constraintlayout.motion.widget.MotionLayout.MotionTracker! obtainVelocityTracker();
+ method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int, int[]!);
+ method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+ method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+ method public void onStopNestedScroll(android.view.View, int);
+ method @Deprecated public void rebuildMotion();
+ method public void rebuildScene();
+ method public boolean removeTransitionListener(androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener!);
+ method public void rotateTo(int, int);
+ method public void scheduleTransitionTo(int);
+ method public void setDebugMode(int);
+ method public void setDelayedApplicationOfInitialState(boolean);
+ method public void setInteractionEnabled(boolean);
+ method public void setInterpolatedProgress(float);
+ method public void setOnHide(float);
+ method public void setOnShow(float);
+ method public void setProgress(float);
+ method public void setProgress(float, float);
+ method public void setScene(androidx.constraintlayout.motion.widget.MotionScene!);
+ method protected void setTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public void setTransition(int);
+ method public void setTransition(int, int);
+ method public void setTransitionDuration(int);
+ method public void setTransitionListener(androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener!);
+ method public void setTransitionState(android.os.Bundle!);
+ method public void touchAnimateTo(int, float, float);
+ method public void touchSpringTo(float, float);
+ method public void transitionToEnd();
+ method public void transitionToEnd(Runnable!);
+ method public void transitionToStart();
+ method public void transitionToStart(Runnable!);
+ method public void transitionToState(int);
+ method public void transitionToState(int, int);
+ method public void transitionToState(int, int, int);
+ method public void transitionToState(int, int, int, int);
+ method public void updateState();
+ method public void updateState(int, androidx.constraintlayout.widget.ConstraintSet!);
+ method public void updateStateAnimate(int, androidx.constraintlayout.widget.ConstraintSet!, int);
+ method public void viewTransition(int, android.view.View!...!);
+ field public static final int DEBUG_SHOW_NONE = 0; // 0x0
+ field public static final int DEBUG_SHOW_PATH = 2; // 0x2
+ field public static final int DEBUG_SHOW_PROGRESS = 1; // 0x1
+ field public static boolean IS_IN_EDIT_MODE;
+ field public static final int TOUCH_UP_COMPLETE = 0; // 0x0
+ field public static final int TOUCH_UP_COMPLETE_TO_END = 2; // 0x2
+ field public static final int TOUCH_UP_COMPLETE_TO_START = 1; // 0x1
+ field public static final int TOUCH_UP_DECELERATE = 4; // 0x4
+ field public static final int TOUCH_UP_DECELERATE_AND_COMPLETE = 5; // 0x5
+ field public static final int TOUCH_UP_NEVER_TO_END = 7; // 0x7
+ field public static final int TOUCH_UP_NEVER_TO_START = 6; // 0x6
+ field public static final int TOUCH_UP_STOP = 3; // 0x3
+ field public static final int VELOCITY_LAYOUT = 1; // 0x1
+ field public static final int VELOCITY_POST_LAYOUT = 0; // 0x0
+ field public static final int VELOCITY_STATIC_LAYOUT = 3; // 0x3
+ field public static final int VELOCITY_STATIC_POST_LAYOUT = 2; // 0x2
+ field protected boolean mMeasureDuringTransition;
+ }
+
+ protected static interface MotionLayout.MotionTracker {
+ method public void addMovement(android.view.MotionEvent!);
+ method public void clear();
+ method public void computeCurrentVelocity(int);
+ method public void computeCurrentVelocity(int, float);
+ method public float getXVelocity();
+ method public float getXVelocity(int);
+ method public float getYVelocity();
+ method public float getYVelocity(int);
+ method public void recycle();
+ }
+
+ public static interface MotionLayout.TransitionListener {
+ method public void onTransitionChange(androidx.constraintlayout.motion.widget.MotionLayout!, int, int, float);
+ method public void onTransitionCompleted(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void onTransitionStarted(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public void onTransitionTrigger(androidx.constraintlayout.motion.widget.MotionLayout!, int, boolean, float);
+ }
+
+ public class MotionScene {
+ ctor public MotionScene(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void addOnClickListeners(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void addTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public boolean applyViewTransition(int, androidx.constraintlayout.motion.widget.MotionController!);
+ method public androidx.constraintlayout.motion.widget.MotionScene.Transition! bestTransitionFor(int, float, float, android.view.MotionEvent!);
+ method public void disableAutoTransition(boolean);
+ method public void enableViewTransition(int, boolean);
+ method public int gatPathMotionArc();
+ method public androidx.constraintlayout.widget.ConstraintSet! getConstraintSet(android.content.Context!, String!);
+ method public int[]! getConstraintSetIds();
+ method public java.util.ArrayList<androidx.constraintlayout.motion.widget.MotionScene.Transition!>! getDefinedTransitions();
+ method public int getDuration();
+ method public android.view.animation.Interpolator! getInterpolator();
+ method public void getKeyFrames(androidx.constraintlayout.motion.widget.MotionController!);
+ method public int[]! getMatchingStateLabels(java.lang.String!...!);
+ method public float getPathPercent(android.view.View!, int);
+ method public float getStaggered();
+ method public androidx.constraintlayout.motion.widget.MotionScene.Transition! getTransitionById(int);
+ method public java.util.List<androidx.constraintlayout.motion.widget.MotionScene.Transition!>! getTransitionsWithState(int);
+ method public boolean isViewTransitionEnabled(int);
+ method public int lookUpConstraintId(String!);
+ method public String! lookUpConstraintName(int);
+ method protected void onLayout(boolean, int, int, int, int);
+ method public void removeTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public void setConstraintSet(int, androidx.constraintlayout.widget.ConstraintSet!);
+ method public void setDuration(int);
+ method public void setKeyframe(android.view.View!, int, String!, Object!);
+ method public void setRtl(boolean);
+ method public void setTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public static String! stripID(String!);
+ method public boolean validateLayout(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void viewTransition(int, android.view.View!...!);
+ field public static final int LAYOUT_CALL_MEASURE = 2; // 0x2
+ field public static final int LAYOUT_HONOR_REQUEST = 1; // 0x1
+ field public static final int LAYOUT_IGNORE_REQUEST = 0; // 0x0
+ field public static final int UNSET = -1; // 0xffffffff
+ }
+
+ public static class MotionScene.Transition {
+ ctor public MotionScene.Transition(int, androidx.constraintlayout.motion.widget.MotionScene!, int, int);
+ method public void addKeyFrame(androidx.constraintlayout.motion.widget.KeyFrames!);
+ method public void addOnClick(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public void addOnClick(int, int);
+ method public String! debugString(android.content.Context!);
+ method public int getAutoTransition();
+ method public int getDuration();
+ method public int getEndConstraintSetId();
+ method public int getId();
+ method public java.util.List<androidx.constraintlayout.motion.widget.KeyFrames!>! getKeyFrameList();
+ method public int getLayoutDuringTransition();
+ method public java.util.List<androidx.constraintlayout.motion.widget.MotionScene.Transition.TransitionOnClick!>! getOnClickList();
+ method public int getPathMotionArc();
+ method public float getStagger();
+ method public int getStartConstraintSetId();
+ method public androidx.constraintlayout.motion.widget.TouchResponse! getTouchResponse();
+ method public boolean isEnabled();
+ method public boolean isTransitionFlag(int);
+ method public void removeOnClick(int);
+ method public void setAutoTransition(int);
+ method public void setDuration(int);
+ method public void setEnabled(boolean);
+ method public void setInterpolatorInfo(int, String!, int);
+ method public void setLayoutDuringTransition(int);
+ method public void setOnSwipe(androidx.constraintlayout.motion.widget.OnSwipe!);
+ method public void setOnTouchUp(int);
+ method public void setPathMotionArc(int);
+ method public void setStagger(float);
+ method public void setTransitionFlag(int);
+ field public static final int AUTO_ANIMATE_TO_END = 4; // 0x4
+ field public static final int AUTO_ANIMATE_TO_START = 3; // 0x3
+ field public static final int AUTO_JUMP_TO_END = 2; // 0x2
+ field public static final int AUTO_JUMP_TO_START = 1; // 0x1
+ field public static final int AUTO_NONE = 0; // 0x0
+ field public static final int INTERPOLATE_ANTICIPATE = 6; // 0x6
+ field public static final int INTERPOLATE_BOUNCE = 4; // 0x4
+ field public static final int INTERPOLATE_EASE_IN = 1; // 0x1
+ field public static final int INTERPOLATE_EASE_IN_OUT = 0; // 0x0
+ field public static final int INTERPOLATE_EASE_OUT = 2; // 0x2
+ field public static final int INTERPOLATE_LINEAR = 3; // 0x3
+ field public static final int INTERPOLATE_OVERSHOOT = 5; // 0x5
+ field public static final int INTERPOLATE_REFERENCE_ID = -2; // 0xfffffffe
+ field public static final int INTERPOLATE_SPLINE_STRING = -1; // 0xffffffff
+ }
+
+ public static class MotionScene.Transition.TransitionOnClick implements android.view.View.OnClickListener {
+ ctor public MotionScene.Transition.TransitionOnClick(android.content.Context!, androidx.constraintlayout.motion.widget.MotionScene.Transition!, org.xmlpull.v1.XmlPullParser!);
+ ctor public MotionScene.Transition.TransitionOnClick(androidx.constraintlayout.motion.widget.MotionScene.Transition!, int, int);
+ method public void addOnClickListeners(androidx.constraintlayout.motion.widget.MotionLayout!, int, androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public void onClick(android.view.View!);
+ method public void removeOnClickListeners(androidx.constraintlayout.motion.widget.MotionLayout!);
+ field public static final int ANIM_TOGGLE = 17; // 0x11
+ field public static final int ANIM_TO_END = 1; // 0x1
+ field public static final int ANIM_TO_START = 16; // 0x10
+ field public static final int JUMP_TO_END = 256; // 0x100
+ field public static final int JUMP_TO_START = 4096; // 0x1000
+ }
+
+ public class OnSwipe {
+ ctor public OnSwipe();
+ method public int getAutoCompleteMode();
+ method public int getDragDirection();
+ method public float getDragScale();
+ method public float getDragThreshold();
+ method public int getLimitBoundsTo();
+ method public float getMaxAcceleration();
+ method public float getMaxVelocity();
+ method public boolean getMoveWhenScrollAtTop();
+ method public int getNestedScrollFlags();
+ method public int getOnTouchUp();
+ method public int getRotationCenterId();
+ method public int getSpringBoundary();
+ method public float getSpringDamping();
+ method public float getSpringMass();
+ method public float getSpringStiffness();
+ method public float getSpringStopThreshold();
+ method public int getTouchAnchorId();
+ method public int getTouchAnchorSide();
+ method public int getTouchRegionId();
+ method public void setAutoCompleteMode(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setDragDirection(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setDragScale(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setDragThreshold(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setLimitBoundsTo(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setMaxAcceleration(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setMaxVelocity(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setMoveWhenScrollAtTop(boolean);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setNestedScrollFlags(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setOnTouchUp(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setRotateCenter(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringBoundary(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringDamping(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringMass(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringStiffness(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringStopThreshold(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setTouchAnchorId(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setTouchAnchorSide(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setTouchRegionId(int);
+ field public static final int COMPLETE_MODE_CONTINUOUS_VELOCITY = 0; // 0x0
+ field public static final int COMPLETE_MODE_SPRING = 1; // 0x1
+ field public static final int DRAG_ANTICLOCKWISE = 7; // 0x7
+ field public static final int DRAG_CLOCKWISE = 6; // 0x6
+ field public static final int DRAG_DOWN = 1; // 0x1
+ field public static final int DRAG_END = 5; // 0x5
+ field public static final int DRAG_LEFT = 2; // 0x2
+ field public static final int DRAG_RIGHT = 3; // 0x3
+ field public static final int DRAG_START = 4; // 0x4
+ field public static final int DRAG_UP = 0; // 0x0
+ field public static final int FLAG_DISABLE_POST_SCROLL = 1; // 0x1
+ field public static final int FLAG_DISABLE_SCROLL = 2; // 0x2
+ field public static final int ON_UP_AUTOCOMPLETE = 0; // 0x0
+ field public static final int ON_UP_AUTOCOMPLETE_TO_END = 2; // 0x2
+ field public static final int ON_UP_AUTOCOMPLETE_TO_START = 1; // 0x1
+ field public static final int ON_UP_DECELERATE = 4; // 0x4
+ field public static final int ON_UP_DECELERATE_AND_COMPLETE = 5; // 0x5
+ field public static final int ON_UP_NEVER_TO_END = 7; // 0x7
+ field public static final int ON_UP_NEVER_TO_START = 6; // 0x6
+ field public static final int ON_UP_STOP = 3; // 0x3
+ field public static final int SIDE_BOTTOM = 3; // 0x3
+ field public static final int SIDE_END = 6; // 0x6
+ field public static final int SIDE_LEFT = 1; // 0x1
+ field public static final int SIDE_MIDDLE = 4; // 0x4
+ field public static final int SIDE_RIGHT = 2; // 0x2
+ field public static final int SIDE_START = 5; // 0x5
+ field public static final int SIDE_TOP = 0; // 0x0
+ field public static final int SPRING_BOUNDARY_BOUNCEBOTH = 3; // 0x3
+ field public static final int SPRING_BOUNDARY_BOUNCEEND = 2; // 0x2
+ field public static final int SPRING_BOUNDARY_BOUNCESTART = 1; // 0x1
+ field public static final int SPRING_BOUNDARY_OVERSHOOT = 0; // 0x0
+ }
+
+ public abstract class TransitionAdapter implements androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener {
+ ctor public TransitionAdapter();
+ method public void onTransitionChange(androidx.constraintlayout.motion.widget.MotionLayout!, int, int, float);
+ method public void onTransitionCompleted(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void onTransitionStarted(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public void onTransitionTrigger(androidx.constraintlayout.motion.widget.MotionLayout!, int, boolean, float);
+ }
+
+ public class TransitionBuilder {
+ ctor public TransitionBuilder();
+ method public static androidx.constraintlayout.motion.widget.MotionScene.Transition! buildTransition(androidx.constraintlayout.motion.widget.MotionScene!, int, int, androidx.constraintlayout.widget.ConstraintSet!, int, androidx.constraintlayout.widget.ConstraintSet!);
+ method public static void validate(androidx.constraintlayout.motion.widget.MotionLayout!);
+ }
+
+ public class ViewTransition {
+ method public int getSharedValue();
+ method public int getSharedValueCurrent();
+ method public int getSharedValueID();
+ method public int getStateTransition();
+ method public void setSharedValue(int);
+ method public void setSharedValueCurrent(int);
+ method public void setSharedValueID(int);
+ method public void setStateTransition(int);
+ field public static final String CONSTRAINT_OVERRIDE = "ConstraintOverride";
+ field public static final String CUSTOM_ATTRIBUTE = "CustomAttribute";
+ field public static final String CUSTOM_METHOD = "CustomMethod";
+ field public static final String KEY_FRAME_SET_TAG = "KeyFrameSet";
+ field public static final int ONSTATE_ACTION_DOWN = 1; // 0x1
+ field public static final int ONSTATE_ACTION_DOWN_UP = 3; // 0x3
+ field public static final int ONSTATE_ACTION_UP = 2; // 0x2
+ field public static final int ONSTATE_SHARED_VALUE_SET = 4; // 0x4
+ field public static final int ONSTATE_SHARED_VALUE_UNSET = 5; // 0x5
+ field public static final String VIEW_TRANSITION_TAG = "ViewTransition";
+ }
+
+ public class ViewTransitionController {
+ ctor public ViewTransitionController(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void add(androidx.constraintlayout.motion.widget.ViewTransition!);
+ }
+
+}
+
+package androidx.constraintlayout.utils.widget {
+
+ public class ImageFilterButton extends androidx.appcompat.widget.AppCompatImageButton {
+ ctor public ImageFilterButton(android.content.Context!);
+ ctor public ImageFilterButton(android.content.Context!, android.util.AttributeSet!);
+ ctor public ImageFilterButton(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getContrast();
+ method public float getCrossfade();
+ method public float getImagePanX();
+ method public float getImagePanY();
+ method public float getImageRotate();
+ method public float getImageZoom();
+ method public float getRound();
+ method public float getRoundPercent();
+ method public float getSaturation();
+ method public float getWarmth();
+ method public void setAltImageResource(int);
+ method public void setBrightness(float);
+ method public void setContrast(float);
+ method public void setCrossfade(float);
+ method public void setImagePanX(float);
+ method public void setImagePanY(float);
+ method public void setImageRotate(float);
+ method public void setImageZoom(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ method public void setSaturation(float);
+ method public void setWarmth(float);
+ }
+
+ public class ImageFilterView extends androidx.appcompat.widget.AppCompatImageView {
+ ctor public ImageFilterView(android.content.Context!);
+ ctor public ImageFilterView(android.content.Context!, android.util.AttributeSet!);
+ ctor public ImageFilterView(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getBrightness();
+ method public float getContrast();
+ method public float getCrossfade();
+ method public float getImagePanX();
+ method public float getImagePanY();
+ method public float getImageRotate();
+ method public float getImageZoom();
+ method public float getRound();
+ method public float getRoundPercent();
+ method public float getSaturation();
+ method public float getWarmth();
+ method public void setAltImageDrawable(android.graphics.drawable.Drawable!);
+ method public void setAltImageResource(int);
+ method public void setBrightness(float);
+ method public void setContrast(float);
+ method public void setCrossfade(float);
+ method public void setImagePanX(float);
+ method public void setImagePanY(float);
+ method public void setImageRotate(float);
+ method public void setImageZoom(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ method public void setSaturation(float);
+ method public void setWarmth(float);
+ }
+
+ public class MockView extends android.view.View {
+ ctor public MockView(android.content.Context!);
+ ctor public MockView(android.content.Context!, android.util.AttributeSet!);
+ ctor public MockView(android.content.Context!, android.util.AttributeSet!, int);
+ method public void onDraw(android.graphics.Canvas);
+ field protected String! mText;
+ }
+
+ public class MotionButton extends androidx.appcompat.widget.AppCompatButton {
+ ctor public MotionButton(android.content.Context!);
+ ctor public MotionButton(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionButton(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getRound();
+ method public float getRoundPercent();
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ }
+
+ public class MotionLabel extends android.view.View implements androidx.constraintlayout.motion.widget.FloatLayout {
+ ctor public MotionLabel(android.content.Context!);
+ ctor public MotionLabel(android.content.Context!, android.util.AttributeSet?);
+ ctor public MotionLabel(android.content.Context!, android.util.AttributeSet?, int);
+ method public float getRound();
+ method public float getRoundPercent();
+ method public float getScaleFromTextSize();
+ method public float getTextBackgroundPanX();
+ method public float getTextBackgroundPanY();
+ method public float getTextBackgroundRotate();
+ method public float getTextBackgroundZoom();
+ method public int getTextOutlineColor();
+ method public float getTextPanX();
+ method public float getTextPanY();
+ method public float getTextureHeight();
+ method public float getTextureWidth();
+ method public android.graphics.Typeface! getTypeface();
+ method public void layout(float, float, float, float);
+ method public void setGravity(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ method public void setScaleFromTextSize(float);
+ method public void setText(CharSequence!);
+ method public void setTextBackgroundPanX(float);
+ method public void setTextBackgroundPanY(float);
+ method public void setTextBackgroundRotate(float);
+ method public void setTextBackgroundZoom(float);
+ method public void setTextFillColor(int);
+ method public void setTextOutlineColor(int);
+ method public void setTextOutlineThickness(float);
+ method public void setTextPanX(float);
+ method public void setTextPanY(float);
+ method public void setTextSize(float);
+ method public void setTextureHeight(float);
+ method public void setTextureWidth(float);
+ method public void setTypeface(android.graphics.Typeface!);
+ }
+
+ public class MotionTelltales extends androidx.constraintlayout.utils.widget.MockView {
+ ctor public MotionTelltales(android.content.Context!);
+ ctor public MotionTelltales(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionTelltales(android.content.Context!, android.util.AttributeSet!, int);
+ method public void setText(CharSequence!);
+ }
+
+}
+
+package androidx.constraintlayout.widget {
+
+ public class Barrier extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public Barrier(android.content.Context!);
+ ctor public Barrier(android.content.Context!, android.util.AttributeSet!);
+ ctor public Barrier(android.content.Context!, android.util.AttributeSet!, int);
+ method @Deprecated public boolean allowsGoneWidget();
+ method public boolean getAllowsGoneWidget();
+ method public int getMargin();
+ method public int getType();
+ method public void setAllowsGoneWidget(boolean);
+ method public void setDpMargin(int);
+ method public void setMargin(int);
+ method public void setType(int);
+ field public static final int BOTTOM = 3; // 0x3
+ field public static final int END = 6; // 0x6
+ field public static final int LEFT = 0; // 0x0
+ field public static final int RIGHT = 1; // 0x1
+ field public static final int START = 5; // 0x5
+ field public static final int TOP = 2; // 0x2
+ }
+
+ public class ConstraintAttribute {
+ ctor public ConstraintAttribute(androidx.constraintlayout.widget.ConstraintAttribute!, Object!);
+ ctor public ConstraintAttribute(String!, androidx.constraintlayout.widget.ConstraintAttribute.AttributeType!);
+ ctor public ConstraintAttribute(String!, androidx.constraintlayout.widget.ConstraintAttribute.AttributeType!, Object!, boolean);
+ method public void applyCustom(android.view.View!);
+ method public boolean diff(androidx.constraintlayout.widget.ConstraintAttribute!);
+ method public static java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>! extractAttributes(java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>!, android.view.View!);
+ method public int getColorValue();
+ method public float getFloatValue();
+ method public int getIntegerValue();
+ method public String! getName();
+ method public String! getStringValue();
+ method public androidx.constraintlayout.widget.ConstraintAttribute.AttributeType! getType();
+ method public float getValueToInterpolate();
+ method public void getValuesToInterpolate(float[]!);
+ method public boolean isBooleanValue();
+ method public boolean isContinuous();
+ method public boolean isMethod();
+ method public int numberOfInterpolatedValues();
+ method public static void parse(android.content.Context!, org.xmlpull.v1.XmlPullParser!, java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public static void setAttributes(android.view.View!, java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public void setColorValue(int);
+ method public void setFloatValue(float);
+ method public void setIntValue(int);
+ method public void setStringValue(String!);
+ method public void setValue(float[]!);
+ method public void setValue(Object!);
+ }
+
+ public enum ConstraintAttribute.AttributeType {
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType BOOLEAN_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType COLOR_DRAWABLE_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType COLOR_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType DIMENSION_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType FLOAT_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType INT_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType REFERENCE_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType STRING_TYPE;
+ }
+
+ public abstract class ConstraintHelper extends android.view.View {
+ ctor public ConstraintHelper(android.content.Context!);
+ ctor public ConstraintHelper(android.content.Context!, android.util.AttributeSet!);
+ ctor public ConstraintHelper(android.content.Context!, android.util.AttributeSet!, int);
+ method public void addView(android.view.View!);
+ method public void applyHelperParams();
+ method protected void applyLayoutFeatures();
+ method protected void applyLayoutFeatures(androidx.constraintlayout.widget.ConstraintLayout!);
+ method protected void applyLayoutFeaturesInConstraintSet(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public boolean containsId(int);
+ method public int[]! getReferencedIds();
+ method protected android.view.View![]! getViews(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public int indexFromId(int);
+ method protected void init(android.util.AttributeSet!);
+ method public static boolean isChildOfHelper(android.view.View!);
+ method public void loadParameters(androidx.constraintlayout.widget.ConstraintSet.Constraint!, androidx.constraintlayout.core.widgets.HelperWidget!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void onDraw(android.graphics.Canvas);
+ method public int removeView(android.view.View!);
+ method public void resolveRtl(androidx.constraintlayout.core.widgets.ConstraintWidget!, boolean);
+ method protected void setIds(String!);
+ method protected void setReferenceTags(String!);
+ method public void setReferencedIds(int[]!);
+ method public void updatePostConstraints(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePostLayout(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePostMeasure(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePreDraw(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePreLayout(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.widgets.Helper!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void updatePreLayout(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void validateParams();
+ field protected static final String CHILD_TAG = "CONSTRAINT_LAYOUT_HELPER_CHILD";
+ field protected int mCount;
+ field protected androidx.constraintlayout.core.widgets.Helper! mHelperWidget;
+ field protected int[]! mIds;
+ field protected java.util.HashMap<java.lang.Integer!,java.lang.String!>! mMap;
+ field protected String! mReferenceIds;
+ field protected String! mReferenceTags;
+ field protected boolean mUseViewMeasure;
+ field protected android.content.Context! myContext;
+ }
+
+ public class ConstraintLayout extends android.view.ViewGroup {
+ ctor public ConstraintLayout(android.content.Context);
+ ctor public ConstraintLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public ConstraintLayout(android.content.Context, android.util.AttributeSet?, int);
+ ctor public ConstraintLayout(android.content.Context, android.util.AttributeSet?, int, int);
+ method public void addValueModifier(androidx.constraintlayout.widget.ConstraintLayout.ValueModifier!);
+ method protected void applyConstraintsFromLayoutParams(boolean, android.view.View!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method protected boolean dynamicUpdateConstraints(int, int);
+ method public void fillMetrics(androidx.constraintlayout.core.Metrics!);
+ method protected androidx.constraintlayout.widget.ConstraintLayout.LayoutParams! generateDefaultLayoutParams();
+ method public androidx.constraintlayout.widget.ConstraintLayout.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+ method public Object! getDesignInformation(int, Object!);
+ method public int getMaxHeight();
+ method public int getMaxWidth();
+ method public int getMinHeight();
+ method public int getMinWidth();
+ method public int getOptimizationLevel();
+ method public String! getSceneString();
+ method public static androidx.constraintlayout.widget.SharedValues! getSharedValues();
+ method public android.view.View! getViewById(int);
+ method public final androidx.constraintlayout.core.widgets.ConstraintWidget! getViewWidget(android.view.View!);
+ method protected boolean isRtl();
+ method public void loadLayoutDescription(int);
+ method protected void parseLayoutDescription(int);
+ method protected void resolveMeasuredDimension(int, int, int, int, boolean, boolean);
+ method protected void resolveSystem(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int);
+ method public void setConstraintSet(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void setDesignInformation(int, Object!, Object!);
+ method public void setMaxHeight(int);
+ method public void setMaxWidth(int);
+ method public void setMinHeight(int);
+ method public void setMinWidth(int);
+ method public void setOnConstraintsChanged(androidx.constraintlayout.widget.ConstraintsChangedListener!);
+ method public void setOptimizationLevel(int);
+ method protected void setSelfDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int);
+ method public void setState(int, int, int);
+ field public static final int DESIGN_INFO_ID = 0; // 0x0
+ field public static final String VERSION = "ConstraintLayout-2.2.0-alpha04";
+ field protected androidx.constraintlayout.widget.ConstraintLayoutStates! mConstraintLayoutSpec;
+ field protected boolean mDirtyHierarchy;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! mLayoutWidget;
+ }
+
+ public static class ConstraintLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+ ctor public ConstraintLayout.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+ ctor public ConstraintLayout.LayoutParams(android.view.ViewGroup.LayoutParams!);
+ ctor public ConstraintLayout.LayoutParams(int, int);
+ method public String! getConstraintTag();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public void reset();
+ method public void setWidgetDebugName(String!);
+ method public void validate();
+ field public static final int BASELINE = 5; // 0x5
+ field public static final int BOTTOM = 4; // 0x4
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static final int CIRCLE = 8; // 0x8
+ field public static final int END = 7; // 0x7
+ field public static final int GONE_UNSET = -2147483648; // 0x80000000
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int LEFT = 1; // 0x1
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_PERCENT = 2; // 0x2
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int RIGHT = 2; // 0x2
+ field public static final int START = 6; // 0x6
+ field public static final int TOP = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int WRAP_BEHAVIOR_HORIZONTAL_ONLY = 1; // 0x1
+ field public static final int WRAP_BEHAVIOR_INCLUDED = 0; // 0x0
+ field public static final int WRAP_BEHAVIOR_SKIPPED = 3; // 0x3
+ field public static final int WRAP_BEHAVIOR_VERTICAL_ONLY = 2; // 0x2
+ field public int baselineMargin;
+ field public int baselineToBaseline;
+ field public int baselineToBottom;
+ field public int baselineToTop;
+ field public int bottomToBottom;
+ field public int bottomToTop;
+ field public float circleAngle;
+ field public int circleConstraint;
+ field public int circleRadius;
+ field public boolean constrainedHeight;
+ field public boolean constrainedWidth;
+ field public String! constraintTag;
+ field public String! dimensionRatio;
+ field public int editorAbsoluteX;
+ field public int editorAbsoluteY;
+ field public int endToEnd;
+ field public int endToStart;
+ field public int goneBaselineMargin;
+ field public int goneBottomMargin;
+ field public int goneEndMargin;
+ field public int goneLeftMargin;
+ field public int goneRightMargin;
+ field public int goneStartMargin;
+ field public int goneTopMargin;
+ field public int guideBegin;
+ field public int guideEnd;
+ field public float guidePercent;
+ field public boolean guidelineUseRtl;
+ field public boolean helped;
+ field public float horizontalBias;
+ field public int horizontalChainStyle;
+ field public float horizontalWeight;
+ field public int leftToLeft;
+ field public int leftToRight;
+ field public int matchConstraintDefaultHeight;
+ field public int matchConstraintDefaultWidth;
+ field public int matchConstraintMaxHeight;
+ field public int matchConstraintMaxWidth;
+ field public int matchConstraintMinHeight;
+ field public int matchConstraintMinWidth;
+ field public float matchConstraintPercentHeight;
+ field public float matchConstraintPercentWidth;
+ field public int orientation;
+ field public int rightToLeft;
+ field public int rightToRight;
+ field public int startToEnd;
+ field public int startToStart;
+ field public int topToBottom;
+ field public int topToTop;
+ field public float verticalBias;
+ field public int verticalChainStyle;
+ field public float verticalWeight;
+ field public int wrapBehaviorInParent;
+ }
+
+ public static interface ConstraintLayout.ValueModifier {
+ method public boolean update(int, int, int, android.view.View!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!);
+ }
+
+ public class ConstraintLayoutStates {
+ method public boolean needsToChange(int, float, float);
+ method public void setOnConstraintsChanged(androidx.constraintlayout.widget.ConstraintsChangedListener!);
+ method public void updateConstraints(int, float, float);
+ field public static final String TAG = "ConstraintLayoutStates";
+ }
+
+ public class ConstraintLayoutStatistics {
+ ctor public ConstraintLayoutStatistics(androidx.constraintlayout.widget.ConstraintLayout!);
+ ctor public ConstraintLayoutStatistics(androidx.constraintlayout.widget.ConstraintLayoutStatistics!);
+ method public void attach(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public androidx.constraintlayout.widget.ConstraintLayoutStatistics! clone();
+ method public void detach();
+ method public long getValue(int);
+ method public void logSummary(String!);
+ method public void logSummary(String!, androidx.constraintlayout.widget.ConstraintLayoutStatistics!);
+ method public void reset();
+ field public static final int DURATION_OF_CHILD_MEASURES = 5; // 0x5
+ field public static final int DURATION_OF_LAYOUT = 7; // 0x7
+ field public static final int DURATION_OF_MEASURES = 6; // 0x6
+ field public static final int NUMBER_OF_CHILD_MEASURES = 4; // 0x4
+ field public static final int NUMBER_OF_CHILD_VIEWS = 3; // 0x3
+ field public static final int NUMBER_OF_EQUATIONS = 9; // 0x9
+ field public static final int NUMBER_OF_LAYOUTS = 1; // 0x1
+ field public static final int NUMBER_OF_ON_MEASURES = 2; // 0x2
+ field public static final int NUMBER_OF_SIMPLE_EQUATIONS = 10; // 0xa
+ field public static final int NUMBER_OF_VARIABLES = 8; // 0x8
+ }
+
+ public class ConstraintProperties {
+ ctor public ConstraintProperties(android.view.View!);
+ method public androidx.constraintlayout.widget.ConstraintProperties! addToHorizontalChain(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! addToHorizontalChainRTL(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! addToVerticalChain(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! alpha(float);
+ method public void apply();
+ method public androidx.constraintlayout.widget.ConstraintProperties! center(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontally(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontally(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontallyRtl(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontallyRtl(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerVertically(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerVertically(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! connect(int, int, int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainDefaultHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainDefaultWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMaxHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMaxWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMinHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMinWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! dimensionRatio(String!);
+ method public androidx.constraintlayout.widget.ConstraintProperties! elevation(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! goneMargin(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! horizontalBias(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! horizontalChainStyle(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! horizontalWeight(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! margin(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! removeConstraints(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! removeFromHorizontalChain();
+ method public androidx.constraintlayout.widget.ConstraintProperties! removeFromVerticalChain();
+ method public androidx.constraintlayout.widget.ConstraintProperties! rotation(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! rotationX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! rotationY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! scaleX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! scaleY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! transformPivot(float, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! transformPivotX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! transformPivotY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translation(float, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translationX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translationY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translationZ(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! verticalBias(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! verticalChainStyle(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! verticalWeight(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! visibility(int);
+ field public static final int BASELINE = 5; // 0x5
+ field public static final int BOTTOM = 4; // 0x4
+ field public static final int END = 7; // 0x7
+ field public static final int LEFT = 1; // 0x1
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int RIGHT = 2; // 0x2
+ field public static final int START = 6; // 0x6
+ field public static final int TOP = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ }
+
+ public class ConstraintSet {
+ ctor public ConstraintSet();
+ method public void addColorAttributes(java.lang.String!...!);
+ method public void addFloatAttributes(java.lang.String!...!);
+ method public void addIntAttributes(java.lang.String!...!);
+ method public void addStringAttributes(java.lang.String!...!);
+ method public void addToHorizontalChain(int, int, int);
+ method public void addToHorizontalChainRTL(int, int, int);
+ method public void addToVerticalChain(int, int, int);
+ method public void applyCustomAttributes(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void applyDeltaFrom(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void applyTo(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void applyToHelper(androidx.constraintlayout.widget.ConstraintHelper!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void applyToLayoutParams(int, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!);
+ method public void applyToWithoutCustom(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public static androidx.constraintlayout.widget.ConstraintSet.Constraint! buildDelta(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public void center(int, int, int, int, int, int, int, float);
+ method public void centerHorizontally(int, int);
+ method public void centerHorizontally(int, int, int, int, int, int, int, float);
+ method public void centerHorizontallyRtl(int, int);
+ method public void centerHorizontallyRtl(int, int, int, int, int, int, int, float);
+ method public void centerVertically(int, int);
+ method public void centerVertically(int, int, int, int, int, int, int, float);
+ method public void clear(int);
+ method public void clear(int, int);
+ method public void clone(android.content.Context!, int);
+ method public void clone(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void clone(androidx.constraintlayout.widget.Constraints!);
+ method public void clone(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void connect(int, int, int, int);
+ method public void connect(int, int, int, int, int);
+ method public void constrainCircle(int, int, int, float);
+ method public void constrainDefaultHeight(int, int);
+ method public void constrainDefaultWidth(int, int);
+ method public void constrainHeight(int, int);
+ method public void constrainMaxHeight(int, int);
+ method public void constrainMaxWidth(int, int);
+ method public void constrainMinHeight(int, int);
+ method public void constrainMinWidth(int, int);
+ method public void constrainPercentHeight(int, float);
+ method public void constrainPercentWidth(int, float);
+ method public void constrainWidth(int, int);
+ method public void constrainedHeight(int, boolean);
+ method public void constrainedWidth(int, boolean);
+ method public void create(int, int);
+ method public void createBarrier(int, int, int, int...!);
+ method public void createHorizontalChain(int, int, int, int, int[]!, float[]!, int);
+ method public void createHorizontalChainRtl(int, int, int, int, int[]!, float[]!, int);
+ method public void createVerticalChain(int, int, int, int, int[]!, float[]!, int);
+ method public void dump(androidx.constraintlayout.motion.widget.MotionScene!, int...!);
+ method public boolean getApplyElevation(int);
+ method public androidx.constraintlayout.widget.ConstraintSet.Constraint! getConstraint(int);
+ method public java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>! getCustomAttributeSet();
+ method public int getHeight(int);
+ method public int[]! getKnownIds();
+ method public androidx.constraintlayout.widget.ConstraintSet.Constraint! getParameters(int);
+ method public int[]! getReferencedIds(int);
+ method public String![]! getStateLabels();
+ method public int getVisibility(int);
+ method public int getVisibilityMode(int);
+ method public int getWidth(int);
+ method public boolean isForceId();
+ method public boolean isValidateOnParse();
+ method public void load(android.content.Context!, int);
+ method public void load(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public boolean matchesLabels(java.lang.String!...!);
+ method public void parseColorAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void parseFloatAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void parseIntAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void parseStringAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void readFallback(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void readFallback(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void removeAttribute(String!);
+ method public void removeFromHorizontalChain(int);
+ method public void removeFromVerticalChain(int);
+ method public void setAlpha(int, float);
+ method public void setApplyElevation(int, boolean);
+ method public void setBarrierType(int, int);
+ method public void setColorValue(int, String!, int);
+ method public void setDimensionRatio(int, String!);
+ method public void setEditorAbsoluteX(int, int);
+ method public void setEditorAbsoluteY(int, int);
+ method public void setElevation(int, float);
+ method public void setFloatValue(int, String!, float);
+ method public void setForceId(boolean);
+ method public void setGoneMargin(int, int, int);
+ method public void setGuidelineBegin(int, int);
+ method public void setGuidelineEnd(int, int);
+ method public void setGuidelinePercent(int, float);
+ method public void setHorizontalBias(int, float);
+ method public void setHorizontalChainStyle(int, int);
+ method public void setHorizontalWeight(int, float);
+ method public void setIntValue(int, String!, int);
+ method public void setLayoutWrapBehavior(int, int);
+ method public void setMargin(int, int, int);
+ method public void setReferencedIds(int, int...!);
+ method public void setRotation(int, float);
+ method public void setRotationX(int, float);
+ method public void setRotationY(int, float);
+ method public void setScaleX(int, float);
+ method public void setScaleY(int, float);
+ method public void setStateLabels(String!);
+ method public void setStateLabelsList(java.lang.String!...!);
+ method public void setStringValue(int, String!, String!);
+ method public void setTransformPivot(int, float, float);
+ method public void setTransformPivotX(int, float);
+ method public void setTransformPivotY(int, float);
+ method public void setTranslation(int, float, float);
+ method public void setTranslationX(int, float);
+ method public void setTranslationY(int, float);
+ method public void setTranslationZ(int, float);
+ method public void setValidateOnParse(boolean);
+ method public void setVerticalBias(int, float);
+ method public void setVerticalChainStyle(int, int);
+ method public void setVerticalWeight(int, float);
+ method public void setVisibility(int, int);
+ method public void setVisibilityMode(int, int);
+ method public void writeState(java.io.Writer!, androidx.constraintlayout.widget.ConstraintLayout!, int) throws java.io.IOException;
+ field public static final int BASELINE = 5; // 0x5
+ field public static final int BOTTOM = 4; // 0x4
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static final int CIRCLE_REFERENCE = 8; // 0x8
+ field public static final int END = 7; // 0x7
+ field public static final int GONE = 8; // 0x8
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int HORIZONTAL_GUIDELINE = 0; // 0x0
+ field public static final int INVISIBLE = 4; // 0x4
+ field public static final int LEFT = 1; // 0x1
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_PERCENT = 2; // 0x2
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int RIGHT = 2; // 0x2
+ field public static final int ROTATE_LEFT_OF_PORTRATE = 4; // 0x4
+ field public static final int ROTATE_NONE = 0; // 0x0
+ field public static final int ROTATE_PORTRATE_OF_LEFT = 2; // 0x2
+ field public static final int ROTATE_PORTRATE_OF_RIGHT = 1; // 0x1
+ field public static final int ROTATE_RIGHT_OF_PORTRATE = 3; // 0x3
+ field public static final int START = 6; // 0x6
+ field public static final int TOP = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int VERTICAL_GUIDELINE = 1; // 0x1
+ field public static final int VISIBILITY_MODE_IGNORE = 1; // 0x1
+ field public static final int VISIBILITY_MODE_NORMAL = 0; // 0x0
+ field public static final int VISIBLE = 0; // 0x0
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ field public String! derivedState;
+ field public String! mIdString;
+ field public int mRotate;
+ }
+
+ public static class ConstraintSet.Constraint {
+ ctor public ConstraintSet.Constraint();
+ method public void applyDelta(androidx.constraintlayout.widget.ConstraintSet.Constraint!);
+ method public void applyTo(androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!);
+ method public androidx.constraintlayout.widget.ConstraintSet.Constraint! clone();
+ method public void printDelta(String!);
+ field public final androidx.constraintlayout.widget.ConstraintSet.Layout! layout;
+ field public java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>! mCustomConstraints;
+ field public final androidx.constraintlayout.widget.ConstraintSet.Motion! motion;
+ field public final androidx.constraintlayout.widget.ConstraintSet.PropertySet! propertySet;
+ field public final androidx.constraintlayout.widget.ConstraintSet.Transform! transform;
+ }
+
+ public static class ConstraintSet.Layout {
+ ctor public ConstraintSet.Layout();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.Layout!);
+ method public void dump(androidx.constraintlayout.motion.widget.MotionScene!, StringBuilder!);
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int UNSET_GONE_MARGIN = -2147483648; // 0x80000000
+ field public int baselineMargin;
+ field public int baselineToBaseline;
+ field public int baselineToBottom;
+ field public int baselineToTop;
+ field public int bottomMargin;
+ field public int bottomToBottom;
+ field public int bottomToTop;
+ field public float circleAngle;
+ field public int circleConstraint;
+ field public int circleRadius;
+ field public boolean constrainedHeight;
+ field public boolean constrainedWidth;
+ field public String! dimensionRatio;
+ field public int editorAbsoluteX;
+ field public int editorAbsoluteY;
+ field public int endMargin;
+ field public int endToEnd;
+ field public int endToStart;
+ field public int goneBaselineMargin;
+ field public int goneBottomMargin;
+ field public int goneEndMargin;
+ field public int goneLeftMargin;
+ field public int goneRightMargin;
+ field public int goneStartMargin;
+ field public int goneTopMargin;
+ field public int guideBegin;
+ field public int guideEnd;
+ field public float guidePercent;
+ field public boolean guidelineUseRtl;
+ field public int heightDefault;
+ field public int heightMax;
+ field public int heightMin;
+ field public float heightPercent;
+ field public float horizontalBias;
+ field public int horizontalChainStyle;
+ field public float horizontalWeight;
+ field public int leftMargin;
+ field public int leftToLeft;
+ field public int leftToRight;
+ field public boolean mApply;
+ field public boolean mBarrierAllowsGoneWidgets;
+ field public int mBarrierDirection;
+ field public int mBarrierMargin;
+ field public String! mConstraintTag;
+ field public int mHeight;
+ field public int mHelperType;
+ field public boolean mIsGuideline;
+ field public boolean mOverride;
+ field public String! mReferenceIdString;
+ field public int[]! mReferenceIds;
+ field public int mWidth;
+ field public int mWrapBehavior;
+ field public int orientation;
+ field public int rightMargin;
+ field public int rightToLeft;
+ field public int rightToRight;
+ field public int startMargin;
+ field public int startToEnd;
+ field public int startToStart;
+ field public int topMargin;
+ field public int topToBottom;
+ field public int topToTop;
+ field public float verticalBias;
+ field public int verticalChainStyle;
+ field public float verticalWeight;
+ field public int widthDefault;
+ field public int widthMax;
+ field public int widthMin;
+ field public float widthPercent;
+ }
+
+ public static class ConstraintSet.Motion {
+ ctor public ConstraintSet.Motion();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.Motion!);
+ field public int mAnimateCircleAngleTo;
+ field public int mAnimateRelativeTo;
+ field public boolean mApply;
+ field public int mDrawPath;
+ field public float mMotionStagger;
+ field public int mPathMotionArc;
+ field public float mPathRotate;
+ field public int mPolarRelativeTo;
+ field public int mQuantizeInterpolatorID;
+ field public String! mQuantizeInterpolatorString;
+ field public int mQuantizeInterpolatorType;
+ field public float mQuantizeMotionPhase;
+ field public int mQuantizeMotionSteps;
+ field public String! mTransitionEasing;
+ }
+
+ public static class ConstraintSet.PropertySet {
+ ctor public ConstraintSet.PropertySet();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.PropertySet!);
+ field public float alpha;
+ field public boolean mApply;
+ field public float mProgress;
+ field public int mVisibilityMode;
+ field public int visibility;
+ }
+
+ public static class ConstraintSet.Transform {
+ ctor public ConstraintSet.Transform();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.Transform!);
+ field public boolean applyElevation;
+ field public float elevation;
+ field public boolean mApply;
+ field public float rotation;
+ field public float rotationX;
+ field public float rotationY;
+ field public float scaleX;
+ field public float scaleY;
+ field public int transformPivotTarget;
+ field public float transformPivotX;
+ field public float transformPivotY;
+ field public float translationX;
+ field public float translationY;
+ field public float translationZ;
+ }
+
+ public class Constraints extends android.view.ViewGroup {
+ ctor public Constraints(android.content.Context!);
+ ctor public Constraints(android.content.Context!, android.util.AttributeSet!);
+ ctor public Constraints(android.content.Context!, android.util.AttributeSet!, int);
+ method protected androidx.constraintlayout.widget.Constraints.LayoutParams! generateDefaultLayoutParams();
+ method public androidx.constraintlayout.widget.Constraints.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+ method public androidx.constraintlayout.widget.ConstraintSet! getConstraintSet();
+ field public static final String TAG = "Constraints";
+ }
+
+ public static class Constraints.LayoutParams extends androidx.constraintlayout.widget.ConstraintLayout.LayoutParams {
+ ctor public Constraints.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+ ctor public Constraints.LayoutParams(androidx.constraintlayout.widget.Constraints.LayoutParams!);
+ ctor public Constraints.LayoutParams(int, int);
+ field public float alpha;
+ field public boolean applyElevation;
+ field public float elevation;
+ field public float rotation;
+ field public float rotationX;
+ field public float rotationY;
+ field public float scaleX;
+ field public float scaleY;
+ field public float transformPivotX;
+ field public float transformPivotY;
+ field public float translationX;
+ field public float translationY;
+ field public float translationZ;
+ }
+
+ public abstract class ConstraintsChangedListener {
+ ctor public ConstraintsChangedListener();
+ method public void postLayoutChange(int, int);
+ method public void preLayoutChange(int, int);
+ }
+
+ public class Group extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public Group(android.content.Context!);
+ ctor public Group(android.content.Context!, android.util.AttributeSet!);
+ ctor public Group(android.content.Context!, android.util.AttributeSet!, int);
+ method public void onAttachedToWindow();
+ }
+
+ public class Guideline extends android.view.View {
+ ctor public Guideline(android.content.Context!);
+ ctor public Guideline(android.content.Context!, android.util.AttributeSet!);
+ ctor public Guideline(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public Guideline(android.content.Context!, android.util.AttributeSet!, int, int);
+ method public void setFilterRedundantCalls(boolean);
+ method public void setGuidelineBegin(int);
+ method public void setGuidelineEnd(int);
+ method public void setGuidelinePercent(float);
+ }
+
+ public class Placeholder extends android.view.View {
+ ctor public Placeholder(android.content.Context!);
+ ctor public Placeholder(android.content.Context!, android.util.AttributeSet!);
+ ctor public Placeholder(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public Placeholder(android.content.Context!, android.util.AttributeSet!, int, int);
+ method public android.view.View! getContent();
+ method public int getEmptyVisibility();
+ method public void onDraw(android.graphics.Canvas);
+ method public void setContentId(int);
+ method public void setEmptyVisibility(int);
+ method public void updatePostMeasure(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePreLayout(androidx.constraintlayout.widget.ConstraintLayout!);
+ }
+
+ public class ReactiveGuide extends android.view.View implements androidx.constraintlayout.widget.SharedValues.SharedValuesListener {
+ ctor public ReactiveGuide(android.content.Context!);
+ ctor public ReactiveGuide(android.content.Context!, android.util.AttributeSet!);
+ ctor public ReactiveGuide(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public ReactiveGuide(android.content.Context!, android.util.AttributeSet!, int, int);
+ method public int getApplyToConstraintSetId();
+ method public int getAttributeId();
+ method public boolean isAnimatingChange();
+ method public void onNewValue(int, int, int);
+ method public void setAnimateChange(boolean);
+ method public void setApplyToConstraintSetId(int);
+ method public void setAttributeId(int);
+ method public void setGuidelineBegin(int);
+ method public void setGuidelineEnd(int);
+ method public void setGuidelinePercent(float);
+ }
+
+ public class SharedValues {
+ ctor public SharedValues();
+ method public void addListener(int, androidx.constraintlayout.widget.SharedValues.SharedValuesListener!);
+ method public void clearListeners();
+ method public void fireNewValue(int, int);
+ method public int getValue(int);
+ method public void removeListener(androidx.constraintlayout.widget.SharedValues.SharedValuesListener!);
+ method public void removeListener(int, androidx.constraintlayout.widget.SharedValues.SharedValuesListener!);
+ field public static final int UNSET = -1; // 0xffffffff
+ }
+
+ public static interface SharedValues.SharedValuesListener {
+ method public void onNewValue(int, int, int);
+ }
+
+ public class StateSet {
+ ctor public StateSet(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public int convertToConstraintSet(int, int, float, float);
+ method public boolean needsToChange(int, float, float);
+ method public void setOnConstraintsChanged(androidx.constraintlayout.widget.ConstraintsChangedListener!);
+ method public int stateGetConstraintID(int, int, int);
+ method public int updateConstraints(int, int, float, float);
+ field public static final String TAG = "ConstraintLayoutStates";
+ }
+
+ public abstract class VirtualLayout extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public VirtualLayout(android.content.Context!);
+ ctor public VirtualLayout(android.content.Context!, android.util.AttributeSet!);
+ ctor public VirtualLayout(android.content.Context!, android.util.AttributeSet!, int);
+ method public void onAttachedToWindow();
+ method public void onMeasure(androidx.constraintlayout.core.widgets.VirtualLayout!, int, int);
+ }
+
+}
+
diff --git a/activity/activity-compose/api/res-1.10.0-beta01.txt b/constraintlayout/constraintlayout/api/res-2.2.0-beta01.txt
similarity index 100%
copy from activity/activity-compose/api/res-1.10.0-beta01.txt
copy to constraintlayout/constraintlayout/api/res-2.2.0-beta01.txt
diff --git a/constraintlayout/constraintlayout/api/restricted_2.2.0-beta01.txt b/constraintlayout/constraintlayout/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..2d187a8
--- /dev/null
+++ b/constraintlayout/constraintlayout/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,1714 @@
+// Signature format: 4.0
+package androidx.constraintlayout.helper.widget {
+
+ public class Carousel extends androidx.constraintlayout.motion.widget.MotionHelper {
+ ctor public Carousel(android.content.Context!);
+ ctor public Carousel(android.content.Context!, android.util.AttributeSet!);
+ ctor public Carousel(android.content.Context!, android.util.AttributeSet!, int);
+ method public int getCount();
+ method public int getCurrentIndex();
+ method public boolean isInfinite();
+ method public void jumpToIndex(int);
+ method public void refresh();
+ method public void setAdapter(androidx.constraintlayout.helper.widget.Carousel.Adapter!);
+ method public void setInfinite(boolean);
+ method public void transitionToIndex(int, int);
+ field public static final int TOUCH_UP_CARRY_ON = 2; // 0x2
+ field public static final int TOUCH_UP_IMMEDIATE_STOP = 1; // 0x1
+ }
+
+ public static interface Carousel.Adapter {
+ method public int count();
+ method public void onNewItem(int);
+ method public void populate(android.view.View!, int);
+ }
+
+ public class CircularFlow extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public CircularFlow(android.content.Context!);
+ ctor public CircularFlow(android.content.Context!, android.util.AttributeSet!);
+ ctor public CircularFlow(android.content.Context!, android.util.AttributeSet!, int);
+ method public void addViewToCircularFlow(android.view.View!, int, float);
+ method public float[]! getAngles();
+ method public int[]! getRadius();
+ method public boolean isUpdatable(android.view.View!);
+ method public void setDefaultAngle(float);
+ method public void setDefaultRadius(int);
+ method public void updateAngle(android.view.View!, float);
+ method public void updateRadius(android.view.View!, int);
+ method public void updateReference(android.view.View!, int, float);
+ }
+
+ public class Flow extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public Flow(android.content.Context!);
+ ctor public Flow(android.content.Context!, android.util.AttributeSet!);
+ ctor public Flow(android.content.Context!, android.util.AttributeSet!, int);
+ method public void setFirstHorizontalBias(float);
+ method public void setFirstHorizontalStyle(int);
+ method public void setFirstVerticalBias(float);
+ method public void setFirstVerticalStyle(int);
+ method public void setHorizontalAlign(int);
+ method public void setHorizontalBias(float);
+ method public void setHorizontalGap(int);
+ method public void setHorizontalStyle(int);
+ method public void setLastHorizontalBias(float);
+ method public void setLastHorizontalStyle(int);
+ method public void setLastVerticalBias(float);
+ method public void setLastVerticalStyle(int);
+ method public void setMaxElementsWrap(int);
+ method public void setOrientation(int);
+ method public void setPadding(int);
+ method public void setPaddingBottom(int);
+ method public void setPaddingLeft(int);
+ method public void setPaddingRight(int);
+ method public void setPaddingTop(int);
+ method public void setVerticalAlign(int);
+ method public void setVerticalBias(float);
+ method public void setVerticalGap(int);
+ method public void setVerticalStyle(int);
+ method public void setWrapMode(int);
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_START = 0; // 0x0
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_BASELINE = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 0; // 0x0
+ field public static final int WRAP_ALIGNED = 2; // 0x2
+ field public static final int WRAP_CHAIN = 1; // 0x1
+ field public static final int WRAP_NONE = 0; // 0x0
+ }
+
+ public class Grid extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public Grid(android.content.Context!);
+ ctor public Grid(android.content.Context!, android.util.AttributeSet!);
+ ctor public Grid(android.content.Context!, android.util.AttributeSet!, int);
+ method public String! getColumnWeights();
+ method public int getColumns();
+ method public float getHorizontalGaps();
+ method public int getOrientation();
+ method public String! getRowWeights();
+ method public int getRows();
+ method public String! getSkips();
+ method public String! getSpans();
+ method public float getVerticalGaps();
+ method public void setColumnWeights(String!);
+ method public void setColumns(int);
+ method public void setHorizontalGaps(float);
+ method public void setOrientation(int);
+ method public void setRowWeights(String!);
+ method public void setRows(int);
+ method public void setSkips(String!);
+ method public void setSpans(CharSequence!);
+ method public void setVerticalGaps(float);
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int VERTICAL = 1; // 0x1
+ }
+
+ public class Layer extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public Layer(android.content.Context!);
+ ctor public Layer(android.content.Context!, android.util.AttributeSet!);
+ ctor public Layer(android.content.Context!, android.util.AttributeSet!, int);
+ method protected void calcCenters();
+ field protected float mComputedCenterX;
+ field protected float mComputedCenterY;
+ field protected float mComputedMaxX;
+ field protected float mComputedMaxY;
+ field protected float mComputedMinX;
+ field protected float mComputedMinY;
+ }
+
+ public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
+ ctor public MotionEffect(android.content.Context!);
+ ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!, int);
+ field public static final int AUTO = -1; // 0xffffffff
+ field public static final int EAST = 2; // 0x2
+ field public static final int NORTH = 0; // 0x0
+ field public static final int SOUTH = 1; // 0x1
+ field public static final String TAG = "FadeMove";
+ field public static final int WEST = 3; // 0x3
+ }
+
+ public class MotionPlaceholder extends androidx.constraintlayout.widget.VirtualLayout {
+ ctor public MotionPlaceholder(android.content.Context!);
+ ctor public MotionPlaceholder(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionPlaceholder(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public MotionPlaceholder(android.content.Context!, android.util.AttributeSet!, int, int);
+ }
+
+}
+
+package androidx.constraintlayout.motion.utils {
+
+ public class CustomSupport {
+ ctor public CustomSupport();
+ method public static void setInterpolatedValue(androidx.constraintlayout.widget.ConstraintAttribute!, android.view.View!, float[]!);
+ }
+
+ public class StopLogic extends androidx.constraintlayout.motion.widget.MotionInterpolator {
+ ctor public StopLogic();
+ method public void config(float, float, float, float, float, float);
+ method public String! debug(String!, float);
+ method public float getInterpolation(float);
+ method public float getVelocity();
+ method public float getVelocity(float);
+ method public boolean isStopped();
+ method public void springConfig(float, float, float, float, float, float, float, int);
+ }
+
+ public abstract class ViewOscillator extends androidx.constraintlayout.core.motion.utils.KeyCycleOscillator {
+ ctor public ViewOscillator();
+ method public static androidx.constraintlayout.motion.utils.ViewOscillator! makeSpline(String!);
+ method public abstract void setProperty(android.view.View!, float);
+ }
+
+ public static class ViewOscillator.PathRotateSet extends androidx.constraintlayout.motion.utils.ViewOscillator {
+ ctor public ViewOscillator.PathRotateSet();
+ method public void setPathRotate(android.view.View!, float, double, double);
+ method public void setProperty(android.view.View!, float);
+ }
+
+ public abstract class ViewSpline extends androidx.constraintlayout.core.motion.utils.SplineSet {
+ ctor public ViewSpline();
+ method public static androidx.constraintlayout.motion.utils.ViewSpline! makeCustomSpline(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public static androidx.constraintlayout.motion.utils.ViewSpline! makeSpline(String!);
+ method public abstract void setProperty(android.view.View!, float);
+ }
+
+ public static class ViewSpline.CustomSet extends androidx.constraintlayout.motion.utils.ViewSpline {
+ ctor public ViewSpline.CustomSet(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public void setPoint(int, androidx.constraintlayout.widget.ConstraintAttribute!);
+ method public void setProperty(android.view.View!, float);
+ }
+
+ public static class ViewSpline.PathRotate extends androidx.constraintlayout.motion.utils.ViewSpline {
+ ctor public ViewSpline.PathRotate();
+ method public void setPathRotate(android.view.View!, float, double, double);
+ method public void setProperty(android.view.View!, float);
+ }
+
+ public class ViewState {
+ ctor public ViewState();
+ method public void getState(android.view.View!);
+ method public int height();
+ method public int width();
+ field public int bottom;
+ field public int left;
+ field public int right;
+ field public float rotation;
+ field public int top;
+ }
+
+ public abstract class ViewTimeCycle extends androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet {
+ ctor public ViewTimeCycle();
+ method public float get(float, long, android.view.View!, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ method public static androidx.constraintlayout.motion.utils.ViewTimeCycle! makeCustomSpline(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public static androidx.constraintlayout.motion.utils.ViewTimeCycle! makeSpline(String!, long);
+ method public abstract boolean setProperty(android.view.View!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ public static class ViewTimeCycle.CustomSet extends androidx.constraintlayout.motion.utils.ViewTimeCycle {
+ ctor public ViewTimeCycle.CustomSet(String!, android.util.SparseArray<androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public void setPoint(int, androidx.constraintlayout.widget.ConstraintAttribute!, float, int, float);
+ method public boolean setProperty(android.view.View!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+ public static class ViewTimeCycle.PathRotate extends androidx.constraintlayout.motion.utils.ViewTimeCycle {
+ ctor public ViewTimeCycle.PathRotate();
+ method public boolean setPathRotate(android.view.View!, androidx.constraintlayout.core.motion.utils.KeyCache!, float, long, double, double);
+ method public boolean setProperty(android.view.View!, float, long, androidx.constraintlayout.core.motion.utils.KeyCache!);
+ }
+
+}
+
+package androidx.constraintlayout.motion.widget {
+
+ public interface Animatable {
+ method public float getProgress();
+ method public void setProgress(float);
+ }
+
+ public interface CustomFloatAttributes {
+ method public float get(String!);
+ method public String![]! getListOfAttributes();
+ method public void set(String!, float);
+ }
+
+ public class Debug {
+ ctor public Debug();
+ method public static void dumpLayoutParams(android.view.ViewGroup!, String!);
+ method public static void dumpLayoutParams(android.view.ViewGroup.LayoutParams!, String!);
+ method public static void dumpPoc(Object!);
+ method public static String! getActionType(android.view.MotionEvent!);
+ method public static String! getCallFrom(int);
+ method public static String! getLoc();
+ method public static String! getLocation();
+ method public static String! getLocation2();
+ method public static String! getName(android.content.Context!, int);
+ method public static String! getName(android.content.Context!, int[]!);
+ method public static String! getName(android.view.View!);
+ method public static String! getState(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public static String! getState(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public static void logStack(String!, String!, int);
+ method public static void printStack(String!, int);
+ }
+
+ public class DesignTool {
+ ctor public DesignTool(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public int designAccess(int, String!, Object!, float[]!, int, float[]!, int);
+ method public void disableAutoTransition(boolean);
+ method public void dumpConstraintSet(String!);
+ method public int getAnimationKeyFrames(Object!, float[]!);
+ method public int getAnimationPath(Object!, float[]!, int);
+ method public void getAnimationRectangles(Object!, float[]!);
+ method public String! getEndState();
+ method public int getKeyFrameInfo(Object!, int, int[]!);
+ method public float getKeyFramePosition(Object!, int, float, float);
+ method public int getKeyFramePositions(Object!, int[]!, float[]!);
+ method public Object! getKeyframe(int, int, int);
+ method public Object! getKeyframe(Object!, int, int);
+ method public Object! getKeyframeAtLocation(Object!, float, float);
+ method public Boolean! getPositionKeyframe(Object!, Object!, float, float, String![]!, float[]!);
+ method public float getProgress();
+ method public String! getStartState();
+ method public String! getState();
+ method public long getTransitionTimeMs();
+ method public boolean isInTransition();
+ method public void setAttributes(int, String!, Object!, Object!);
+ method public void setKeyFrame(Object!, int, String!, Object!);
+ method public boolean setKeyFramePosition(Object!, int, int, float, float);
+ method public void setKeyframe(Object!, String!, Object!);
+ method public void setState(String!);
+ method public void setToolPosition(float);
+ method public void setTransition(String!, String!);
+ method public void setViewDebug(Object!, int);
+ }
+
+ public interface FloatLayout {
+ method public void layout(float, float, float, float);
+ }
+
+ public abstract class Key {
+ ctor public Key();
+ method public abstract void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public abstract androidx.constraintlayout.motion.widget.Key! clone();
+ method public androidx.constraintlayout.motion.widget.Key! copy(androidx.constraintlayout.motion.widget.Key!);
+ method public int getFramePosition();
+ method public void setFramePosition(int);
+ method public void setInterpolation(java.util.HashMap<java.lang.String!,java.lang.Integer!>!);
+ method public abstract void setValue(String!, Object!);
+ method public androidx.constraintlayout.motion.widget.Key! setViewId(int);
+ field public static final String ALPHA = "alpha";
+ field public static final String CURVEFIT = "curveFit";
+ field public static final String CUSTOM = "CUSTOM";
+ field public static final String ELEVATION = "elevation";
+ field public static final String MOTIONPROGRESS = "motionProgress";
+ field public static final String PIVOT_X = "transformPivotX";
+ field public static final String PIVOT_Y = "transformPivotY";
+ field public static final String PROGRESS = "progress";
+ field public static final String ROTATION = "rotation";
+ field public static final String ROTATION_X = "rotationX";
+ field public static final String ROTATION_Y = "rotationY";
+ field public static final String SCALE_X = "scaleX";
+ field public static final String SCALE_Y = "scaleY";
+ field public static final String TRANSITIONEASING = "transitionEasing";
+ field public static final String TRANSITION_PATH_ROTATE = "transitionPathRotate";
+ field public static final String TRANSLATION_X = "translationX";
+ field public static final String TRANSLATION_Y = "translationY";
+ field public static final String TRANSLATION_Z = "translationZ";
+ field public static int UNSET;
+ field public static final String VISIBILITY = "visibility";
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_PHASE = "wavePhase";
+ field public static final String WAVE_VARIES_BY = "waveVariesBy";
+ field protected int mType;
+ }
+
+ public class KeyAttributes extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyAttributes();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final int KEY_TYPE = 1; // 0x1
+ }
+
+ public class KeyCycle extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyCycle();
+ method public void addCycleValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewOscillator!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public float getValue(String!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final int KEY_TYPE = 4; // 0x4
+ field public static final int SHAPE_BOUNCE = 6; // 0x6
+ field public static final int SHAPE_COS_WAVE = 5; // 0x5
+ field public static final int SHAPE_REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SHAPE_SAW_WAVE = 3; // 0x3
+ field public static final int SHAPE_SIN_WAVE = 0; // 0x0
+ field public static final int SHAPE_SQUARE_WAVE = 1; // 0x1
+ field public static final int SHAPE_TRIANGLE_WAVE = 2; // 0x2
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_PHASE = "wavePhase";
+ field public static final String WAVE_SHAPE = "waveShape";
+ }
+
+ public class KeyFrames {
+ ctor public KeyFrames();
+ ctor public KeyFrames(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public void addAllFrames(androidx.constraintlayout.motion.widget.MotionController!);
+ method public void addFrames(androidx.constraintlayout.motion.widget.MotionController!);
+ method public void addKey(androidx.constraintlayout.motion.widget.Key!);
+ method public java.util.ArrayList<androidx.constraintlayout.motion.widget.Key!>! getKeyFramesForView(int);
+ method public java.util.Set<java.lang.Integer!>! getKeys();
+ field public static final int UNSET = -1; // 0xffffffff
+ }
+
+ public class KeyPosition extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyPosition();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public boolean intersects(int, int, android.graphics.RectF!, android.graphics.RectF!, float, float);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void positionAttributes(android.view.View!, android.graphics.RectF!, android.graphics.RectF!, float, float, String![]!, float[]!);
+ method public void setType(int);
+ method public void setValue(String!, Object!);
+ field public static final String DRAWPATH = "drawPath";
+ field public static final String PERCENT_HEIGHT = "percentHeight";
+ field public static final String PERCENT_WIDTH = "percentWidth";
+ field public static final String PERCENT_X = "percentX";
+ field public static final String PERCENT_Y = "percentY";
+ field public static final String SIZE_PERCENT = "sizePercent";
+ field public static final String TRANSITION_EASING = "transitionEasing";
+ field public static final int TYPE_AXIS = 3; // 0x3
+ field public static final int TYPE_CARTESIAN = 0; // 0x0
+ field public static final int TYPE_PATH = 1; // 0x1
+ field public static final int TYPE_SCREEN = 2; // 0x2
+ }
+
+ public class KeyTimeCycle extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyTimeCycle();
+ method public void addTimeValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewTimeCycle!>!);
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final int KEY_TYPE = 3; // 0x3
+ field public static final int SHAPE_BOUNCE = 6; // 0x6
+ field public static final int SHAPE_COS_WAVE = 5; // 0x5
+ field public static final int SHAPE_REVERSE_SAW_WAVE = 4; // 0x4
+ field public static final int SHAPE_SAW_WAVE = 3; // 0x3
+ field public static final int SHAPE_SIN_WAVE = 0; // 0x0
+ field public static final int SHAPE_SQUARE_WAVE = 1; // 0x1
+ field public static final int SHAPE_TRIANGLE_WAVE = 2; // 0x2
+ field public static final String WAVE_OFFSET = "waveOffset";
+ field public static final String WAVE_PERIOD = "wavePeriod";
+ field public static final String WAVE_SHAPE = "waveShape";
+ }
+
+ public class KeyTrigger extends androidx.constraintlayout.motion.widget.Key {
+ ctor public KeyTrigger();
+ method public void addValues(java.util.HashMap<java.lang.String!,androidx.constraintlayout.motion.utils.ViewSpline!>!);
+ method public androidx.constraintlayout.motion.widget.Key! clone();
+ method public void conditionallyFire(float, android.view.View!);
+ method public void getAttributeNames(java.util.HashSet<java.lang.String!>!);
+ method public void load(android.content.Context!, android.util.AttributeSet!);
+ method public void setValue(String!, Object!);
+ field public static final String CROSS = "CROSS";
+ field public static final int KEY_TYPE = 5; // 0x5
+ field public static final String NEGATIVE_CROSS = "negativeCross";
+ field public static final String POSITIVE_CROSS = "positiveCross";
+ field public static final String POST_LAYOUT = "postLayout";
+ field public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
+ field public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
+ field public static final String TRIGGER_ID = "triggerID";
+ field public static final String TRIGGER_RECEIVER = "triggerReceiver";
+ field public static final String TRIGGER_SLACK = "triggerSlack";
+ field public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
+ field public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
+ field public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
+ }
+
+ public class MotionController {
+ method public void addKey(androidx.constraintlayout.motion.widget.Key!);
+ method public int getAnimateRelativeTo();
+ method public void getCenter(double, float[]!, float[]!);
+ method public float getCenterX();
+ method public float getCenterY();
+ method public int getDrawPath();
+ method public float getFinalHeight();
+ method public float getFinalWidth();
+ method public float getFinalX();
+ method public float getFinalY();
+ method public int getKeyFrameInfo(int, int[]!);
+ method public int getKeyFramePositions(int[]!, float[]!);
+ method public float getStartHeight();
+ method public float getStartWidth();
+ method public float getStartX();
+ method public float getStartY();
+ method public int getTransformPivotTarget();
+ method public android.view.View! getView();
+ method public void remeasure();
+ method public void setDrawPath(int);
+ method public void setPathMotionArc(int);
+ method public void setStartState(androidx.constraintlayout.motion.utils.ViewState!, android.view.View!, int, int, int);
+ method public void setTransformPivotTarget(int);
+ method public void setView(android.view.View!);
+ method public void setup(int, int, float, long);
+ method public void setupRelative(androidx.constraintlayout.motion.widget.MotionController!);
+ field public static final int DRAW_PATH_AS_CONFIGURED = 4; // 0x4
+ field public static final int DRAW_PATH_BASIC = 1; // 0x1
+ field public static final int DRAW_PATH_CARTESIAN = 3; // 0x3
+ field public static final int DRAW_PATH_NONE = 0; // 0x0
+ field public static final int DRAW_PATH_RECTANGLE = 5; // 0x5
+ field public static final int DRAW_PATH_RELATIVE = 2; // 0x2
+ field public static final int DRAW_PATH_SCREEN = 6; // 0x6
+ field public static final int HORIZONTAL_PATH_X = 2; // 0x2
+ field public static final int HORIZONTAL_PATH_Y = 3; // 0x3
+ field public static final int PATH_PERCENT = 0; // 0x0
+ field public static final int PATH_PERPENDICULAR = 1; // 0x1
+ field public static final int ROTATION_LEFT = 2; // 0x2
+ field public static final int ROTATION_RIGHT = 1; // 0x1
+ field public static final int VERTICAL_PATH_X = 4; // 0x4
+ field public static final int VERTICAL_PATH_Y = 5; // 0x5
+ }
+
+ public class MotionHelper extends androidx.constraintlayout.widget.ConstraintHelper implements androidx.constraintlayout.motion.widget.MotionHelperInterface {
+ ctor public MotionHelper(android.content.Context!);
+ ctor public MotionHelper(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionHelper(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getProgress();
+ method public boolean isDecorator();
+ method public boolean isUseOnHide();
+ method public boolean isUsedOnShow();
+ method public void onFinishedMotionScene(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void onPostDraw(android.graphics.Canvas!);
+ method public void onPreDraw(android.graphics.Canvas!);
+ method public void onPreSetup(androidx.constraintlayout.motion.widget.MotionLayout!, java.util.HashMap<android.view.View!,androidx.constraintlayout.motion.widget.MotionController!>!);
+ method public void onTransitionChange(androidx.constraintlayout.motion.widget.MotionLayout!, int, int, float);
+ method public void onTransitionCompleted(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void onTransitionStarted(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public void onTransitionTrigger(androidx.constraintlayout.motion.widget.MotionLayout!, int, boolean, float);
+ method public void setProgress(android.view.View!, float);
+ method public void setProgress(float);
+ field protected android.view.View![]! views;
+ }
+
+ public interface MotionHelperInterface extends androidx.constraintlayout.motion.widget.Animatable androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener {
+ method public boolean isDecorator();
+ method public boolean isUseOnHide();
+ method public boolean isUsedOnShow();
+ method public void onFinishedMotionScene(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void onPostDraw(android.graphics.Canvas!);
+ method public void onPreDraw(android.graphics.Canvas!);
+ method public void onPreSetup(androidx.constraintlayout.motion.widget.MotionLayout!, java.util.HashMap<android.view.View!,androidx.constraintlayout.motion.widget.MotionController!>!);
+ }
+
+ public abstract class MotionInterpolator implements android.view.animation.Interpolator {
+ ctor public MotionInterpolator();
+ method public abstract float getVelocity();
+ }
+
+ public class MotionLayout extends androidx.constraintlayout.widget.ConstraintLayout implements androidx.core.view.NestedScrollingParent3 {
+ ctor public MotionLayout(android.content.Context);
+ ctor public MotionLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public MotionLayout(android.content.Context, android.util.AttributeSet?, int);
+ method public void addTransitionListener(androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener!);
+ method public boolean applyViewTransition(int, androidx.constraintlayout.motion.widget.MotionController!);
+ method public androidx.constraintlayout.widget.ConstraintSet! cloneConstraintSet(int);
+ method public void enableTransition(int, boolean);
+ method public void enableViewTransition(int, boolean);
+ method protected void fireTransitionCompleted();
+ method public void fireTrigger(int, boolean, float);
+ method public androidx.constraintlayout.widget.ConstraintSet! getConstraintSet(int);
+ method @IdRes public int[]! getConstraintSetIds();
+ method public int getCurrentState();
+ method public java.util.ArrayList<androidx.constraintlayout.motion.widget.MotionScene.Transition!>! getDefinedTransitions();
+ method public androidx.constraintlayout.motion.widget.DesignTool! getDesignTool();
+ method public int getEndState();
+ method public int[]! getMatchingConstraintSetIds(java.lang.String!...!);
+ method protected long getNanoTime();
+ method public float getProgress();
+ method public androidx.constraintlayout.motion.widget.MotionScene! getScene();
+ method public int getStartState();
+ method public float getTargetPosition();
+ method public androidx.constraintlayout.motion.widget.MotionScene.Transition! getTransition(int);
+ method public android.os.Bundle! getTransitionState();
+ method public long getTransitionTimeMs();
+ method public float getVelocity();
+ method public void getViewVelocity(android.view.View!, float, float, float[]!, int);
+ method public boolean isDelayedApplicationOfInitialState();
+ method public boolean isInRotation();
+ method public boolean isInteractionEnabled();
+ method public boolean isViewTransitionEnabled(int);
+ method public void jumpToState(int);
+ method protected androidx.constraintlayout.motion.widget.MotionLayout.MotionTracker! obtainVelocityTracker();
+ method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int, int[]!);
+ method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+ method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+ method public void onStopNestedScroll(android.view.View, int);
+ method @Deprecated public void rebuildMotion();
+ method public void rebuildScene();
+ method public boolean removeTransitionListener(androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener!);
+ method public void rotateTo(int, int);
+ method public void scheduleTransitionTo(int);
+ method public void setDebugMode(int);
+ method public void setDelayedApplicationOfInitialState(boolean);
+ method public void setInteractionEnabled(boolean);
+ method public void setInterpolatedProgress(float);
+ method public void setOnHide(float);
+ method public void setOnShow(float);
+ method public void setProgress(float);
+ method public void setProgress(float, float);
+ method public void setScene(androidx.constraintlayout.motion.widget.MotionScene!);
+ method protected void setTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public void setTransition(int);
+ method public void setTransition(int, int);
+ method public void setTransitionDuration(int);
+ method public void setTransitionListener(androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener!);
+ method public void setTransitionState(android.os.Bundle!);
+ method public void touchAnimateTo(int, float, float);
+ method public void touchSpringTo(float, float);
+ method public void transitionToEnd();
+ method public void transitionToEnd(Runnable!);
+ method public void transitionToStart();
+ method public void transitionToStart(Runnable!);
+ method public void transitionToState(int);
+ method public void transitionToState(int, int);
+ method public void transitionToState(int, int, int);
+ method public void transitionToState(int, int, int, int);
+ method public void updateState();
+ method public void updateState(int, androidx.constraintlayout.widget.ConstraintSet!);
+ method public void updateStateAnimate(int, androidx.constraintlayout.widget.ConstraintSet!, int);
+ method public void viewTransition(int, android.view.View!...!);
+ field public static final int DEBUG_SHOW_NONE = 0; // 0x0
+ field public static final int DEBUG_SHOW_PATH = 2; // 0x2
+ field public static final int DEBUG_SHOW_PROGRESS = 1; // 0x1
+ field public static boolean IS_IN_EDIT_MODE;
+ field public static final int TOUCH_UP_COMPLETE = 0; // 0x0
+ field public static final int TOUCH_UP_COMPLETE_TO_END = 2; // 0x2
+ field public static final int TOUCH_UP_COMPLETE_TO_START = 1; // 0x1
+ field public static final int TOUCH_UP_DECELERATE = 4; // 0x4
+ field public static final int TOUCH_UP_DECELERATE_AND_COMPLETE = 5; // 0x5
+ field public static final int TOUCH_UP_NEVER_TO_END = 7; // 0x7
+ field public static final int TOUCH_UP_NEVER_TO_START = 6; // 0x6
+ field public static final int TOUCH_UP_STOP = 3; // 0x3
+ field public static final int VELOCITY_LAYOUT = 1; // 0x1
+ field public static final int VELOCITY_POST_LAYOUT = 0; // 0x0
+ field public static final int VELOCITY_STATIC_LAYOUT = 3; // 0x3
+ field public static final int VELOCITY_STATIC_POST_LAYOUT = 2; // 0x2
+ field protected boolean mMeasureDuringTransition;
+ }
+
+ protected static interface MotionLayout.MotionTracker {
+ method public void addMovement(android.view.MotionEvent!);
+ method public void clear();
+ method public void computeCurrentVelocity(int);
+ method public void computeCurrentVelocity(int, float);
+ method public float getXVelocity();
+ method public float getXVelocity(int);
+ method public float getYVelocity();
+ method public float getYVelocity(int);
+ method public void recycle();
+ }
+
+ public static interface MotionLayout.TransitionListener {
+ method public void onTransitionChange(androidx.constraintlayout.motion.widget.MotionLayout!, int, int, float);
+ method public void onTransitionCompleted(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void onTransitionStarted(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public void onTransitionTrigger(androidx.constraintlayout.motion.widget.MotionLayout!, int, boolean, float);
+ }
+
+ public class MotionScene {
+ ctor public MotionScene(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void addOnClickListeners(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void addTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public boolean applyViewTransition(int, androidx.constraintlayout.motion.widget.MotionController!);
+ method public androidx.constraintlayout.motion.widget.MotionScene.Transition! bestTransitionFor(int, float, float, android.view.MotionEvent!);
+ method public void disableAutoTransition(boolean);
+ method public void enableViewTransition(int, boolean);
+ method public int gatPathMotionArc();
+ method public androidx.constraintlayout.widget.ConstraintSet! getConstraintSet(android.content.Context!, String!);
+ method public int[]! getConstraintSetIds();
+ method public java.util.ArrayList<androidx.constraintlayout.motion.widget.MotionScene.Transition!>! getDefinedTransitions();
+ method public int getDuration();
+ method public android.view.animation.Interpolator! getInterpolator();
+ method public void getKeyFrames(androidx.constraintlayout.motion.widget.MotionController!);
+ method public int[]! getMatchingStateLabels(java.lang.String!...!);
+ method public float getPathPercent(android.view.View!, int);
+ method public float getStaggered();
+ method public androidx.constraintlayout.motion.widget.MotionScene.Transition! getTransitionById(int);
+ method public java.util.List<androidx.constraintlayout.motion.widget.MotionScene.Transition!>! getTransitionsWithState(int);
+ method public boolean isViewTransitionEnabled(int);
+ method public int lookUpConstraintId(String!);
+ method public String! lookUpConstraintName(int);
+ method protected void onLayout(boolean, int, int, int, int);
+ method public void removeTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public void setConstraintSet(int, androidx.constraintlayout.widget.ConstraintSet!);
+ method public void setDuration(int);
+ method public void setKeyframe(android.view.View!, int, String!, Object!);
+ method public void setRtl(boolean);
+ method public void setTransition(androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public static String! stripID(String!);
+ method public boolean validateLayout(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void viewTransition(int, android.view.View!...!);
+ field public static final int LAYOUT_CALL_MEASURE = 2; // 0x2
+ field public static final int LAYOUT_HONOR_REQUEST = 1; // 0x1
+ field public static final int LAYOUT_IGNORE_REQUEST = 0; // 0x0
+ field public static final int UNSET = -1; // 0xffffffff
+ }
+
+ public static class MotionScene.Transition {
+ ctor public MotionScene.Transition(int, androidx.constraintlayout.motion.widget.MotionScene!, int, int);
+ method public void addKeyFrame(androidx.constraintlayout.motion.widget.KeyFrames!);
+ method public void addOnClick(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public void addOnClick(int, int);
+ method public String! debugString(android.content.Context!);
+ method public int getAutoTransition();
+ method public int getDuration();
+ method public int getEndConstraintSetId();
+ method public int getId();
+ method public java.util.List<androidx.constraintlayout.motion.widget.KeyFrames!>! getKeyFrameList();
+ method public int getLayoutDuringTransition();
+ method public java.util.List<androidx.constraintlayout.motion.widget.MotionScene.Transition.TransitionOnClick!>! getOnClickList();
+ method public int getPathMotionArc();
+ method public float getStagger();
+ method public int getStartConstraintSetId();
+ method public androidx.constraintlayout.motion.widget.TouchResponse! getTouchResponse();
+ method public boolean isEnabled();
+ method public boolean isTransitionFlag(int);
+ method public void removeOnClick(int);
+ method public void setAutoTransition(int);
+ method public void setDuration(int);
+ method public void setEnabled(boolean);
+ method public void setInterpolatorInfo(int, String!, int);
+ method public void setLayoutDuringTransition(int);
+ method public void setOnSwipe(androidx.constraintlayout.motion.widget.OnSwipe!);
+ method public void setOnTouchUp(int);
+ method public void setPathMotionArc(int);
+ method public void setStagger(float);
+ method public void setTransitionFlag(int);
+ field public static final int AUTO_ANIMATE_TO_END = 4; // 0x4
+ field public static final int AUTO_ANIMATE_TO_START = 3; // 0x3
+ field public static final int AUTO_JUMP_TO_END = 2; // 0x2
+ field public static final int AUTO_JUMP_TO_START = 1; // 0x1
+ field public static final int AUTO_NONE = 0; // 0x0
+ field public static final int INTERPOLATE_ANTICIPATE = 6; // 0x6
+ field public static final int INTERPOLATE_BOUNCE = 4; // 0x4
+ field public static final int INTERPOLATE_EASE_IN = 1; // 0x1
+ field public static final int INTERPOLATE_EASE_IN_OUT = 0; // 0x0
+ field public static final int INTERPOLATE_EASE_OUT = 2; // 0x2
+ field public static final int INTERPOLATE_LINEAR = 3; // 0x3
+ field public static final int INTERPOLATE_OVERSHOOT = 5; // 0x5
+ field public static final int INTERPOLATE_REFERENCE_ID = -2; // 0xfffffffe
+ field public static final int INTERPOLATE_SPLINE_STRING = -1; // 0xffffffff
+ }
+
+ public static class MotionScene.Transition.TransitionOnClick implements android.view.View.OnClickListener {
+ ctor public MotionScene.Transition.TransitionOnClick(android.content.Context!, androidx.constraintlayout.motion.widget.MotionScene.Transition!, org.xmlpull.v1.XmlPullParser!);
+ ctor public MotionScene.Transition.TransitionOnClick(androidx.constraintlayout.motion.widget.MotionScene.Transition!, int, int);
+ method public void addOnClickListeners(androidx.constraintlayout.motion.widget.MotionLayout!, int, androidx.constraintlayout.motion.widget.MotionScene.Transition!);
+ method public void onClick(android.view.View!);
+ method public void removeOnClickListeners(androidx.constraintlayout.motion.widget.MotionLayout!);
+ field public static final int ANIM_TOGGLE = 17; // 0x11
+ field public static final int ANIM_TO_END = 1; // 0x1
+ field public static final int ANIM_TO_START = 16; // 0x10
+ field public static final int JUMP_TO_END = 256; // 0x100
+ field public static final int JUMP_TO_START = 4096; // 0x1000
+ }
+
+ public class OnSwipe {
+ ctor public OnSwipe();
+ method public int getAutoCompleteMode();
+ method public int getDragDirection();
+ method public float getDragScale();
+ method public float getDragThreshold();
+ method public int getLimitBoundsTo();
+ method public float getMaxAcceleration();
+ method public float getMaxVelocity();
+ method public boolean getMoveWhenScrollAtTop();
+ method public int getNestedScrollFlags();
+ method public int getOnTouchUp();
+ method public int getRotationCenterId();
+ method public int getSpringBoundary();
+ method public float getSpringDamping();
+ method public float getSpringMass();
+ method public float getSpringStiffness();
+ method public float getSpringStopThreshold();
+ method public int getTouchAnchorId();
+ method public int getTouchAnchorSide();
+ method public int getTouchRegionId();
+ method public void setAutoCompleteMode(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setDragDirection(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setDragScale(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setDragThreshold(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setLimitBoundsTo(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setMaxAcceleration(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setMaxVelocity(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setMoveWhenScrollAtTop(boolean);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setNestedScrollFlags(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setOnTouchUp(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setRotateCenter(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringBoundary(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringDamping(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringMass(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringStiffness(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setSpringStopThreshold(float);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setTouchAnchorId(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setTouchAnchorSide(int);
+ method public androidx.constraintlayout.motion.widget.OnSwipe! setTouchRegionId(int);
+ field public static final int COMPLETE_MODE_CONTINUOUS_VELOCITY = 0; // 0x0
+ field public static final int COMPLETE_MODE_SPRING = 1; // 0x1
+ field public static final int DRAG_ANTICLOCKWISE = 7; // 0x7
+ field public static final int DRAG_CLOCKWISE = 6; // 0x6
+ field public static final int DRAG_DOWN = 1; // 0x1
+ field public static final int DRAG_END = 5; // 0x5
+ field public static final int DRAG_LEFT = 2; // 0x2
+ field public static final int DRAG_RIGHT = 3; // 0x3
+ field public static final int DRAG_START = 4; // 0x4
+ field public static final int DRAG_UP = 0; // 0x0
+ field public static final int FLAG_DISABLE_POST_SCROLL = 1; // 0x1
+ field public static final int FLAG_DISABLE_SCROLL = 2; // 0x2
+ field public static final int ON_UP_AUTOCOMPLETE = 0; // 0x0
+ field public static final int ON_UP_AUTOCOMPLETE_TO_END = 2; // 0x2
+ field public static final int ON_UP_AUTOCOMPLETE_TO_START = 1; // 0x1
+ field public static final int ON_UP_DECELERATE = 4; // 0x4
+ field public static final int ON_UP_DECELERATE_AND_COMPLETE = 5; // 0x5
+ field public static final int ON_UP_NEVER_TO_END = 7; // 0x7
+ field public static final int ON_UP_NEVER_TO_START = 6; // 0x6
+ field public static final int ON_UP_STOP = 3; // 0x3
+ field public static final int SIDE_BOTTOM = 3; // 0x3
+ field public static final int SIDE_END = 6; // 0x6
+ field public static final int SIDE_LEFT = 1; // 0x1
+ field public static final int SIDE_MIDDLE = 4; // 0x4
+ field public static final int SIDE_RIGHT = 2; // 0x2
+ field public static final int SIDE_START = 5; // 0x5
+ field public static final int SIDE_TOP = 0; // 0x0
+ field public static final int SPRING_BOUNDARY_BOUNCEBOTH = 3; // 0x3
+ field public static final int SPRING_BOUNDARY_BOUNCEEND = 2; // 0x2
+ field public static final int SPRING_BOUNDARY_BOUNCESTART = 1; // 0x1
+ field public static final int SPRING_BOUNDARY_OVERSHOOT = 0; // 0x0
+ }
+
+ public abstract class TransitionAdapter implements androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener {
+ ctor public TransitionAdapter();
+ method public void onTransitionChange(androidx.constraintlayout.motion.widget.MotionLayout!, int, int, float);
+ method public void onTransitionCompleted(androidx.constraintlayout.motion.widget.MotionLayout!, int);
+ method public void onTransitionStarted(androidx.constraintlayout.motion.widget.MotionLayout!, int, int);
+ method public void onTransitionTrigger(androidx.constraintlayout.motion.widget.MotionLayout!, int, boolean, float);
+ }
+
+ public class TransitionBuilder {
+ ctor public TransitionBuilder();
+ method public static androidx.constraintlayout.motion.widget.MotionScene.Transition! buildTransition(androidx.constraintlayout.motion.widget.MotionScene!, int, int, androidx.constraintlayout.widget.ConstraintSet!, int, androidx.constraintlayout.widget.ConstraintSet!);
+ method public static void validate(androidx.constraintlayout.motion.widget.MotionLayout!);
+ }
+
+ public class ViewTransition {
+ method public int getSharedValue();
+ method public int getSharedValueCurrent();
+ method public int getSharedValueID();
+ method public int getStateTransition();
+ method public void setSharedValue(int);
+ method public void setSharedValueCurrent(int);
+ method public void setSharedValueID(int);
+ method public void setStateTransition(int);
+ field public static final String CONSTRAINT_OVERRIDE = "ConstraintOverride";
+ field public static final String CUSTOM_ATTRIBUTE = "CustomAttribute";
+ field public static final String CUSTOM_METHOD = "CustomMethod";
+ field public static final String KEY_FRAME_SET_TAG = "KeyFrameSet";
+ field public static final int ONSTATE_ACTION_DOWN = 1; // 0x1
+ field public static final int ONSTATE_ACTION_DOWN_UP = 3; // 0x3
+ field public static final int ONSTATE_ACTION_UP = 2; // 0x2
+ field public static final int ONSTATE_SHARED_VALUE_SET = 4; // 0x4
+ field public static final int ONSTATE_SHARED_VALUE_UNSET = 5; // 0x5
+ field public static final String VIEW_TRANSITION_TAG = "ViewTransition";
+ }
+
+ public class ViewTransitionController {
+ ctor public ViewTransitionController(androidx.constraintlayout.motion.widget.MotionLayout!);
+ method public void add(androidx.constraintlayout.motion.widget.ViewTransition!);
+ }
+
+}
+
+package androidx.constraintlayout.utils.widget {
+
+ public class ImageFilterButton extends androidx.appcompat.widget.AppCompatImageButton {
+ ctor public ImageFilterButton(android.content.Context!);
+ ctor public ImageFilterButton(android.content.Context!, android.util.AttributeSet!);
+ ctor public ImageFilterButton(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getContrast();
+ method public float getCrossfade();
+ method public float getImagePanX();
+ method public float getImagePanY();
+ method public float getImageRotate();
+ method public float getImageZoom();
+ method public float getRound();
+ method public float getRoundPercent();
+ method public float getSaturation();
+ method public float getWarmth();
+ method public void setAltImageResource(int);
+ method public void setBrightness(float);
+ method public void setContrast(float);
+ method public void setCrossfade(float);
+ method public void setImagePanX(float);
+ method public void setImagePanY(float);
+ method public void setImageRotate(float);
+ method public void setImageZoom(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ method public void setSaturation(float);
+ method public void setWarmth(float);
+ }
+
+ public class ImageFilterView extends androidx.appcompat.widget.AppCompatImageView {
+ ctor public ImageFilterView(android.content.Context!);
+ ctor public ImageFilterView(android.content.Context!, android.util.AttributeSet!);
+ ctor public ImageFilterView(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getBrightness();
+ method public float getContrast();
+ method public float getCrossfade();
+ method public float getImagePanX();
+ method public float getImagePanY();
+ method public float getImageRotate();
+ method public float getImageZoom();
+ method public float getRound();
+ method public float getRoundPercent();
+ method public float getSaturation();
+ method public float getWarmth();
+ method public void setAltImageDrawable(android.graphics.drawable.Drawable!);
+ method public void setAltImageResource(int);
+ method public void setBrightness(float);
+ method public void setContrast(float);
+ method public void setCrossfade(float);
+ method public void setImagePanX(float);
+ method public void setImagePanY(float);
+ method public void setImageRotate(float);
+ method public void setImageZoom(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ method public void setSaturation(float);
+ method public void setWarmth(float);
+ }
+
+ public class MockView extends android.view.View {
+ ctor public MockView(android.content.Context!);
+ ctor public MockView(android.content.Context!, android.util.AttributeSet!);
+ ctor public MockView(android.content.Context!, android.util.AttributeSet!, int);
+ method public void onDraw(android.graphics.Canvas);
+ field protected String! mText;
+ }
+
+ public class MotionButton extends androidx.appcompat.widget.AppCompatButton {
+ ctor public MotionButton(android.content.Context!);
+ ctor public MotionButton(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionButton(android.content.Context!, android.util.AttributeSet!, int);
+ method public float getRound();
+ method public float getRoundPercent();
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ }
+
+ public class MotionLabel extends android.view.View implements androidx.constraintlayout.motion.widget.FloatLayout {
+ ctor public MotionLabel(android.content.Context!);
+ ctor public MotionLabel(android.content.Context!, android.util.AttributeSet?);
+ ctor public MotionLabel(android.content.Context!, android.util.AttributeSet?, int);
+ method public float getRound();
+ method public float getRoundPercent();
+ method public float getScaleFromTextSize();
+ method public float getTextBackgroundPanX();
+ method public float getTextBackgroundPanY();
+ method public float getTextBackgroundRotate();
+ method public float getTextBackgroundZoom();
+ method public int getTextOutlineColor();
+ method public float getTextPanX();
+ method public float getTextPanY();
+ method public float getTextureHeight();
+ method public float getTextureWidth();
+ method public android.graphics.Typeface! getTypeface();
+ method public void layout(float, float, float, float);
+ method public void setGravity(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRound(float);
+ method @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) public void setRoundPercent(float);
+ method public void setScaleFromTextSize(float);
+ method public void setText(CharSequence!);
+ method public void setTextBackgroundPanX(float);
+ method public void setTextBackgroundPanY(float);
+ method public void setTextBackgroundRotate(float);
+ method public void setTextBackgroundZoom(float);
+ method public void setTextFillColor(int);
+ method public void setTextOutlineColor(int);
+ method public void setTextOutlineThickness(float);
+ method public void setTextPanX(float);
+ method public void setTextPanY(float);
+ method public void setTextSize(float);
+ method public void setTextureHeight(float);
+ method public void setTextureWidth(float);
+ method public void setTypeface(android.graphics.Typeface!);
+ }
+
+ public class MotionTelltales extends androidx.constraintlayout.utils.widget.MockView {
+ ctor public MotionTelltales(android.content.Context!);
+ ctor public MotionTelltales(android.content.Context!, android.util.AttributeSet!);
+ ctor public MotionTelltales(android.content.Context!, android.util.AttributeSet!, int);
+ method public void setText(CharSequence!);
+ }
+
+}
+
+package androidx.constraintlayout.widget {
+
+ public class Barrier extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public Barrier(android.content.Context!);
+ ctor public Barrier(android.content.Context!, android.util.AttributeSet!);
+ ctor public Barrier(android.content.Context!, android.util.AttributeSet!, int);
+ method @Deprecated public boolean allowsGoneWidget();
+ method public boolean getAllowsGoneWidget();
+ method public int getMargin();
+ method public int getType();
+ method public void setAllowsGoneWidget(boolean);
+ method public void setDpMargin(int);
+ method public void setMargin(int);
+ method public void setType(int);
+ field public static final int BOTTOM = 3; // 0x3
+ field public static final int END = 6; // 0x6
+ field public static final int LEFT = 0; // 0x0
+ field public static final int RIGHT = 1; // 0x1
+ field public static final int START = 5; // 0x5
+ field public static final int TOP = 2; // 0x2
+ }
+
+ public class ConstraintAttribute {
+ ctor public ConstraintAttribute(androidx.constraintlayout.widget.ConstraintAttribute!, Object!);
+ ctor public ConstraintAttribute(String!, androidx.constraintlayout.widget.ConstraintAttribute.AttributeType!);
+ ctor public ConstraintAttribute(String!, androidx.constraintlayout.widget.ConstraintAttribute.AttributeType!, Object!, boolean);
+ method public void applyCustom(android.view.View!);
+ method public boolean diff(androidx.constraintlayout.widget.ConstraintAttribute!);
+ method public static java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>! extractAttributes(java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>!, android.view.View!);
+ method public int getColorValue();
+ method public float getFloatValue();
+ method public int getIntegerValue();
+ method public String! getName();
+ method public String! getStringValue();
+ method public androidx.constraintlayout.widget.ConstraintAttribute.AttributeType! getType();
+ method public float getValueToInterpolate();
+ method public void getValuesToInterpolate(float[]!);
+ method public boolean isBooleanValue();
+ method public boolean isContinuous();
+ method public boolean isMethod();
+ method public int numberOfInterpolatedValues();
+ method public static void parse(android.content.Context!, org.xmlpull.v1.XmlPullParser!, java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public static void setAttributes(android.view.View!, java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>!);
+ method public void setColorValue(int);
+ method public void setFloatValue(float);
+ method public void setIntValue(int);
+ method public void setStringValue(String!);
+ method public void setValue(float[]!);
+ method public void setValue(Object!);
+ }
+
+ public enum ConstraintAttribute.AttributeType {
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType BOOLEAN_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType COLOR_DRAWABLE_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType COLOR_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType DIMENSION_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType FLOAT_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType INT_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType REFERENCE_TYPE;
+ enum_constant public static final androidx.constraintlayout.widget.ConstraintAttribute.AttributeType STRING_TYPE;
+ }
+
+ public abstract class ConstraintHelper extends android.view.View {
+ ctor public ConstraintHelper(android.content.Context!);
+ ctor public ConstraintHelper(android.content.Context!, android.util.AttributeSet!);
+ ctor public ConstraintHelper(android.content.Context!, android.util.AttributeSet!, int);
+ method public void addView(android.view.View!);
+ method public void applyHelperParams();
+ method protected void applyLayoutFeatures();
+ method protected void applyLayoutFeatures(androidx.constraintlayout.widget.ConstraintLayout!);
+ method protected void applyLayoutFeaturesInConstraintSet(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public boolean containsId(int);
+ method public int[]! getReferencedIds();
+ method protected android.view.View![]! getViews(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public int indexFromId(int);
+ method protected void init(android.util.AttributeSet!);
+ method public static boolean isChildOfHelper(android.view.View!);
+ method public void loadParameters(androidx.constraintlayout.widget.ConstraintSet.Constraint!, androidx.constraintlayout.core.widgets.HelperWidget!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void onDraw(android.graphics.Canvas);
+ method public int removeView(android.view.View!);
+ method public void resolveRtl(androidx.constraintlayout.core.widgets.ConstraintWidget!, boolean);
+ method protected void setIds(String!);
+ method protected void setReferenceTags(String!);
+ method public void setReferencedIds(int[]!);
+ method public void updatePostConstraints(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePostLayout(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePostMeasure(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePreDraw(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePreLayout(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, androidx.constraintlayout.core.widgets.Helper!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void updatePreLayout(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void validateParams();
+ field protected static final String CHILD_TAG = "CONSTRAINT_LAYOUT_HELPER_CHILD";
+ field protected int mCount;
+ field protected androidx.constraintlayout.core.widgets.Helper! mHelperWidget;
+ field protected int[]! mIds;
+ field protected java.util.HashMap<java.lang.Integer!,java.lang.String!>! mMap;
+ field protected String! mReferenceIds;
+ field protected String! mReferenceTags;
+ field protected boolean mUseViewMeasure;
+ field protected android.content.Context! myContext;
+ }
+
+ public class ConstraintLayout extends android.view.ViewGroup {
+ ctor public ConstraintLayout(android.content.Context);
+ ctor public ConstraintLayout(android.content.Context, android.util.AttributeSet?);
+ ctor public ConstraintLayout(android.content.Context, android.util.AttributeSet?, int);
+ ctor public ConstraintLayout(android.content.Context, android.util.AttributeSet?, int, int);
+ method public void addValueModifier(androidx.constraintlayout.widget.ConstraintLayout.ValueModifier!);
+ method protected void applyConstraintsFromLayoutParams(boolean, android.view.View!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method protected boolean dynamicUpdateConstraints(int, int);
+ method public void fillMetrics(androidx.constraintlayout.core.Metrics!);
+ method protected androidx.constraintlayout.widget.ConstraintLayout.LayoutParams! generateDefaultLayoutParams();
+ method public androidx.constraintlayout.widget.ConstraintLayout.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+ method public Object! getDesignInformation(int, Object!);
+ method public int getMaxHeight();
+ method public int getMaxWidth();
+ method public int getMinHeight();
+ method public int getMinWidth();
+ method public int getOptimizationLevel();
+ method public String! getSceneString();
+ method public static androidx.constraintlayout.widget.SharedValues! getSharedValues();
+ method public android.view.View! getViewById(int);
+ method public final androidx.constraintlayout.core.widgets.ConstraintWidget! getViewWidget(android.view.View!);
+ method protected boolean isRtl();
+ method public void loadLayoutDescription(int);
+ method protected void parseLayoutDescription(int);
+ method protected void resolveMeasuredDimension(int, int, int, int, boolean, boolean);
+ method protected void resolveSystem(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int);
+ method public void setConstraintSet(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void setDesignInformation(int, Object!, Object!);
+ method public void setMaxHeight(int);
+ method public void setMaxWidth(int);
+ method public void setMinHeight(int);
+ method public void setMinWidth(int);
+ method public void setOnConstraintsChanged(androidx.constraintlayout.widget.ConstraintsChangedListener!);
+ method public void setOptimizationLevel(int);
+ method protected void setSelfDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int);
+ method public void setState(int, int, int);
+ field public static final int DESIGN_INFO_ID = 0; // 0x0
+ field public static final String VERSION = "ConstraintLayout-2.2.0-alpha04";
+ field protected androidx.constraintlayout.widget.ConstraintLayoutStates! mConstraintLayoutSpec;
+ field protected boolean mDirtyHierarchy;
+ field protected androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! mLayoutWidget;
+ }
+
+ public static class ConstraintLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+ ctor public ConstraintLayout.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+ ctor public ConstraintLayout.LayoutParams(android.view.ViewGroup.LayoutParams!);
+ ctor public ConstraintLayout.LayoutParams(int, int);
+ method public String! getConstraintTag();
+ method public androidx.constraintlayout.core.widgets.ConstraintWidget! getConstraintWidget();
+ method public void reset();
+ method public void setWidgetDebugName(String!);
+ method public void validate();
+ field public static final int BASELINE = 5; // 0x5
+ field public static final int BOTTOM = 4; // 0x4
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static final int CIRCLE = 8; // 0x8
+ field public static final int END = 7; // 0x7
+ field public static final int GONE_UNSET = -2147483648; // 0x80000000
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int LEFT = 1; // 0x1
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_PERCENT = 2; // 0x2
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int RIGHT = 2; // 0x2
+ field public static final int START = 6; // 0x6
+ field public static final int TOP = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int WRAP_BEHAVIOR_HORIZONTAL_ONLY = 1; // 0x1
+ field public static final int WRAP_BEHAVIOR_INCLUDED = 0; // 0x0
+ field public static final int WRAP_BEHAVIOR_SKIPPED = 3; // 0x3
+ field public static final int WRAP_BEHAVIOR_VERTICAL_ONLY = 2; // 0x2
+ field public int baselineMargin;
+ field public int baselineToBaseline;
+ field public int baselineToBottom;
+ field public int baselineToTop;
+ field public int bottomToBottom;
+ field public int bottomToTop;
+ field public float circleAngle;
+ field public int circleConstraint;
+ field public int circleRadius;
+ field public boolean constrainedHeight;
+ field public boolean constrainedWidth;
+ field public String! constraintTag;
+ field public String! dimensionRatio;
+ field public int editorAbsoluteX;
+ field public int editorAbsoluteY;
+ field public int endToEnd;
+ field public int endToStart;
+ field public int goneBaselineMargin;
+ field public int goneBottomMargin;
+ field public int goneEndMargin;
+ field public int goneLeftMargin;
+ field public int goneRightMargin;
+ field public int goneStartMargin;
+ field public int goneTopMargin;
+ field public int guideBegin;
+ field public int guideEnd;
+ field public float guidePercent;
+ field public boolean guidelineUseRtl;
+ field public boolean helped;
+ field public float horizontalBias;
+ field public int horizontalChainStyle;
+ field public float horizontalWeight;
+ field public int leftToLeft;
+ field public int leftToRight;
+ field public int matchConstraintDefaultHeight;
+ field public int matchConstraintDefaultWidth;
+ field public int matchConstraintMaxHeight;
+ field public int matchConstraintMaxWidth;
+ field public int matchConstraintMinHeight;
+ field public int matchConstraintMinWidth;
+ field public float matchConstraintPercentHeight;
+ field public float matchConstraintPercentWidth;
+ field public int orientation;
+ field public int rightToLeft;
+ field public int rightToRight;
+ field public int startToEnd;
+ field public int startToStart;
+ field public int topToBottom;
+ field public int topToTop;
+ field public float verticalBias;
+ field public int verticalChainStyle;
+ field public float verticalWeight;
+ field public int wrapBehaviorInParent;
+ }
+
+ public static interface ConstraintLayout.ValueModifier {
+ method public boolean update(int, int, int, android.view.View!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!);
+ }
+
+ public class ConstraintLayoutStates {
+ method public boolean needsToChange(int, float, float);
+ method public void setOnConstraintsChanged(androidx.constraintlayout.widget.ConstraintsChangedListener!);
+ method public void updateConstraints(int, float, float);
+ field public static final String TAG = "ConstraintLayoutStates";
+ }
+
+ public class ConstraintLayoutStatistics {
+ ctor public ConstraintLayoutStatistics(androidx.constraintlayout.widget.ConstraintLayout!);
+ ctor public ConstraintLayoutStatistics(androidx.constraintlayout.widget.ConstraintLayoutStatistics!);
+ method public void attach(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public androidx.constraintlayout.widget.ConstraintLayoutStatistics! clone();
+ method public void detach();
+ method public long getValue(int);
+ method public void logSummary(String!);
+ method public void logSummary(String!, androidx.constraintlayout.widget.ConstraintLayoutStatistics!);
+ method public void reset();
+ field public static final int DURATION_OF_CHILD_MEASURES = 5; // 0x5
+ field public static final int DURATION_OF_LAYOUT = 7; // 0x7
+ field public static final int DURATION_OF_MEASURES = 6; // 0x6
+ field public static final int NUMBER_OF_CHILD_MEASURES = 4; // 0x4
+ field public static final int NUMBER_OF_CHILD_VIEWS = 3; // 0x3
+ field public static final int NUMBER_OF_EQUATIONS = 9; // 0x9
+ field public static final int NUMBER_OF_LAYOUTS = 1; // 0x1
+ field public static final int NUMBER_OF_ON_MEASURES = 2; // 0x2
+ field public static final int NUMBER_OF_SIMPLE_EQUATIONS = 10; // 0xa
+ field public static final int NUMBER_OF_VARIABLES = 8; // 0x8
+ }
+
+ public class ConstraintProperties {
+ ctor public ConstraintProperties(android.view.View!);
+ method public androidx.constraintlayout.widget.ConstraintProperties! addToHorizontalChain(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! addToHorizontalChainRTL(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! addToVerticalChain(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! alpha(float);
+ method public void apply();
+ method public androidx.constraintlayout.widget.ConstraintProperties! center(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontally(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontally(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontallyRtl(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerHorizontallyRtl(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerVertically(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! centerVertically(int, int, int, int, int, int, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! connect(int, int, int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainDefaultHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainDefaultWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMaxHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMaxWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMinHeight(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainMinWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! constrainWidth(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! dimensionRatio(String!);
+ method public androidx.constraintlayout.widget.ConstraintProperties! elevation(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! goneMargin(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! horizontalBias(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! horizontalChainStyle(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! horizontalWeight(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! margin(int, int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! removeConstraints(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! removeFromHorizontalChain();
+ method public androidx.constraintlayout.widget.ConstraintProperties! removeFromVerticalChain();
+ method public androidx.constraintlayout.widget.ConstraintProperties! rotation(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! rotationX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! rotationY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! scaleX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! scaleY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! transformPivot(float, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! transformPivotX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! transformPivotY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translation(float, float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translationX(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translationY(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! translationZ(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! verticalBias(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! verticalChainStyle(int);
+ method public androidx.constraintlayout.widget.ConstraintProperties! verticalWeight(float);
+ method public androidx.constraintlayout.widget.ConstraintProperties! visibility(int);
+ field public static final int BASELINE = 5; // 0x5
+ field public static final int BOTTOM = 4; // 0x4
+ field public static final int END = 7; // 0x7
+ field public static final int LEFT = 1; // 0x1
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int RIGHT = 2; // 0x2
+ field public static final int START = 6; // 0x6
+ field public static final int TOP = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ }
+
+ public class ConstraintSet {
+ ctor public ConstraintSet();
+ method public void addColorAttributes(java.lang.String!...!);
+ method public void addFloatAttributes(java.lang.String!...!);
+ method public void addIntAttributes(java.lang.String!...!);
+ method public void addStringAttributes(java.lang.String!...!);
+ method public void addToHorizontalChain(int, int, int);
+ method public void addToHorizontalChainRTL(int, int, int);
+ method public void addToVerticalChain(int, int, int);
+ method public void applyCustomAttributes(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void applyDeltaFrom(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void applyTo(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void applyToHelper(androidx.constraintlayout.widget.ConstraintHelper!, androidx.constraintlayout.core.widgets.ConstraintWidget!, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!, android.util.SparseArray<androidx.constraintlayout.core.widgets.ConstraintWidget!>!);
+ method public void applyToLayoutParams(int, androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!);
+ method public void applyToWithoutCustom(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public static androidx.constraintlayout.widget.ConstraintSet.Constraint! buildDelta(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public void center(int, int, int, int, int, int, int, float);
+ method public void centerHorizontally(int, int);
+ method public void centerHorizontally(int, int, int, int, int, int, int, float);
+ method public void centerHorizontallyRtl(int, int);
+ method public void centerHorizontallyRtl(int, int, int, int, int, int, int, float);
+ method public void centerVertically(int, int);
+ method public void centerVertically(int, int, int, int, int, int, int, float);
+ method public void clear(int);
+ method public void clear(int, int);
+ method public void clone(android.content.Context!, int);
+ method public void clone(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void clone(androidx.constraintlayout.widget.Constraints!);
+ method public void clone(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void connect(int, int, int, int);
+ method public void connect(int, int, int, int, int);
+ method public void constrainCircle(int, int, int, float);
+ method public void constrainDefaultHeight(int, int);
+ method public void constrainDefaultWidth(int, int);
+ method public void constrainHeight(int, int);
+ method public void constrainMaxHeight(int, int);
+ method public void constrainMaxWidth(int, int);
+ method public void constrainMinHeight(int, int);
+ method public void constrainMinWidth(int, int);
+ method public void constrainPercentHeight(int, float);
+ method public void constrainPercentWidth(int, float);
+ method public void constrainWidth(int, int);
+ method public void constrainedHeight(int, boolean);
+ method public void constrainedWidth(int, boolean);
+ method public void create(int, int);
+ method public void createBarrier(int, int, int, int...!);
+ method public void createHorizontalChain(int, int, int, int, int[]!, float[]!, int);
+ method public void createHorizontalChainRtl(int, int, int, int, int[]!, float[]!, int);
+ method public void createVerticalChain(int, int, int, int, int[]!, float[]!, int);
+ method public void dump(androidx.constraintlayout.motion.widget.MotionScene!, int...!);
+ method public boolean getApplyElevation(int);
+ method public androidx.constraintlayout.widget.ConstraintSet.Constraint! getConstraint(int);
+ method public java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>! getCustomAttributeSet();
+ method public int getHeight(int);
+ method public int[]! getKnownIds();
+ method public androidx.constraintlayout.widget.ConstraintSet.Constraint! getParameters(int);
+ method public int[]! getReferencedIds(int);
+ method public String![]! getStateLabels();
+ method public int getVisibility(int);
+ method public int getVisibilityMode(int);
+ method public int getWidth(int);
+ method public boolean isForceId();
+ method public boolean isValidateOnParse();
+ method public void load(android.content.Context!, int);
+ method public void load(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public boolean matchesLabels(java.lang.String!...!);
+ method public void parseColorAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void parseFloatAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void parseIntAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void parseStringAttributes(androidx.constraintlayout.widget.ConstraintSet.Constraint!, String!);
+ method public void readFallback(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void readFallback(androidx.constraintlayout.widget.ConstraintSet!);
+ method public void removeAttribute(String!);
+ method public void removeFromHorizontalChain(int);
+ method public void removeFromVerticalChain(int);
+ method public void setAlpha(int, float);
+ method public void setApplyElevation(int, boolean);
+ method public void setBarrierType(int, int);
+ method public void setColorValue(int, String!, int);
+ method public void setDimensionRatio(int, String!);
+ method public void setEditorAbsoluteX(int, int);
+ method public void setEditorAbsoluteY(int, int);
+ method public void setElevation(int, float);
+ method public void setFloatValue(int, String!, float);
+ method public void setForceId(boolean);
+ method public void setGoneMargin(int, int, int);
+ method public void setGuidelineBegin(int, int);
+ method public void setGuidelineEnd(int, int);
+ method public void setGuidelinePercent(int, float);
+ method public void setHorizontalBias(int, float);
+ method public void setHorizontalChainStyle(int, int);
+ method public void setHorizontalWeight(int, float);
+ method public void setIntValue(int, String!, int);
+ method public void setLayoutWrapBehavior(int, int);
+ method public void setMargin(int, int, int);
+ method public void setReferencedIds(int, int...!);
+ method public void setRotation(int, float);
+ method public void setRotationX(int, float);
+ method public void setRotationY(int, float);
+ method public void setScaleX(int, float);
+ method public void setScaleY(int, float);
+ method public void setStateLabels(String!);
+ method public void setStateLabelsList(java.lang.String!...!);
+ method public void setStringValue(int, String!, String!);
+ method public void setTransformPivot(int, float, float);
+ method public void setTransformPivotX(int, float);
+ method public void setTransformPivotY(int, float);
+ method public void setTranslation(int, float, float);
+ method public void setTranslationX(int, float);
+ method public void setTranslationY(int, float);
+ method public void setTranslationZ(int, float);
+ method public void setValidateOnParse(boolean);
+ method public void setVerticalBias(int, float);
+ method public void setVerticalChainStyle(int, int);
+ method public void setVerticalWeight(int, float);
+ method public void setVisibility(int, int);
+ method public void setVisibilityMode(int, int);
+ method public void writeState(java.io.Writer!, androidx.constraintlayout.widget.ConstraintLayout!, int) throws java.io.IOException;
+ field public static final int BASELINE = 5; // 0x5
+ field public static final int BOTTOM = 4; // 0x4
+ field public static final int CHAIN_PACKED = 2; // 0x2
+ field public static final int CHAIN_SPREAD = 0; // 0x0
+ field public static final int CHAIN_SPREAD_INSIDE = 1; // 0x1
+ field public static final int CIRCLE_REFERENCE = 8; // 0x8
+ field public static final int END = 7; // 0x7
+ field public static final int GONE = 8; // 0x8
+ field public static final int HORIZONTAL = 0; // 0x0
+ field public static final int HORIZONTAL_GUIDELINE = 0; // 0x0
+ field public static final int INVISIBLE = 4; // 0x4
+ field public static final int LEFT = 1; // 0x1
+ field public static final int MATCH_CONSTRAINT = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_PERCENT = 2; // 0x2
+ field public static final int MATCH_CONSTRAINT_SPREAD = 0; // 0x0
+ field public static final int MATCH_CONSTRAINT_WRAP = 1; // 0x1
+ field public static final int PARENT_ID = 0; // 0x0
+ field public static final int RIGHT = 2; // 0x2
+ field public static final int ROTATE_LEFT_OF_PORTRATE = 4; // 0x4
+ field public static final int ROTATE_NONE = 0; // 0x0
+ field public static final int ROTATE_PORTRATE_OF_LEFT = 2; // 0x2
+ field public static final int ROTATE_PORTRATE_OF_RIGHT = 1; // 0x1
+ field public static final int ROTATE_RIGHT_OF_PORTRATE = 3; // 0x3
+ field public static final int START = 6; // 0x6
+ field public static final int TOP = 3; // 0x3
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int VERTICAL = 1; // 0x1
+ field public static final int VERTICAL_GUIDELINE = 1; // 0x1
+ field public static final int VISIBILITY_MODE_IGNORE = 1; // 0x1
+ field public static final int VISIBILITY_MODE_NORMAL = 0; // 0x0
+ field public static final int VISIBLE = 0; // 0x0
+ field public static final int WRAP_CONTENT = -2; // 0xfffffffe
+ field public String! derivedState;
+ field public String! mIdString;
+ field public int mRotate;
+ }
+
+ public static class ConstraintSet.Constraint {
+ ctor public ConstraintSet.Constraint();
+ method public void applyDelta(androidx.constraintlayout.widget.ConstraintSet.Constraint!);
+ method public void applyTo(androidx.constraintlayout.widget.ConstraintLayout.LayoutParams!);
+ method public androidx.constraintlayout.widget.ConstraintSet.Constraint! clone();
+ method public void printDelta(String!);
+ field public final androidx.constraintlayout.widget.ConstraintSet.Layout! layout;
+ field public java.util.HashMap<java.lang.String!,androidx.constraintlayout.widget.ConstraintAttribute!>! mCustomConstraints;
+ field public final androidx.constraintlayout.widget.ConstraintSet.Motion! motion;
+ field public final androidx.constraintlayout.widget.ConstraintSet.PropertySet! propertySet;
+ field public final androidx.constraintlayout.widget.ConstraintSet.Transform! transform;
+ }
+
+ public static class ConstraintSet.Layout {
+ ctor public ConstraintSet.Layout();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.Layout!);
+ method public void dump(androidx.constraintlayout.motion.widget.MotionScene!, StringBuilder!);
+ field public static final int UNSET = -1; // 0xffffffff
+ field public static final int UNSET_GONE_MARGIN = -2147483648; // 0x80000000
+ field public int baselineMargin;
+ field public int baselineToBaseline;
+ field public int baselineToBottom;
+ field public int baselineToTop;
+ field public int bottomMargin;
+ field public int bottomToBottom;
+ field public int bottomToTop;
+ field public float circleAngle;
+ field public int circleConstraint;
+ field public int circleRadius;
+ field public boolean constrainedHeight;
+ field public boolean constrainedWidth;
+ field public String! dimensionRatio;
+ field public int editorAbsoluteX;
+ field public int editorAbsoluteY;
+ field public int endMargin;
+ field public int endToEnd;
+ field public int endToStart;
+ field public int goneBaselineMargin;
+ field public int goneBottomMargin;
+ field public int goneEndMargin;
+ field public int goneLeftMargin;
+ field public int goneRightMargin;
+ field public int goneStartMargin;
+ field public int goneTopMargin;
+ field public int guideBegin;
+ field public int guideEnd;
+ field public float guidePercent;
+ field public boolean guidelineUseRtl;
+ field public int heightDefault;
+ field public int heightMax;
+ field public int heightMin;
+ field public float heightPercent;
+ field public float horizontalBias;
+ field public int horizontalChainStyle;
+ field public float horizontalWeight;
+ field public int leftMargin;
+ field public int leftToLeft;
+ field public int leftToRight;
+ field public boolean mApply;
+ field public boolean mBarrierAllowsGoneWidgets;
+ field public int mBarrierDirection;
+ field public int mBarrierMargin;
+ field public String! mConstraintTag;
+ field public int mHeight;
+ field public int mHelperType;
+ field public boolean mIsGuideline;
+ field public boolean mOverride;
+ field public String! mReferenceIdString;
+ field public int[]! mReferenceIds;
+ field public int mWidth;
+ field public int mWrapBehavior;
+ field public int orientation;
+ field public int rightMargin;
+ field public int rightToLeft;
+ field public int rightToRight;
+ field public int startMargin;
+ field public int startToEnd;
+ field public int startToStart;
+ field public int topMargin;
+ field public int topToBottom;
+ field public int topToTop;
+ field public float verticalBias;
+ field public int verticalChainStyle;
+ field public float verticalWeight;
+ field public int widthDefault;
+ field public int widthMax;
+ field public int widthMin;
+ field public float widthPercent;
+ }
+
+ public static class ConstraintSet.Motion {
+ ctor public ConstraintSet.Motion();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.Motion!);
+ field public int mAnimateCircleAngleTo;
+ field public int mAnimateRelativeTo;
+ field public boolean mApply;
+ field public int mDrawPath;
+ field public float mMotionStagger;
+ field public int mPathMotionArc;
+ field public float mPathRotate;
+ field public int mPolarRelativeTo;
+ field public int mQuantizeInterpolatorID;
+ field public String! mQuantizeInterpolatorString;
+ field public int mQuantizeInterpolatorType;
+ field public float mQuantizeMotionPhase;
+ field public int mQuantizeMotionSteps;
+ field public String! mTransitionEasing;
+ }
+
+ public static class ConstraintSet.PropertySet {
+ ctor public ConstraintSet.PropertySet();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.PropertySet!);
+ field public float alpha;
+ field public boolean mApply;
+ field public float mProgress;
+ field public int mVisibilityMode;
+ field public int visibility;
+ }
+
+ public static class ConstraintSet.Transform {
+ ctor public ConstraintSet.Transform();
+ method public void copyFrom(androidx.constraintlayout.widget.ConstraintSet.Transform!);
+ field public boolean applyElevation;
+ field public float elevation;
+ field public boolean mApply;
+ field public float rotation;
+ field public float rotationX;
+ field public float rotationY;
+ field public float scaleX;
+ field public float scaleY;
+ field public int transformPivotTarget;
+ field public float transformPivotX;
+ field public float transformPivotY;
+ field public float translationX;
+ field public float translationY;
+ field public float translationZ;
+ }
+
+ public class Constraints extends android.view.ViewGroup {
+ ctor public Constraints(android.content.Context!);
+ ctor public Constraints(android.content.Context!, android.util.AttributeSet!);
+ ctor public Constraints(android.content.Context!, android.util.AttributeSet!, int);
+ method protected androidx.constraintlayout.widget.Constraints.LayoutParams! generateDefaultLayoutParams();
+ method public androidx.constraintlayout.widget.Constraints.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+ method public androidx.constraintlayout.widget.ConstraintSet! getConstraintSet();
+ field public static final String TAG = "Constraints";
+ }
+
+ public static class Constraints.LayoutParams extends androidx.constraintlayout.widget.ConstraintLayout.LayoutParams {
+ ctor public Constraints.LayoutParams(android.content.Context!, android.util.AttributeSet!);
+ ctor public Constraints.LayoutParams(androidx.constraintlayout.widget.Constraints.LayoutParams!);
+ ctor public Constraints.LayoutParams(int, int);
+ field public float alpha;
+ field public boolean applyElevation;
+ field public float elevation;
+ field public float rotation;
+ field public float rotationX;
+ field public float rotationY;
+ field public float scaleX;
+ field public float scaleY;
+ field public float transformPivotX;
+ field public float transformPivotY;
+ field public float translationX;
+ field public float translationY;
+ field public float translationZ;
+ }
+
+ public abstract class ConstraintsChangedListener {
+ ctor public ConstraintsChangedListener();
+ method public void postLayoutChange(int, int);
+ method public void preLayoutChange(int, int);
+ }
+
+ public class Group extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public Group(android.content.Context!);
+ ctor public Group(android.content.Context!, android.util.AttributeSet!);
+ ctor public Group(android.content.Context!, android.util.AttributeSet!, int);
+ method public void onAttachedToWindow();
+ }
+
+ public class Guideline extends android.view.View {
+ ctor public Guideline(android.content.Context!);
+ ctor public Guideline(android.content.Context!, android.util.AttributeSet!);
+ ctor public Guideline(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public Guideline(android.content.Context!, android.util.AttributeSet!, int, int);
+ method public void setFilterRedundantCalls(boolean);
+ method public void setGuidelineBegin(int);
+ method public void setGuidelineEnd(int);
+ method public void setGuidelinePercent(float);
+ }
+
+ public class Placeholder extends android.view.View {
+ ctor public Placeholder(android.content.Context!);
+ ctor public Placeholder(android.content.Context!, android.util.AttributeSet!);
+ ctor public Placeholder(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public Placeholder(android.content.Context!, android.util.AttributeSet!, int, int);
+ method public android.view.View! getContent();
+ method public int getEmptyVisibility();
+ method public void onDraw(android.graphics.Canvas);
+ method public void setContentId(int);
+ method public void setEmptyVisibility(int);
+ method public void updatePostMeasure(androidx.constraintlayout.widget.ConstraintLayout!);
+ method public void updatePreLayout(androidx.constraintlayout.widget.ConstraintLayout!);
+ }
+
+ public class ReactiveGuide extends android.view.View implements androidx.constraintlayout.widget.SharedValues.SharedValuesListener {
+ ctor public ReactiveGuide(android.content.Context!);
+ ctor public ReactiveGuide(android.content.Context!, android.util.AttributeSet!);
+ ctor public ReactiveGuide(android.content.Context!, android.util.AttributeSet!, int);
+ ctor public ReactiveGuide(android.content.Context!, android.util.AttributeSet!, int, int);
+ method public int getApplyToConstraintSetId();
+ method public int getAttributeId();
+ method public boolean isAnimatingChange();
+ method public void onNewValue(int, int, int);
+ method public void setAnimateChange(boolean);
+ method public void setApplyToConstraintSetId(int);
+ method public void setAttributeId(int);
+ method public void setGuidelineBegin(int);
+ method public void setGuidelineEnd(int);
+ method public void setGuidelinePercent(float);
+ }
+
+ public class SharedValues {
+ ctor public SharedValues();
+ method public void addListener(int, androidx.constraintlayout.widget.SharedValues.SharedValuesListener!);
+ method public void clearListeners();
+ method public void fireNewValue(int, int);
+ method public int getValue(int);
+ method public void removeListener(androidx.constraintlayout.widget.SharedValues.SharedValuesListener!);
+ method public void removeListener(int, androidx.constraintlayout.widget.SharedValues.SharedValuesListener!);
+ field public static final int UNSET = -1; // 0xffffffff
+ }
+
+ public static interface SharedValues.SharedValuesListener {
+ method public void onNewValue(int, int, int);
+ }
+
+ public class StateSet {
+ ctor public StateSet(android.content.Context!, org.xmlpull.v1.XmlPullParser!);
+ method public int convertToConstraintSet(int, int, float, float);
+ method public boolean needsToChange(int, float, float);
+ method public void setOnConstraintsChanged(androidx.constraintlayout.widget.ConstraintsChangedListener!);
+ method public int stateGetConstraintID(int, int, int);
+ method public int updateConstraints(int, int, float, float);
+ field public static final String TAG = "ConstraintLayoutStates";
+ }
+
+ public abstract class VirtualLayout extends androidx.constraintlayout.widget.ConstraintHelper {
+ ctor public VirtualLayout(android.content.Context!);
+ ctor public VirtualLayout(android.content.Context!, android.util.AttributeSet!);
+ ctor public VirtualLayout(android.content.Context!, android.util.AttributeSet!, int);
+ method public void onAttachedToWindow();
+ method public void onMeasure(androidx.constraintlayout.core.widgets.VirtualLayout!, int, int);
+ }
+
+}
+
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index ec8d536..74c7f09 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -19,6 +19,8 @@
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
+import android.util.Base64
+import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import androidx.core.splashscreen.SplashScreenViewProvider
@@ -28,6 +30,7 @@
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.screenshot.matchers.MSSIMMatcher
import androidx.test.uiautomator.UiDevice
+import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -63,6 +66,8 @@
arrayOf("AppCompat", SplashScreenAppCompatTestActivity::class)
)
}
+
+ const val TAG = "SplashscreenParameterizedTest"
}
@Before
@@ -310,6 +315,12 @@
)
if (!matcher.matches) {
+ // Serialize the screenshots and output them through Logcat so as to gather more details
+ // for debugging.
+ logLongMessage(Log::e, TAG, "before", beforeScreenshot.toBase64String())
+ logLongMessage(Log::e, TAG, "after", afterScreenshot.toBase64String())
+ matcher.diff?.let { logLongMessage(Log::e, TAG, "diff", it.toBase64String()) }
+
val bundle = Bundle()
val diff = matcher.diff?.writeToDevice("diff.png")
bundle.putString("splashscreen_diff", diff?.absolutePath)
@@ -330,6 +341,50 @@
}
}
+ /**
+ * A log message has a maximum of 4096 bytes, where date / time, tag, process, etc. included.
+ *
+ * Therefore, we should chunk a large message into some smaller ones.
+ */
+ private fun logLongMessage(
+ logger: (tag: String, msg: String) -> Int,
+ tag: String,
+ title: String,
+ msg: String
+ ) {
+ val chunks = msg.chunked(4000)
+ logger(tag, "$title ${chunks.size}")
+
+ for ((i, chunk) in chunks.withIndex()) {
+ logger(tag, title + " $i/${chunks.size} " + chunk)
+ }
+ }
+
+ /**
+ * Serialize a bitmap into a string in Base64 encoding so that we could output it through logs
+ * when comparisons fail.
+ */
+ private fun Bitmap.toBase64String(): String {
+ val scaledBitmap =
+ Bitmap.createScaledBitmap(
+ this,
+ // Reduce the size of the bitmap
+ width * 3 shr 2,
+ height * 3 shr 2,
+ false
+ )
+ val outputStream = ByteArrayOutputStream()
+ scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
+
+ val bytes = outputStream.toByteArray()
+ val str =
+ Base64.encodeToString(
+ bytes,
+ Base64.NO_WRAP // Not to wrap here as we are going to wrap on our own later
+ )
+ return str
+ }
+
private fun Bitmap.writeToDevice(name: String): File {
return writeToDevice(
{ compress(Bitmap.CompressFormat.PNG, 0 /*ignored for png*/, it) },
diff --git a/core/core-telecom/api/current.txt b/core/core-telecom/api/current.txt
index 6dd11ef..c94b261 100644
--- a/core/core-telecom/api/current.txt
+++ b/core/core-telecom/api/current.txt
@@ -2,17 +2,20 @@
package androidx.core.telecom {
public final class CallAttributesCompat {
- ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, int direction, optional int callType, optional int callCapabilities);
+ ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, int direction, optional int callType, optional int callCapabilities, optional androidx.core.telecom.CallEndpointCompat? preferredStartingCallEndpoint);
method public android.net.Uri getAddress();
method public int getCallCapabilities();
method public int getCallType();
method public int getDirection();
method public CharSequence getDisplayName();
+ method public androidx.core.telecom.CallEndpointCompat? getPreferredStartingCallEndpoint();
+ method public void setPreferredStartingCallEndpoint(androidx.core.telecom.CallEndpointCompat?);
property public final android.net.Uri address;
property public final int callCapabilities;
property public final int callType;
property public final int direction;
property public final CharSequence displayName;
+ property public final androidx.core.telecom.CallEndpointCompat? preferredStartingCallEndpoint;
field public static final int CALL_TYPE_AUDIO_CALL = 1; // 0x1
field public static final int CALL_TYPE_VIDEO_CALL = 2; // 0x2
field public static final androidx.core.telecom.CallAttributesCompat.Companion Companion;
@@ -54,8 +57,9 @@
property public abstract kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted;
}
- @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat {
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat implements java.lang.Comparable<androidx.core.telecom.CallEndpointCompat> {
ctor public CallEndpointCompat(CharSequence name, int type, android.os.ParcelUuid identifier);
+ method public int compareTo(androidx.core.telecom.CallEndpointCompat other);
method public android.os.ParcelUuid getIdentifier();
method public CharSequence getName();
method public int getType();
@@ -93,9 +97,11 @@
public static final class CallException.Companion {
}
- @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager {
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager implements androidx.core.telecom.extensions.CallsManagerExtensions {
ctor public CallsManager(android.content.Context context);
method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public suspend Object? addCall(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onAnswer, kotlin.jvm.functions.Function2<? super android.telecom.DisconnectCause,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onDisconnect, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetActive, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetInactive, kotlin.jvm.functions.Function1<? super androidx.core.telecom.CallControlScope,kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? addCallWithExtensions(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onAnswer, kotlin.jvm.functions.Function2<? super android.telecom.DisconnectCause,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onDisconnect, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetActive, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetInactive, kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.ExtensionInitializationScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> getAvailableStartingCallEndpoints();
method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public void registerAppWithTelecom(int capabilities);
field public static final int CAPABILITY_BASELINE = 1; // 0x1
field public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 4; // 0x4
@@ -106,6 +112,75 @@
public static final class CallsManager.Companion {
}
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public class InCallServiceCompat extends android.telecom.InCallService implements androidx.core.telecom.extensions.CallExtensions androidx.lifecycle.LifecycleOwner {
+ ctor public InCallServiceCompat();
+ method @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? connectExtensions(android.telecom.Call call, kotlin.jvm.functions.Function1<? super androidx.core.telecom.extensions.CallExtensionScope,kotlin.Unit> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+}
+
+package androidx.core.telecom.extensions {
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface CallExtensionScope {
+ method public androidx.core.telecom.extensions.ParticipantExtensionRemote addParticipantExtension(kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.Participant?,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onActiveParticipantChanged, kotlin.jvm.functions.Function2<? super java.util.Set<? extends androidx.core.telecom.extensions.Participant>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onParticipantsUpdated);
+ method public void onConnected(kotlin.jvm.functions.Function2<? super android.telecom.Call,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ }
+
+ public interface CallExtensions {
+ method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? connectExtensions(android.telecom.Call call, kotlin.jvm.functions.Function1<? super androidx.core.telecom.extensions.CallExtensionScope,kotlin.Unit> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ public interface CallsManagerExtensions {
+ method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? addCallWithExtensions(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onAnswer, kotlin.jvm.functions.Function2<? super android.telecom.DisconnectCause,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onDisconnect, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetActive, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetInactive, kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.ExtensionInitializationScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ExtensionInitializationScope {
+ method public androidx.core.telecom.extensions.ParticipantExtension addParticipantExtension(optional java.util.Set<androidx.core.telecom.extensions.Participant> initialParticipants, optional androidx.core.telecom.extensions.Participant? initialActiveParticipant);
+ method public void onCall(kotlin.jvm.functions.Function2<? super androidx.core.telecom.CallControlScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onCall);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface KickParticipantAction {
+ method public boolean isSupported();
+ method public suspend Object? requestKickParticipant(androidx.core.telecom.extensions.Participant participant, kotlin.coroutines.Continuation<? super androidx.core.telecom.CallControlResult>);
+ method public void setSupported(boolean);
+ property public abstract boolean isSupported;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public final class Participant {
+ ctor public Participant(String id, CharSequence name);
+ method public String getId();
+ method public CharSequence getName();
+ property public final String id;
+ property public final CharSequence name;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ParticipantExtension {
+ method public void addKickParticipantSupport(kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.Participant,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onKickParticipant);
+ method public androidx.core.telecom.extensions.RaiseHandState addRaiseHandSupport(optional java.util.List<androidx.core.telecom.extensions.Participant> initialRaisedHands, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onHandRaisedChanged);
+ method public suspend Object? updateActiveParticipant(androidx.core.telecom.extensions.Participant? participant, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? updateParticipants(java.util.Set<androidx.core.telecom.extensions.Participant> newParticipants, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ParticipantExtensionRemote {
+ method public androidx.core.telecom.extensions.KickParticipantAction addKickParticipantAction();
+ method public androidx.core.telecom.extensions.RaiseHandAction addRaiseHandAction(kotlin.jvm.functions.Function2<? super java.util.List<? extends androidx.core.telecom.extensions.Participant>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onRaisedHandsChanged);
+ method public boolean isSupported();
+ property public abstract boolean isSupported;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface RaiseHandAction {
+ method public boolean isSupported();
+ method public suspend Object? requestRaisedHandStateChange(boolean isRaised, kotlin.coroutines.Continuation<? super androidx.core.telecom.CallControlResult>);
+ method public void setSupported(boolean);
+ property public abstract boolean isSupported;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface RaiseHandState {
+ method public suspend Object? updateRaisedHands(java.util.List<androidx.core.telecom.extensions.Participant> raisedHands, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
}
package androidx.core.telecom.util {
diff --git a/core/core-telecom/api/restricted_current.txt b/core/core-telecom/api/restricted_current.txt
index 6dd11ef..c94b261 100644
--- a/core/core-telecom/api/restricted_current.txt
+++ b/core/core-telecom/api/restricted_current.txt
@@ -2,17 +2,20 @@
package androidx.core.telecom {
public final class CallAttributesCompat {
- ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, int direction, optional int callType, optional int callCapabilities);
+ ctor public CallAttributesCompat(CharSequence displayName, android.net.Uri address, int direction, optional int callType, optional int callCapabilities, optional androidx.core.telecom.CallEndpointCompat? preferredStartingCallEndpoint);
method public android.net.Uri getAddress();
method public int getCallCapabilities();
method public int getCallType();
method public int getDirection();
method public CharSequence getDisplayName();
+ method public androidx.core.telecom.CallEndpointCompat? getPreferredStartingCallEndpoint();
+ method public void setPreferredStartingCallEndpoint(androidx.core.telecom.CallEndpointCompat?);
property public final android.net.Uri address;
property public final int callCapabilities;
property public final int callType;
property public final int direction;
property public final CharSequence displayName;
+ property public final androidx.core.telecom.CallEndpointCompat? preferredStartingCallEndpoint;
field public static final int CALL_TYPE_AUDIO_CALL = 1; // 0x1
field public static final int CALL_TYPE_VIDEO_CALL = 2; // 0x2
field public static final androidx.core.telecom.CallAttributesCompat.Companion Companion;
@@ -54,8 +57,9 @@
property public abstract kotlinx.coroutines.flow.Flow<java.lang.Boolean> isMuted;
}
- @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat {
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallEndpointCompat implements java.lang.Comparable<androidx.core.telecom.CallEndpointCompat> {
ctor public CallEndpointCompat(CharSequence name, int type, android.os.ParcelUuid identifier);
+ method public int compareTo(androidx.core.telecom.CallEndpointCompat other);
method public android.os.ParcelUuid getIdentifier();
method public CharSequence getName();
method public int getType();
@@ -93,9 +97,11 @@
public static final class CallException.Companion {
}
- @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager {
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CallsManager implements androidx.core.telecom.extensions.CallsManagerExtensions {
ctor public CallsManager(android.content.Context context);
method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public suspend Object? addCall(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onAnswer, kotlin.jvm.functions.Function2<? super android.telecom.DisconnectCause,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onDisconnect, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetActive, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetInactive, kotlin.jvm.functions.Function1<? super androidx.core.telecom.CallControlScope,kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? addCallWithExtensions(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onAnswer, kotlin.jvm.functions.Function2<? super android.telecom.DisconnectCause,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onDisconnect, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetActive, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetInactive, kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.ExtensionInitializationScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.core.telecom.CallEndpointCompat>> getAvailableStartingCallEndpoints();
method @RequiresPermission("android.permission.MANAGE_OWN_CALLS") public void registerAppWithTelecom(int capabilities);
field public static final int CAPABILITY_BASELINE = 1; // 0x1
field public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 4; // 0x4
@@ -106,6 +112,75 @@
public static final class CallsManager.Companion {
}
+ @RequiresApi(android.os.Build.VERSION_CODES.O) public class InCallServiceCompat extends android.telecom.InCallService implements androidx.core.telecom.extensions.CallExtensions androidx.lifecycle.LifecycleOwner {
+ ctor public InCallServiceCompat();
+ method @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? connectExtensions(android.telecom.Call call, kotlin.jvm.functions.Function1<? super androidx.core.telecom.extensions.CallExtensionScope,kotlin.Unit> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ property public androidx.lifecycle.Lifecycle lifecycle;
+ }
+
+}
+
+package androidx.core.telecom.extensions {
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface CallExtensionScope {
+ method public androidx.core.telecom.extensions.ParticipantExtensionRemote addParticipantExtension(kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.Participant?,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onActiveParticipantChanged, kotlin.jvm.functions.Function2<? super java.util.Set<? extends androidx.core.telecom.extensions.Participant>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onParticipantsUpdated);
+ method public void onConnected(kotlin.jvm.functions.Function2<? super android.telecom.Call,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
+ }
+
+ public interface CallExtensions {
+ method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? connectExtensions(android.telecom.Call call, kotlin.jvm.functions.Function1<? super androidx.core.telecom.extensions.CallExtensionScope,kotlin.Unit> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ public interface CallsManagerExtensions {
+ method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.core.telecom.util.ExperimentalAppActions public suspend Object? addCallWithExtensions(androidx.core.telecom.CallAttributesCompat callAttributes, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onAnswer, kotlin.jvm.functions.Function2<? super android.telecom.DisconnectCause,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onDisconnect, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetActive, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onSetInactive, kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.ExtensionInitializationScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> init, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ExtensionInitializationScope {
+ method public androidx.core.telecom.extensions.ParticipantExtension addParticipantExtension(optional java.util.Set<androidx.core.telecom.extensions.Participant> initialParticipants, optional androidx.core.telecom.extensions.Participant? initialActiveParticipant);
+ method public void onCall(kotlin.jvm.functions.Function2<? super androidx.core.telecom.CallControlScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onCall);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface KickParticipantAction {
+ method public boolean isSupported();
+ method public suspend Object? requestKickParticipant(androidx.core.telecom.extensions.Participant participant, kotlin.coroutines.Continuation<? super androidx.core.telecom.CallControlResult>);
+ method public void setSupported(boolean);
+ property public abstract boolean isSupported;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public final class Participant {
+ ctor public Participant(String id, CharSequence name);
+ method public String getId();
+ method public CharSequence getName();
+ property public final String id;
+ property public final CharSequence name;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ParticipantExtension {
+ method public void addKickParticipantSupport(kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.Participant,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onKickParticipant);
+ method public androidx.core.telecom.extensions.RaiseHandState addRaiseHandSupport(optional java.util.List<androidx.core.telecom.extensions.Participant> initialRaisedHands, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onHandRaisedChanged);
+ method public suspend Object? updateActiveParticipant(androidx.core.telecom.extensions.Participant? participant, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? updateParticipants(java.util.Set<androidx.core.telecom.extensions.Participant> newParticipants, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ParticipantExtensionRemote {
+ method public androidx.core.telecom.extensions.KickParticipantAction addKickParticipantAction();
+ method public androidx.core.telecom.extensions.RaiseHandAction addRaiseHandAction(kotlin.jvm.functions.Function2<? super java.util.List<? extends androidx.core.telecom.extensions.Participant>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onRaisedHandsChanged);
+ method public boolean isSupported();
+ property public abstract boolean isSupported;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface RaiseHandAction {
+ method public boolean isSupported();
+ method public suspend Object? requestRaisedHandStateChange(boolean isRaised, kotlin.coroutines.Continuation<? super androidx.core.telecom.CallControlResult>);
+ method public void setSupported(boolean);
+ property public abstract boolean isSupported;
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface RaiseHandState {
+ method public suspend Object? updateRaisedHands(java.util.List<androidx.core.telecom.extensions.Participant> raisedHands, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
}
package androidx.core.telecom.util {
diff --git a/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml b/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
index 25c5e6d..378517a 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/core/core-telecom/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -18,6 +18,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <!-- BLUETOOTH_CONNECT is needed in order to expose the BT device name -->
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<application
android:icon="@drawable/ic_launcher"
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
index 8e7b013..f4ec9ac 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
@@ -54,7 +54,7 @@
val disconnectButton: Button = itemView.findViewById(R.id.disconnectButton)
// Call Audio Buttons
- val earpieceButton: Button = itemView.findViewById(R.id.earpieceButton)
+ val earpieceButton: Button = itemView.findViewById(R.id.selectEndpointButton)
val speakerButton: Button = itemView.findViewById(R.id.speakerButton)
val bluetoothButton: Button = itemView.findViewById(R.id.bluetoothButton)
}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
index d1303bf..77025be 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
@@ -25,6 +25,7 @@
import android.widget.CheckBox
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.CallsManager
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
@@ -32,6 +33,7 @@
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
@RequiresApi(34)
@@ -44,11 +46,16 @@
// Telecom
private var mCallsManager: CallsManager? = null
- // Call Log objects
+ // Ongoing Call List
private var mRecyclerView: RecyclerView? = null
private var mCallObjects: ArrayList<CallRow> = ArrayList()
private lateinit var mAdapter: CallListAdapter
+ // Pre-Call Endpoint List
+ private var mPreCallEndpointsRecyclerView: RecyclerView? = null
+ private var mCurrentPreCallEndpoints: ArrayList<CallEndpointCompat> = arrayListOf()
+ private lateinit var mPreCallEndpointAdapter: PreCallEndpointsAdapter
+
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
@@ -61,6 +68,11 @@
val registerPhoneAccountButton = findViewById<Button>(R.id.registerButton)
registerPhoneAccountButton.setOnClickListener { mScope.launch { registerPhoneAccount() } }
+ val fetchPreCallEndpointsButton = findViewById<Button>(R.id.preCallAudioEndpointsButton)
+ fetchPreCallEndpointsButton.setOnClickListener {
+ mScope.launch { fetchPreCallEndpoints(findViewById(R.id.cancelFlowButton)) }
+ }
+
val addOutgoingCallButton = findViewById<Button>(R.id.addOutgoingCall)
addOutgoingCallButton.setOnClickListener {
mScope.launch { addCallWithAttributes(Utilities.OUTGOING_CALL_ATTRIBUTES) }
@@ -71,13 +83,17 @@
mScope.launch { addCallWithAttributes(Utilities.INCOMING_CALL_ATTRIBUTES) }
}
- // Set up AudioRecord
+ // setup the adapters which hold the endpoint and call rows
mAdapter = CallListAdapter(mCallObjects, null)
+ mPreCallEndpointAdapter = PreCallEndpointsAdapter(mCurrentPreCallEndpoints)
- // set up the call list view holder
+ // set up the view holders
mRecyclerView = findViewById(R.id.callListRecyclerView)
mRecyclerView?.layoutManager = LinearLayoutManager(this)
mRecyclerView?.adapter = mAdapter
+ mPreCallEndpointsRecyclerView = findViewById(R.id.endpointsRecyclerView)
+ mPreCallEndpointsRecyclerView?.layoutManager = LinearLayoutManager(this)
+ mPreCallEndpointsRecyclerView?.adapter = mPreCallEndpointAdapter
}
override fun onDestroy() {
@@ -117,15 +133,18 @@
Log.i(TAG, "CoroutineExceptionHandler: handling e=$exception")
}
- CoroutineScope(Dispatchers.IO).launch(handler) {
+ CoroutineScope(Dispatchers.Default).launch(handler) {
try {
+ attributes.preferredStartingCallEndpoint =
+ mPreCallEndpointAdapter.mSelectedCallEndpoint
mCallsManager!!.addCall(
attributes,
callObject.mOnAnswerLambda,
callObject.mOnDisconnectLambda,
callObject.mOnSetActiveLambda,
- callObject.mOnSetInActiveLambda
+ callObject.mOnSetInActiveLambda,
) {
+ mPreCallEndpointAdapter.mSelectedCallEndpoint = null
// inject client control interface into the VoIP call object
callObject.setCallId(getCallId().toString())
callObject.setCallControl(this)
@@ -155,6 +174,29 @@
}
}
+ private fun fetchPreCallEndpoints(cancelFlowButton: Button) {
+ val endpointsFlow = mCallsManager!!.getAvailableStartingCallEndpoints()
+ CoroutineScope(Dispatchers.Default).launch {
+ launch {
+ val endpointsCoroutineScope = this
+ Log.i(TAG, "fetchEndpoints: consuming endpoints")
+ endpointsFlow.collect {
+ for (endpoint in it) {
+ Log.i(TAG, "fetchEndpoints: endpoint=[$endpoint}")
+ }
+ cancelFlowButton.setOnClickListener {
+ mPreCallEndpointAdapter.mSelectedCallEndpoint = null
+ endpointsCoroutineScope.cancel()
+ updatePreCallEndpoints(null)
+ }
+ updatePreCallEndpoints(it)
+ }
+ // At this point, the endpointsCoroutineScope has been canceled
+ updatePreCallEndpoints(null)
+ }
+ }
+ }
+
private fun logException(e: Exception, prefix: String) {
Log.i(TAG, "$prefix: e=[$e], e.msg=[${e.message}], e.stack:${e.printStackTrace()}")
}
@@ -168,4 +210,14 @@
private fun updateCallList() {
runOnUiThread { mAdapter.notifyDataSetChanged() }
}
+
+ private fun updatePreCallEndpoints(newEndpoints: List<CallEndpointCompat>?) {
+ runOnUiThread {
+ mCurrentPreCallEndpoints.clear()
+ if (newEndpoints != null) {
+ mCurrentPreCallEndpoints.addAll(newEndpoints)
+ }
+ mPreCallEndpointAdapter.notifyDataSetChanged()
+ }
+ }
}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/PreCallEndpointsAdapter.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/PreCallEndpointsAdapter.kt
new file mode 100644
index 0000000..0ddf351
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/PreCallEndpointsAdapter.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallEndpointCompat
+import androidx.recyclerview.widget.RecyclerView
+
+@RequiresApi(26)
+class PreCallEndpointsAdapter(private var mCurrentEndpoints: ArrayList<CallEndpointCompat>?) :
+ RecyclerView.Adapter<PreCallEndpointsAdapter.ViewHolder>() {
+ var mSelectedCallEndpoint: CallEndpointCompat? = null
+
+ companion object {
+ val TAG: String = PreCallEndpointsAdapter::class.java.simpleName
+ }
+
+ class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
+ // TextViews
+ val endpointName: TextView = itemView.findViewById(R.id.endpoint_name)
+ val endpointType: TextView = itemView.findViewById(R.id.endpoint_type_id)
+ val endpointUuid: TextView = itemView.findViewById(R.id.endpoint_uuid_id)
+ // Call State Buttons
+ val selectButton: Button = itemView.findViewById(R.id.select_endpoint_id)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ // inflates the card_view_design view that is used to hold list item
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.endpoint_row, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun getItemCount(): Int {
+ return mCurrentEndpoints?.size ?: 0
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val callEndpointCompat = mCurrentEndpoints?.get(position)
+ if (callEndpointCompat != null) {
+ holder.endpointName.text = callEndpointCompat.name
+ holder.endpointType.text = callEndpointCompat.type.toString()
+ holder.endpointUuid.text = callEndpointCompat.identifier.toString()
+
+ holder.selectButton.setOnClickListener {
+ Log.i(TAG, "selected: preCallEndpoint=[${callEndpointCompat}]")
+ mSelectedCallEndpoint = callEndpointCompat
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
index ae035a5..68efdc9 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -47,12 +47,14 @@
android:id="@+id/VideoCallingCheckBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:padding="16dp"
android:text="CAPABILITY_SUPPORTS_VIDEO_CALLING" />
<CheckBox
android:id="@+id/streamingCheckBox"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="61dp"
+ android:padding="16dp"
android:text="CAPABILITY_SUPPORTS_CALL_STREAMING" />
<Button
@@ -66,6 +68,32 @@
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/preCallAudioEndpointsButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:backgroundTint="#4CAF50"
+ android:text="Fetch Pre-Call Endpoints" />
+
+ <Button
+ android:id="@+id/cancelFlowButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:backgroundTint="#D92121"
+ android:text="Stop fetching endpoints" />
+ </LinearLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/endpointsRecyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="164dp" />
+
+ <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
@@ -98,7 +126,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/callListRecyclerView"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="273dp"
tools:itemCount="3" />
</LinearLayout>
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
index 9001096..ae53266 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
@@ -17,7 +17,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
@@ -29,17 +30,17 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
- <TextView
- android:id="@+id/callNumber"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="call # -" />
+ <TextView
+ android:id="@+id/callNumber"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="call # -" />
- <TextView
- android:id="@+id/callIdTextView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="callId" />
+ <TextView
+ android:id="@+id/callIdTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="callId" />
</LinearLayout>
<LinearLayout
@@ -47,11 +48,11 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
- <TextView
- android:id="@+id/callStateTextView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="currentCallState=[null]; " />
+ <TextView
+ android:id="@+id/callStateTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="currentCallState=[null]; " />
<TextView
android:id="@+id/endpointStateTextView"
@@ -71,6 +72,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
+ android:backgroundTint="#4CAF50"
android:text="Active" />
<Button
@@ -85,6 +87,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
+ android:backgroundTint="#F44336"
android:text="Disc." />
</LinearLayout>
@@ -94,7 +97,7 @@
android:orientation="horizontal">
<Button
- android:id="@+id/earpieceButton"
+ android:id="@+id/selectEndpointButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/endpoint_row.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/endpoint_row.xml
new file mode 100644
index 0000000..c4aea89
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/endpoint_row.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/endpointType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/endpoint_name"
+ android:layout_width="319dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="endpointName=[""]" />
+
+ <TextView
+ android:id="@+id/endpoint_type_id"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="type=[UNKNOWN]" />
+
+ <TextView
+ android:id="@+id/endpoint_uuid_id"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="uuid=[null]" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/select_endpoint_id"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Select Endpoint" />
+</LinearLayout>
\ No newline at end of file
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallEndpointCompatTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallEndpointCompatTest.kt
index bcd6d36..5ec499d 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallEndpointCompatTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallEndpointCompatTest.kt
@@ -16,14 +16,15 @@
package androidx.core.telecom.test
+import android.media.AudioDeviceInfo
import android.os.Build.VERSION_CODES
-import android.os.ParcelUuid
import android.telecom.CallAudioState
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.internal.utils.EndpointUtils
+import androidx.core.telecom.internal.utils.EndpointUtils.Companion.remapAudioDeviceTypeToCallEndpointType
+import androidx.core.telecom.test.utils.TestUtils
import androidx.test.filters.SdkSuppress
-import java.util.UUID
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -34,7 +35,7 @@
fun testCallEndpointConstructor() {
val name = "Endpoint"
val type = CallEndpointCompat.TYPE_EARPIECE
- val identifier = ParcelUuid.fromString(UUID.randomUUID().toString())
+ val identifier = TestUtils.generateRandomUuid()
val endpoint = CallEndpointCompat(name, type, identifier)
assertEquals(name, endpoint.name)
assertEquals(type, endpoint.type)
@@ -113,4 +114,119 @@
)
assertEquals(CallAudioState.ROUTE_EARPIECE, EndpointUtils.mapTypeToRoute(-1))
}
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testAudioDeviceInfoTypeToCallEndpointTypeRemapping() {
+ assertEquals(
+ CallEndpointCompat.TYPE_EARPIECE,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_SPEAKER,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
+ )
+ // Wired Headset Devices
+ assertEquals(
+ CallEndpointCompat.TYPE_WIRED_HEADSET,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_WIRED_HEADSET)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_WIRED_HEADSET,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_WIRED_HEADPHONES)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_WIRED_HEADSET,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_USB_DEVICE)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_WIRED_HEADSET,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_USB_ACCESSORY)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_WIRED_HEADSET,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_USB_HEADSET)
+ )
+ // Bluetooth Devices
+ assertEquals(
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_HEARING_AID)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_BLE_HEADSET)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_BLE_SPEAKER)
+ )
+ assertEquals(
+ CallEndpointCompat.TYPE_BLUETOOTH,
+ remapAudioDeviceTypeToCallEndpointType(AudioDeviceInfo.TYPE_BLE_BROADCAST)
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testDefaultSort() {
+ val highestPriorityEndpoint = CallEndpointCompat("F", CallEndpointCompat.TYPE_WIRED_HEADSET)
+ val second = CallEndpointCompat("E", CallEndpointCompat.TYPE_BLUETOOTH)
+ val third = CallEndpointCompat("D", CallEndpointCompat.TYPE_SPEAKER)
+ val fourth = CallEndpointCompat("C", CallEndpointCompat.TYPE_EARPIECE)
+ val fifth = CallEndpointCompat("B", CallEndpointCompat.TYPE_STREAMING)
+ val lowestPriorityEndpoint = CallEndpointCompat("A", CallEndpointCompat.TYPE_UNKNOWN)
+
+ val endpoints =
+ mutableListOf(
+ lowestPriorityEndpoint,
+ fourth,
+ second,
+ fifth,
+ third,
+ highestPriorityEndpoint
+ )
+
+ endpoints.sort()
+
+ assertEquals(highestPriorityEndpoint, endpoints[0])
+ assertEquals(second, endpoints[1])
+ assertEquals(third, endpoints[2])
+ assertEquals(fourth, endpoints[3])
+ assertEquals(fifth, endpoints[4])
+ assertEquals(lowestPriorityEndpoint, endpoints[5])
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @Test
+ fun testDefaultSortWithDuplicateTypes() {
+ val highestPriorityEndpoint = CallEndpointCompat("A", CallEndpointCompat.TYPE_BLUETOOTH)
+ val second = CallEndpointCompat("B", CallEndpointCompat.TYPE_BLUETOOTH)
+ val third = CallEndpointCompat("C", CallEndpointCompat.TYPE_BLUETOOTH)
+ val fourth = CallEndpointCompat("D", CallEndpointCompat.TYPE_BLUETOOTH)
+ val fifth = CallEndpointCompat("E", CallEndpointCompat.TYPE_BLUETOOTH)
+ val lowestPriorityEndpoint = CallEndpointCompat("F", CallEndpointCompat.TYPE_BLUETOOTH)
+
+ val endpoints =
+ mutableListOf(
+ lowestPriorityEndpoint,
+ fourth,
+ second,
+ fifth,
+ third,
+ highestPriorityEndpoint
+ )
+
+ endpoints.sort()
+
+ assertEquals(highestPriorityEndpoint, endpoints[0])
+ assertEquals(second, endpoints[1])
+ assertEquals(third, endpoints[2])
+ assertEquals(fourth, endpoints[3])
+ assertEquals(fifth, endpoints[4])
+ assertEquals(lowestPriorityEndpoint, endpoints[5])
+ }
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionLegacyTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionLegacyTest.kt
new file mode 100644
index 0000000..e326c4f
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionLegacyTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test
+
+import android.os.Build.VERSION_CODES
+import android.os.ParcelUuid
+import android.telecom.CallAudioState
+import android.telecom.CallEndpoint
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallEndpointCompat
+import androidx.core.telecom.internal.CallChannels
+import androidx.core.telecom.internal.CallSessionLegacy
+import androidx.core.telecom.internal.PreCallEndpoints
+import androidx.core.telecom.internal.utils.EndpointUtils
+import androidx.core.telecom.test.utils.BaseTelecomTest
+import androidx.core.telecom.test.utils.TestUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import java.util.UUID
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = VERSION_CODES.O /* api=26 */)
+@RequiresApi(VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class CallSessionLegacyTest : BaseTelecomTest() {
+ val mEarpieceEndpoint = CallEndpointCompat("EARPIECE", CallEndpoint.TYPE_EARPIECE)
+ val mSpeakerEndpoint = CallEndpointCompat("SPEAKER", CallEndpoint.TYPE_SPEAKER)
+ val mEarAndSpeakerEndpoints = listOf(mEarpieceEndpoint, mSpeakerEndpoint)
+
+ /**
+ * Verify the [CallEndpoint]s echoed from the platform are re-mapped to the existing
+ * [CallEndpointCompat]s the user received with
+ * [androidx.core.telecom.CallsManager#getAvailableStartingCallEndpoints()]
+ */
+ @SmallTest
+ @Test
+ fun testPlatformEndpointsAreRemappedToExistingEndpoints() {
+ setUpBackwardsCompatTest()
+ runBlocking {
+ val callSession =
+ initCallSessionLegacy(
+ coroutineContext,
+ null,
+ PreCallEndpoints(mEarAndSpeakerEndpoints.toMutableList(), Channel())
+ )
+ val supportedRouteMask = CallAudioState.ROUTE_EARPIECE or CallAudioState.ROUTE_SPEAKER
+
+ val platformEndpoints =
+ EndpointUtils.toCallEndpointsCompat(
+ CallAudioState(false, CallAudioState.ROUTE_EARPIECE, supportedRouteMask)
+ )
+
+ val platformEarpiece = platformEndpoints[0]
+ assertEquals(CallEndpointCompat.TYPE_EARPIECE, platformEarpiece.type)
+ assertEquals(
+ mEarpieceEndpoint,
+ callSession.toRemappedCallEndpointCompat(platformEarpiece)
+ )
+
+ val platformSpeaker = platformEndpoints[1]
+ assertEquals(CallEndpointCompat.TYPE_SPEAKER, platformSpeaker.type)
+ assertEquals(
+ mSpeakerEndpoint,
+ callSession.toRemappedCallEndpointCompat(platformSpeaker)
+ )
+ }
+ }
+
+ private fun initCallSessionLegacy(
+ coroutineContext: CoroutineContext,
+ preferredStartingEndpoint: CallEndpointCompat?,
+ preCallEndpoints: PreCallEndpoints
+ ): CallSessionLegacy {
+ return CallSessionLegacy(
+ getRandomParcelUuid(),
+ TestUtils.INCOMING_CALL_ATTRIBUTES,
+ CallChannels(),
+ coroutineContext,
+ TestUtils.mOnAnswerLambda,
+ TestUtils.mOnDisconnectLambda,
+ TestUtils.mOnSetActiveLambda,
+ TestUtils.mOnSetInActiveLambda,
+ { _, _ -> },
+ preferredStartingEndpoint,
+ preCallEndpoints,
+ CompletableDeferred(Unit),
+ )
+ }
+
+ private fun getRandomParcelUuid(): ParcelUuid {
+ return ParcelUuid.fromString(UUID.randomUUID().toString())
+ }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionTest.kt
index 9dd0e39..2bb2315 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallSessionTest.kt
@@ -23,6 +23,7 @@
import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.internal.CallChannels
import androidx.core.telecom.internal.CallSession
+import androidx.core.telecom.internal.PreCallEndpoints
import androidx.core.telecom.test.utils.BaseTelecomTest
import androidx.core.telecom.test.utils.TestUtils
import androidx.core.telecom.util.ExperimentalAppActions
@@ -32,9 +33,11 @@
import java.util.UUID
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -50,11 +53,11 @@
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@RunWith(AndroidJUnit4::class)
class CallSessionTest : BaseTelecomTest() {
- val mEarpieceEndpoint = CallEndpointCompat("EARPIECE", CallEndpoint.TYPE_EARPIECE)
- val mSpeakerEndpoint = CallEndpointCompat("SPEAKER", CallEndpoint.TYPE_SPEAKER)
- val mBluetoothEndpoint = CallEndpointCompat("BLUETOOTH", CallEndpoint.TYPE_BLUETOOTH)
- val mEarAndSpeakerEndpoints = listOf(mEarpieceEndpoint, mSpeakerEndpoint)
- val mEarAndSpeakerAndBtEndpoints =
+ private val mEarpieceEndpoint = CallEndpointCompat("EARPIECE", CallEndpoint.TYPE_EARPIECE)
+ private val mSpeakerEndpoint = CallEndpointCompat("SPEAKER", CallEndpoint.TYPE_SPEAKER)
+ private val mBluetoothEndpoint = CallEndpointCompat("BLUETOOTH", CallEndpoint.TYPE_BLUETOOTH)
+ private val mEarAndSpeakerEndpoints = listOf(mEarpieceEndpoint, mSpeakerEndpoint)
+ private val mEarAndSpeakerAndBtEndpoints =
listOf(mEarpieceEndpoint, mSpeakerEndpoint, mBluetoothEndpoint)
/**
@@ -152,9 +155,71 @@
}
}
+ /**
+ * Verify the [CallEndpoint]s echoed from the platform are re-mapped to the existing
+ * [CallEndpointCompat]s the user received with
+ * [androidx.core.telecom.CallsManager#getAvailableStartingCallEndpoints()]
+ */
+ @SdkSuppress(minSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @SmallTest
+ @Test
+ fun testPlatformEndpointsAreRemappedToExistingEndpoints() {
+ setUpV2Test()
+ runBlocking {
+ val callSession =
+ initCallSession(
+ coroutineContext,
+ CallChannels(),
+ PreCallEndpoints(mEarAndSpeakerAndBtEndpoints.toMutableList(), Channel())
+ )
+
+ val platformEarpiece =
+ CallEndpoint(
+ mEarpieceEndpoint.name,
+ CallEndpoint.TYPE_EARPIECE,
+ getRandomParcelUuid()
+ )
+ assertNotEquals(mEarpieceEndpoint.identifier, platformEarpiece.identifier)
+ val platformSpeaker =
+ CallEndpoint(
+ mSpeakerEndpoint.name,
+ CallEndpoint.TYPE_SPEAKER,
+ getRandomParcelUuid()
+ )
+ assertNotEquals(mSpeakerEndpoint.identifier, platformSpeaker.identifier)
+ val platformBt =
+ CallEndpoint(
+ mBluetoothEndpoint.name,
+ CallEndpoint.TYPE_BLUETOOTH,
+ getRandomParcelUuid()
+ )
+ assertNotEquals(mBluetoothEndpoint.identifier, platformBt.identifier)
+
+ val callSessionUuidRemapping = callSession.mJetpackToPlatformCallEndpoint
+ assertEquals(
+ mEarpieceEndpoint,
+ callSession.toRemappedCallEndpointCompat(platformEarpiece)
+ )
+ assertTrue(callSessionUuidRemapping.containsKey(mEarpieceEndpoint.identifier))
+ assertEquals(platformEarpiece, callSessionUuidRemapping[mEarpieceEndpoint.identifier])
+
+ assertEquals(
+ mSpeakerEndpoint,
+ callSession.toRemappedCallEndpointCompat(platformSpeaker)
+ )
+ assertTrue(callSessionUuidRemapping.containsKey(mSpeakerEndpoint.identifier))
+ assertEquals(platformSpeaker, callSessionUuidRemapping[mSpeakerEndpoint.identifier])
+
+ assertEquals(mBluetoothEndpoint, callSession.toRemappedCallEndpointCompat(platformBt))
+ assertTrue(callSessionUuidRemapping.containsKey(mBluetoothEndpoint.identifier))
+ assertEquals(platformBt, callSessionUuidRemapping[mBluetoothEndpoint.identifier])
+ }
+ }
+
private fun initCallSession(
coroutineContext: CoroutineContext,
- callChannels: CallChannels
+ callChannels: CallChannels,
+ preCallEndpoints: PreCallEndpoints? = null,
): CallSession {
return CallSession(
coroutineContext,
@@ -163,6 +228,7 @@
TestUtils.mOnDisconnectLambda,
TestUtils.mOnSetActiveLambda,
TestUtils.mOnSetInActiveLambda,
+ preCallEndpoints,
callChannels,
{ _, _ -> },
CompletableDeferred(Unit)
@@ -170,11 +236,7 @@
}
fun getCurrentEndpoint(): CallEndpoint {
- return CallEndpoint(
- "EARPIECE",
- CallEndpoint.TYPE_EARPIECE,
- ParcelUuid.fromString(UUID.randomUUID().toString())
- )
+ return CallEndpoint("EARPIECE", CallEndpoint.TYPE_EARPIECE, getRandomParcelUuid())
}
fun getAvailableEndpoint(): List<CallEndpoint> {
@@ -182,4 +244,8 @@
endpoints.add(getCurrentEndpoint())
return endpoints
}
+
+ private fun getRandomParcelUuid(): ParcelUuid {
+ return ParcelUuid.fromString(UUID.randomUUID().toString())
+ }
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallsManagerTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallsManagerTest.kt
index cd56b97..1f93cff 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallsManagerTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/CallsManagerTest.kt
@@ -37,7 +37,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -226,6 +229,104 @@
}
}
+ @SdkSuppress(minSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @SmallTest
+ @Test
+ fun testEndToEndSelectingAStartingEndpointTransactional() {
+ setUpV2Test()
+ runBlocking { assertStartingCallEndpoint(coroutineContext) }
+ }
+
+ @SdkSuppress(minSdkVersion = VERSION_CODES.O)
+ @SmallTest
+ @Test
+ fun testEndToEndSelectingAStartingEndpointBackwardsCompat() {
+ setUpBackwardsCompatTest()
+ runBlocking { assertStartingCallEndpoint(coroutineContext) }
+ }
+
+ private suspend fun assertStartingCallEndpoint(coroutineContext: CoroutineContext) {
+ mCallsManager.registerAppWithTelecom(
+ CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
+ CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
+ )
+ var preCallEndpointsScope: CoroutineScope? = null
+ try {
+ val endpointsFlow = mCallsManager.getAvailableStartingCallEndpoints()
+
+ val initialEndpointsJob = CompletableDeferred<List<CallEndpointCompat>>()
+ CoroutineScope(coroutineContext).launch {
+ preCallEndpointsScope = this
+ Log.i(TAG, "launched initialEndpointsJob")
+ endpointsFlow.collect {
+ it.forEach { endpoint ->
+ Log.i(TAG, "endpointsFlow: collecting endpoint=[$endpoint]")
+ }
+ initialEndpointsJob.complete(it)
+ }
+ }
+ Log.i(TAG, "initialEndpointsJob STARTED")
+ initialEndpointsJob.await()
+ Log.i(TAG, "initialEndpointsJob COMPLETED")
+ val initialEndpoints = initialEndpointsJob.getCompleted()
+ val earpieceEndpoint =
+ initialEndpoints.find { it.type == CallEndpointCompat.TYPE_EARPIECE }
+ if (initialEndpoints.size > 1 && earpieceEndpoint != null) {
+ Log.i(TAG, "found 2 endpoints, including TYPE_EARPIECE")
+ TestUtils.OUTGOING_CALL_ATTRIBUTES.preferredStartingCallEndpoint = earpieceEndpoint
+ mCallsManager.addCall(
+ TestUtils.OUTGOING_CALL_ATTRIBUTES,
+ TestUtils.mOnAnswerLambda,
+ TestUtils.mOnDisconnectLambda,
+ TestUtils.mOnSetActiveLambda,
+ TestUtils.mOnSetInActiveLambda,
+ ) {
+ Log.i(TAG, "addCallWithStartingCallEndpoint: running CallControlScope")
+ launch {
+ val waitUntilEarpieceEndpointJob = CompletableDeferred<CallEndpointCompat>()
+
+ val flowsJob = launch {
+ val earpieceFlow =
+ currentCallEndpoint.filter {
+ Log.i(TAG, "currentCallEndpoint: e=[$it]")
+ it.type == CallEndpointCompat.TYPE_EARPIECE
+ }
+
+ earpieceFlow.collect {
+ Log.i(TAG, "earpieceFlow.collect=[$it]")
+ waitUntilEarpieceEndpointJob.complete(it)
+ }
+ }
+
+ Log.i(TAG, "addCallWithStartingCallEndpoint: before await")
+ waitUntilEarpieceEndpointJob.await()
+ Log.i(TAG, "addCallWithStartingCallEndpoint: after await")
+
+ // at this point, the CallEndpoint has been found
+ val endpoint = waitUntilEarpieceEndpointJob.getCompleted()
+ assertNotNull(endpoint)
+ assertEquals(CallEndpointCompat.TYPE_EARPIECE, endpoint.type)
+
+ // finally, terminate the call
+ disconnect(DisconnectCause(DisconnectCause.LOCAL))
+ // stop collecting flows so the test can end
+ flowsJob.cancel()
+ Log.i(TAG, " flowsJob.cancel()")
+ }
+ }
+ } else {
+ Log.i(
+ TAG,
+ "assertStartingCallEndpoint: " +
+ "endpoints.size=[${initialEndpoints.size}], earpiece=[$earpieceEndpoint]"
+ )
+ preCallEndpointsScope?.cancel()
+ }
+ } finally {
+ preCallEndpointsScope?.cancel()
+ }
+ }
+
suspend fun assertVideoCallStartsWithSpeakerEndpoint() {
assertWithinTimeout_addCall(
CallAttributesCompat(
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
index b810a34..b45aab8 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
@@ -24,11 +24,10 @@
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallControlResult
import androidx.core.telecom.CallsManager
-import androidx.core.telecom.extensions.CallExtensionsScope
+import androidx.core.telecom.extensions.CallExtensionScopeImpl
import androidx.core.telecom.internal.utils.Utils
import androidx.core.telecom.test.utils.BaseTelecomTest
import androidx.core.telecom.test.utils.TestUtils
-import androidx.core.telecom.util.ExperimentalAppActions
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -54,11 +53,10 @@
* or if the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key is present in the call
* extras (pre-U devices). In the future, this will be expanded to be provide more robust testing to
* verify binder functionality as well as supporting the case for auto
- * ([CallsManager.EXTRA_VOIP_API_VERSION]).
+ * ([CallExtensionScopeImpl.EXTRA_VOIP_API_VERSION]).
*/
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@RequiresApi(Build.VERSION_CODES.O)
-@OptIn(ExperimentalAppActions::class)
@RunWith(AndroidJUnit4::class)
class E2ECallExtensionExtrasTests : BaseTelecomTest() {
companion object {
@@ -164,7 +162,7 @@
try {
val call = TestUtils.waitOnInCallServiceToReachXCalls(ics, 1)
Assert.assertNotNull("The returned Call object is <NULL>", call!!)
- val extensions = CallExtensionsScope(mContext, this, call)
+ val extensions = CallExtensionScopeImpl(mContext, this, call)
// Assert the call extra or call property from the details
assertCallExtraOrProperty(extensions, call)
} finally {
@@ -181,9 +179,9 @@
}
/** Helper to assert the call extra or property set on the call coming from Telecom. */
- private suspend fun assertCallExtraOrProperty(extensions: CallExtensionsScope, call: Call) {
+ private suspend fun assertCallExtraOrProperty(extensions: CallExtensionScopeImpl, call: Call) {
val type = extensions.resolveCallExtensionsType()
- assertEquals(CallExtensionsScope.CAPABILITY_EXCHANGE, type)
+ assertEquals(CallExtensionScopeImpl.CAPABILITY_EXCHANGE, type)
// Assert the specifics of the extensions are correct. Note, resolveCallExtensionsType also
// internally assures the details are set properly
val callDetails = call.details!!
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
index 0109014..b46b7ce 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
@@ -21,15 +21,14 @@
import android.os.Build
import android.os.Build.VERSION_CODES
import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallControlResult
import androidx.core.telecom.InCallServiceCompat
-import androidx.core.telecom.extensions.CallExtensionCreator
-import androidx.core.telecom.extensions.CallExtensionsScope
+import androidx.core.telecom.extensions.CallExtensionScope
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.Extensions
import androidx.core.telecom.extensions.Participant
-import androidx.core.telecom.extensions.ParticipantExtension
+import androidx.core.telecom.extensions.ParticipantExtensionImpl
import androidx.core.telecom.extensions.ParticipantExtensionRemote
-import androidx.core.telecom.internal.CapabilityExchangeListenerRemote
import androidx.core.telecom.test.VoipAppWithExtensions.VoipAppWithExtensionsControl
import androidx.core.telecom.test.VoipAppWithExtensions.VoipAppWithExtensionsControlLocal
import androidx.core.telecom.test.utils.BaseTelecomTest
@@ -42,14 +41,10 @@
import androidx.test.rule.GrantPermissionRule
import androidx.test.rule.ServiceTestRule
import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.coroutines.yield
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Assume.assumeTrue
@@ -78,11 +73,11 @@
private val CAPABILITY_PARTICIPANT_WITH_ACTIONS =
createCapability(
id = Extensions.PARTICIPANT,
- version = ParticipantExtension.VERSION,
+ version = ParticipantExtensionImpl.VERSION,
actions =
setOf(
- ParticipantExtension.RAISE_HAND_ACTION,
- ParticipantExtension.KICK_PARTICIPANT_ACTION
+ ParticipantExtensionImpl.RAISE_HAND_ACTION,
+ ParticipantExtensionImpl.KICK_PARTICIPANT_ACTION
)
)
@@ -110,7 +105,7 @@
}
}
- internal class CachedParticipants(scope: CallExtensionsScope) {
+ internal class CachedParticipants(scope: CallExtensionScope) {
private val participantState = MutableStateFlow<Set<Participant>>(emptySet())
private val activeParticipantState = MutableStateFlow<Participant?>(null)
val extension =
@@ -137,10 +132,10 @@
}
internal class CachedRaisedHands(extension: ParticipantExtensionRemote) {
- private val raisedHands = MutableStateFlow<Set<Participant>>(emptySet())
+ private val raisedHands = MutableStateFlow<List<Participant>>(emptyList())
val action = extension.addRaiseHandAction(raisedHands::emit)
- suspend fun waitForRaisedHands(expected: Set<Participant>) {
+ suspend fun waitForRaisedHands(expected: List<Participant>) {
val result =
withTimeoutOrNull(ICS_EXTENSION_UPDATE_TIMEOUT_MS) {
raisedHands.first { it == expected }
@@ -207,7 +202,9 @@
TestUtils.waitOnInCallServiceToReachXCalls(ics, 1)
try {
// Send updateParticipants to ensure there is no error/exception
- voipAppControl.updateParticipants(listOf(TestUtils.getDefaultParticipant()))
+ voipAppControl.updateParticipants(
+ listOf(TestUtils.getDefaultParticipantParcelable())
+ )
} catch (e: Exception) {
fail("calling extension methods should not result in any exceptions: Exception: $e")
}
@@ -215,36 +212,43 @@
}
/**
- * Create a new VOIP call and use [InCallServiceCompat.connectExtensions] in the ICS to connect
- * to the VOIP call. Once complete, use the [CallExtensionsScope.registerExtension] method to
- * register an unknown extension and ensure we get the correct null indication.
+ * Create a VOIP call with a participants extension and no actions Verify that all of the
+ * participant extension functions work as expected.
*/
@LargeTest
@Test(timeout = 10000)
- fun testIcsExtensionsCreationUnknownCapability() = runBlocking {
+ fun testVoipAndIcsWithParticipants() = runBlocking {
usingIcs { ics ->
val voipAppControl = bindToVoipAppWithExtensions()
+ val callback = TestCallCallbackListener(this)
+ voipAppControl.setCallback(callback)
createAndVerifyVoipCall(
voipAppControl,
- listOf(CAPABILITY_PARTICIPANT_WITH_ACTIONS),
+ listOf(getParticipantCapability(emptySet())),
parameters.direction
)
val call = TestUtils.waitOnInCallServiceToReachXCalls(ics, 1)!!
var hasConnected = false
- // Manually connect extensions here to exercise the CallExtensionsScope class
with(ics) {
connectExtensions(call) {
- // Create an extension that the VOIP app does not know about and ensure that
- // we receive a null response during negotiation so we can notify the ICS of the
- // state of that extension
- val nonexistentRemote = registerInvalidExtension(this)
+ val participants = CachedParticipants(this)
onConnected {
hasConnected = true
- assertNull(
- "Connection to remote should be null for features with no VOIP support",
- nonexistentRemote.await()
+ // Wait for initial state
+ participants.waitForParticipants(emptySet())
+ participants.waitForActiveParticipant(null)
+ // Test VOIP -> ICS connection by updating state
+ val currentParticipants = TestUtils.generateParticipants(2)
+ voipAppControl.updateParticipants(
+ currentParticipants.map { it.toParticipantParcelable() }
)
+ participants.waitForParticipants(currentParticipants.toSet())
+ voipAppControl.updateActiveParticipant(
+ currentParticipants[0].toParticipantParcelable()
+ )
+ participants.waitForActiveParticipant(currentParticipants[0])
+
call.disconnect()
}
}
@@ -255,11 +259,11 @@
/**
* Create a VOIP call with a participants extension and attach participant Call extensions.
- * Verify that all of the participant extension functions work as expected.
+ * Verify raised hands functionality works as expected
*/
@LargeTest
@Test(timeout = 10000)
- fun testVoipAndIcsWithParticipants() = runBlocking {
+ fun testVoipAndIcsRaisedHands() = runBlocking {
usingIcs { ics ->
val voipAppControl = bindToVoipAppWithExtensions()
val callback = TestCallCallbackListener(this)
@@ -267,7 +271,9 @@
val voipCallId =
createAndVerifyVoipCall(
voipAppControl,
- listOf(CAPABILITY_PARTICIPANT_WITH_ACTIONS),
+ listOf(
+ getParticipantCapability(setOf(ParticipantExtensionImpl.RAISE_HAND_ACTION))
+ ),
parameters.direction
)
@@ -277,33 +283,83 @@
connectExtensions(call) {
val participants = CachedParticipants(this)
val raiseHandAction = CachedRaisedHands(participants.extension)
- val kickParticipantAction = participants.extension.addKickParticipantAction()
onConnected {
hasConnected = true
- // Test VOIP -> ICS connection by updating state
- participants.waitForParticipants(emptySet())
- participants.waitForActiveParticipant(null)
+ val currentParticipants = TestUtils.generateParticipants(3)
+ voipAppControl.updateParticipants(
+ currentParticipants.map { it.toParticipantParcelable() }
+ )
+ participants.waitForParticipants(currentParticipants.toSet())
- voipAppControl.updateParticipants(listOf(TestUtils.getDefaultParticipant()))
- participants.waitForParticipants(setOf(TestUtils.getDefaultParticipant()))
-
- voipAppControl.updateActiveParticipant(TestUtils.getDefaultParticipant())
- participants.waitForActiveParticipant(TestUtils.getDefaultParticipant())
-
- voipAppControl.updateRaisedHands(listOf(TestUtils.getDefaultParticipant()))
- raiseHandAction.waitForRaisedHands(setOf(TestUtils.getDefaultParticipant()))
+ // Reverse the ordering of the list to ensure that ordering is maintained
+ // across the binder.
+ val raisedHands = currentParticipants.reversed()
+ voipAppControl.updateRaisedHands(
+ raisedHands.map { it.toParticipantParcelable() }
+ )
+ raiseHandAction.waitForRaisedHands(raisedHands)
+ val raisedHandsAndInvalid = ArrayList(raisedHands)
+ raisedHandsAndInvalid.add(Participant("INVALID", "INVALID"))
+ voipAppControl.updateRaisedHands(
+ raisedHandsAndInvalid.map { it.toParticipantParcelable() }
+ )
+ // action should not contain the invalid Participant
+ raiseHandAction.waitForRaisedHands(raisedHands)
// Test ICS -> VOIP connection by sending events
raiseHandAction.action.requestRaisedHandStateChange(true)
callback.waitForRaiseHandState(voipCallId, true)
- kickParticipantAction.requestKickParticipant(
- TestUtils.getDefaultParticipant()
+ call.disconnect()
+ }
+ }
+ }
+ assertTrue("onConnected never received", hasConnected)
+ }
+ }
+
+ /**
+ * Create a VOIP call with a participants extension and attach participant Call extensions.
+ * Verify kick participant functionality works as expected
+ */
+ @LargeTest
+ @Test(timeout = 10000)
+ fun testVoipAndIcsKickParticipant() = runBlocking {
+ usingIcs { ics ->
+ val voipAppControl = bindToVoipAppWithExtensions()
+ val callback = TestCallCallbackListener(this)
+ voipAppControl.setCallback(callback)
+ val voipCallId =
+ createAndVerifyVoipCall(
+ voipAppControl,
+ listOf(
+ getParticipantCapability(
+ setOf(ParticipantExtensionImpl.KICK_PARTICIPANT_ACTION)
)
- callback.waitForKickParticipant(
- voipCallId,
- TestUtils.getDefaultParticipant()
+ ),
+ parameters.direction
+ )
+
+ val call = TestUtils.waitOnInCallServiceToReachXCalls(ics, 1)!!
+ var hasConnected = false
+ with(ics) {
+ connectExtensions(call) {
+ val participants = CachedParticipants(this)
+ val kickParticipant = participants.extension.addKickParticipantAction()
+ onConnected {
+ hasConnected = true
+ val currentParticipants = TestUtils.generateParticipants(3)
+ voipAppControl.updateParticipants(
+ currentParticipants.map { it.toParticipantParcelable() }
)
+ participants.waitForParticipants(currentParticipants.toSet())
+ // Kick a valid participant
+ assertEquals(
+ "Never received response to kickParticipant request",
+ CallControlResult.Success(),
+ kickParticipant.requestKickParticipant(currentParticipants[0])
+ )
+ callback.waitForKickParticipant(voipCallId, currentParticipants[0])
call.disconnect()
}
@@ -318,22 +374,6 @@
* Helpers
* =========================================================================================
*/
- private fun registerInvalidExtension(
- scope: CallExtensionsScope,
- ): CompletableDeferred<CapabilityExchangeListenerRemote?> {
- val deferredVal = CompletableDeferred<CapabilityExchangeListenerRemote?>()
- scope.registerExtension {
- CallExtensionCreator(
- extensionCapability =
- createCapability(id = 8675309, version = 42, actions = emptySet()),
- onExchangeComplete = { capability, remote ->
- assertNull("Expected null capability", capability)
- deferredVal.complete(remote)
- }
- )
- }
- return deferredVal
- }
/**
* Creates a VOIP call using the specified capabilities and direction and then verifies that it
@@ -385,19 +425,11 @@
return ITestAppControl.Stub.asInterface(voipAppServiceRule.bindService(serviceIntent))
}
- /**
- * Tests the value returned from the [supplier] using [predicate] and retries until the criteria
- * is met. Retries every second for up to 5 seconds.
- */
- private suspend fun <R> waitForResult(predicate: (R?) -> Boolean, supplier: () -> R): R? {
- var result = supplier()
- withTimeoutOrNull(5000) {
- while (!predicate(result)) {
- yield()
- delay(1000)
- result = supplier()
- }
- }
- return result
+ private fun getParticipantCapability(actions: Set<Int>): Capability {
+ return createCapability(
+ id = Extensions.PARTICIPANT,
+ version = ParticipantExtensionImpl.VERSION,
+ actions = actions
+ )
}
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt
index db5bbd7..8e5dfbe 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt
@@ -255,6 +255,8 @@
TestUtils.mOnSetActiveLambda,
TestUtils.mOnSetInActiveLambda,
TestUtils.mOnEventLambda,
+ null,
+ null,
CompletableDeferred()
)
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt
new file mode 100644
index 0000000..bd4a102
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/PreCallEndpointsTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallEndpointCompat
+import androidx.core.telecom.internal.PreCallEndpoints
+import androidx.test.filters.SmallTest
+import kotlinx.coroutines.channels.Channel
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@RequiresApi(Build.VERSION_CODES.O)
+class PreCallEndpointsTest {
+ private val defaultEarpiece = CallEndpointCompat("E", CallEndpointCompat.TYPE_EARPIECE)
+ private val defaultSpeaker = CallEndpointCompat("S", CallEndpointCompat.TYPE_SPEAKER)
+ private val defaultBluetooth = CallEndpointCompat("B", CallEndpointCompat.TYPE_BLUETOOTH)
+
+ @SmallTest
+ @Test
+ fun testInitialValues() {
+ val initEndpoints = mutableListOf(defaultEarpiece, defaultSpeaker, defaultBluetooth)
+ val currentPreCallEndpoints = PreCallEndpoints(initEndpoints, Channel())
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultEarpiece))
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultSpeaker))
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultBluetooth))
+ assertTrue(currentPreCallEndpoints.mNonBluetoothEndpoints.contains(defaultEarpiece.type))
+ assertTrue(currentPreCallEndpoints.mNonBluetoothEndpoints.contains(defaultSpeaker.type))
+ assertTrue(currentPreCallEndpoints.mBluetoothEndpoints.contains(defaultBluetooth.name))
+ }
+
+ @SmallTest
+ @Test
+ fun testEndpointsAddedWithNewEndpoint() {
+ val initEndpoints = mutableListOf(defaultEarpiece)
+ val currentPreCallEndpoints = PreCallEndpoints(initEndpoints, Channel())
+
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultEarpiece))
+ assertFalse(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultSpeaker))
+
+ val res = currentPreCallEndpoints.maybeAddCallEndpoint(defaultSpeaker)
+ assertEquals(PreCallEndpoints.START_TRACKING_NEW_ENDPOINT, res)
+ }
+
+ @SmallTest
+ @Test
+ fun testEndpointsAddedWithNoNewEndpoints() {
+ val initEndpoints = mutableListOf(defaultEarpiece, defaultSpeaker)
+ val currentPreCallEndpoints = PreCallEndpoints(initEndpoints, Channel())
+
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultEarpiece))
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultSpeaker))
+
+ val res = currentPreCallEndpoints.maybeAddCallEndpoint(defaultSpeaker)
+ assertEquals(PreCallEndpoints.ALREADY_TRACKING_ENDPOINT, res)
+ }
+
+ @SmallTest
+ @Test
+ fun testEndpointsRemovedWithUntrackedEndpoint() {
+ val initEndpoints = mutableListOf(defaultEarpiece)
+ val currentPreCallEndpoints = PreCallEndpoints(initEndpoints, Channel())
+
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultEarpiece))
+ assertFalse(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultSpeaker))
+
+ val res = currentPreCallEndpoints.maybeRemoveCallEndpoint(defaultSpeaker)
+ assertEquals(PreCallEndpoints.NOT_TRACKING_REMOVED_ENDPOINT, res)
+ }
+
+ @SmallTest
+ @Test
+ fun testEndpointsRemovedWithTrackedEndpoint() {
+ val initEndpoints = mutableListOf(defaultEarpiece, defaultSpeaker)
+ val currentPreCallEndpoints = PreCallEndpoints(initEndpoints, Channel())
+
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultEarpiece))
+ assertTrue(currentPreCallEndpoints.isCallEndpointBeingTracked(defaultSpeaker))
+
+ val res = currentPreCallEndpoints.maybeRemoveCallEndpoint(defaultSpeaker)
+ assertEquals(PreCallEndpoints.STOP_TRACKING_REMOVED_ENDPOINT, res)
+ }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
index f030f12..18b590d 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
@@ -30,6 +30,8 @@
import androidx.core.telecom.CallsManager
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.Participant
+import androidx.core.telecom.extensions.ParticipantParcelable
+import androidx.core.telecom.extensions.toParticipant
import androidx.core.telecom.test.ITestAppControl
import androidx.core.telecom.test.ITestAppControlCallback
import androidx.core.telecom.test.utils.TestUtils
@@ -54,7 +56,7 @@
private var mCallback: ITestAppControlCallback? = null
private var participantsFlow: MutableStateFlow<Set<Participant>> = MutableStateFlow(emptySet())
private var activeParticipantFlow: MutableStateFlow<Participant?> = MutableStateFlow(null)
- private var raisedHandsFlow: MutableStateFlow<Set<Participant>> = MutableStateFlow(emptySet())
+ private var raisedHandsFlow: MutableStateFlow<List<Participant>> = MutableStateFlow(emptyList())
companion object {
val TAG = VoipAppWithExtensionsControl::class.java.simpleName
@@ -110,7 +112,7 @@
raisedHandsFlow
.onEach {
TestUtils.printParticipants(it, "VoIP raised hands")
- raiseHandStateUpdater!!.updateRaisedHands(it)
+ raiseHandStateUpdater?.updateRaisedHands(it)
}
.launchIn(this)
activeParticipantFlow
@@ -129,16 +131,16 @@
return id
}
- override fun updateParticipants(setOfParticipants: List<Participant>) {
- participantsFlow.value = setOfParticipants.toSet()
+ override fun updateParticipants(setOfParticipants: List<ParticipantParcelable>) {
+ participantsFlow.value = setOfParticipants.map { it.toParticipant() }.toSet()
}
- override fun updateActiveParticipant(participant: Participant?) {
- activeParticipantFlow.value = participant
+ override fun updateActiveParticipant(participant: ParticipantParcelable?) {
+ activeParticipantFlow.value = participant?.toParticipant()
}
- override fun updateRaisedHands(raisedHandsParticipants: List<Participant>) {
- raisedHandsFlow.value = raisedHandsParticipants.toSet()
+ override fun updateRaisedHands(raisedHandsParticipants: List<ParticipantParcelable>) {
+ raisedHandsFlow.value = raisedHandsParticipants.map { it.toParticipant() }
}
}
@@ -157,7 +159,7 @@
mCallback = null
participantsFlow.value = emptySet()
activeParticipantFlow.value = null
- raisedHandsFlow.value = emptySet()
+ raisedHandsFlow.value = emptyList()
return false
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
index ceea6e2..fc9d6f5 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
@@ -18,6 +18,7 @@
import android.os.Build
import android.telecom.DisconnectCause
+import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallControlScope
@@ -26,6 +27,7 @@
import androidx.core.telecom.extensions.ExtensionInitializationScope
import androidx.core.telecom.extensions.Extensions
import androidx.core.telecom.extensions.ParticipantExtension
+import androidx.core.telecom.extensions.ParticipantExtensionImpl
import androidx.core.telecom.extensions.RaiseHandState
import androidx.core.telecom.test.ITestAppControlCallback
import androidx.core.telecom.util.ExperimentalAppActions
@@ -37,6 +39,10 @@
private val callback: ITestAppControlCallback?,
private val capabilities: List<Capability>
) {
+ companion object {
+ private const val TAG = "VoipCall"
+ }
+
private lateinit var callId: String
// Participant state updaters
internal var participantStateUpdater: ParticipantExtension? = null
@@ -50,6 +56,7 @@
onSetInactive: suspend () -> Unit,
init: CallControlScope.() -> Unit
) {
+ Log.i(TAG, "addCall: capabilities=$capabilities")
callsManager.addCallWithExtensions(
callAttributes,
onAnswer,
@@ -79,13 +86,15 @@
private fun ParticipantExtension.initializeActions(capability: Capability) {
for (action in capability.supportedActions) {
when (action) {
- ParticipantExtension.RAISE_HAND_ACTION -> {
+ ParticipantExtensionImpl.RAISE_HAND_ACTION -> {
raiseHandStateUpdater = addRaiseHandSupport {
callback?.raiseHandStateAction(callId, it)
}
}
- ParticipantExtension.KICK_PARTICIPANT_ACTION -> {
- addKickParticipantSupport { callback?.kickParticipantAction(callId, it) }
+ ParticipantExtensionImpl.KICK_PARTICIPANT_ACTION -> {
+ addKickParticipantSupport {
+ callback?.kickParticipantAction(callId, it.toParticipantParcelable())
+ }
}
}
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt
index 91f245e..6e23441 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestInCallService.kt
@@ -61,13 +61,13 @@
mCalls.remove(call)
}
- override fun onBind(intent: Intent): IBinder? {
- if (intent.action == SERVICE_INTERFACE) {
+ override fun onBind(intent: Intent?): IBinder? {
+ if (intent?.action == SERVICE_INTERFACE) {
Log.i(LOG_TAG, "InCallService bound from telecom")
mTelecomBoundFlow.tryEmit(true)
return super.onBind(intent)
}
- Log.i(LOG_TAG, "InCallService bound by ${intent.component}")
+ Log.i(LOG_TAG, "InCallService bound by ${intent?.component}")
return localBinder
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
index f2b9829..79b9d8f 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
@@ -22,6 +22,7 @@
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.Bundle
+import android.os.ParcelUuid
import android.os.UserHandle
import android.os.UserManager
import android.telecom.Call
@@ -31,11 +32,14 @@
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.extensions.Participant
+import androidx.core.telecom.extensions.ParticipantParcelable
+import androidx.core.telecom.extensions.toParticipant
import androidx.core.telecom.internal.utils.BuildVersionAdapter
import androidx.core.telecom.test.ITestAppControlCallback
import androidx.core.telecom.util.ExperimentalAppActions
import androidx.test.platform.app.InstrumentationRegistry
import java.io.FileInputStream
+import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
@@ -289,6 +293,10 @@
Log.i(LOG_TAG, "defaultDialer=[${getDefaultDialer()}]")
}
+ fun generateRandomUuid(): ParcelUuid {
+ return ParcelUuid.fromString(UUID.randomUUID().toString())
+ }
+
@OptIn(ExperimentalAppActions::class)
@Suppress("deprecation")
internal suspend fun waitOnInCallServiceToReachXCalls(
@@ -375,20 +383,31 @@
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
}
+ /** Generate a List of [Participant]s, where each ID corresponds to a range of 1 to [num] */
@ExperimentalAppActions
- fun getDefaultParticipant(): Participant {
- val p = Participant()
- p.id = 123
- p.name = "Gemini"
- p.speakerIconUri = null
- return p
+ fun generateParticipants(num: Int): List<Participant> {
+ val participants = ArrayList<Participant>()
+ for (i in 1..num) {
+ participants.add(Participant(i.toString(), "part-$i"))
+ }
+ return participants
}
@ExperimentalAppActions
- fun printParticipants(participants: Set<Participant>, tag: String) {
+ fun getDefaultParticipant(): Participant {
+ return Participant("123", "Gemini")
+ }
+
+ @ExperimentalAppActions
+ fun getDefaultParticipantParcelable(): ParticipantParcelable {
+ return getDefaultParticipant().toParticipantParcelable()
+ }
+
+ @ExperimentalAppActions
+ fun printParticipants(participants: Collection<Participant>, tag: String) {
Log.i(LOG_TAG, tag + ": printParticipants: set size=${participants.size}")
for (v in participants) {
- Log.i(LOG_TAG, "id=${v.id} name=${v.name}, uri=${v.speakerIconUri}")
+ Log.i(LOG_TAG, "\t $v")
}
}
}
@@ -405,9 +424,9 @@
scope.launch { raisedHandFlow.emit(Pair(callId, isHandRaised)) }
}
- override fun kickParticipantAction(callId: String?, participant: Participant?) {
+ override fun kickParticipantAction(callId: String?, participant: ParticipantParcelable?) {
if (callId == null) return
- scope.launch { kickParticipantFlow.emit(Pair(callId, participant)) }
+ scope.launch { kickParticipantFlow.emit(Pair(callId, participant?.toParticipant())) }
}
suspend fun waitForRaiseHandState(callId: String, expectedState: Boolean) {
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantActions.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantActions.aidl
index 945ebdc..0b7d068 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantActions.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantActions.aidl
@@ -16,7 +16,7 @@
package androidx.core.telecom.extensions;
-import androidx.core.telecom.extensions.Participant;
+import androidx.core.telecom.extensions.ParticipantParcelable;
import androidx.core.telecom.extensions.IActionsResultCallback;
// ICS Client -> VOIP App
@@ -25,5 +25,5 @@
oneway interface IParticipantActions {
// V1
void setHandRaised(in boolean handRaisedState, in IActionsResultCallback cb) = 0;
- void kickParticipant(in Participant participant, in IActionsResultCallback cb) = 1;
+ void kickParticipant(in String participantId, in IActionsResultCallback cb) = 1;
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantStateListener.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantStateListener.aidl
index f5f3ccf..bdfb511 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantStateListener.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/IParticipantStateListener.aidl
@@ -17,7 +17,7 @@
package androidx.core.telecom.extensions;
import java.util.List;
-import androidx.core.telecom.extensions.Participant;
+import androidx.core.telecom.extensions.ParticipantParcelable;
import androidx.core.telecom.extensions.IParticipantActions;
import androidx.core.telecom.extensions.IActionsResultCallback;
@@ -26,10 +26,10 @@
@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
oneway interface IParticipantStateListener {
// V1 - Built-in states provided as part of handling basic participant support
- void updateParticipants(in Participant[] participants) = 0;
- void updateActiveParticipant(in int activeParticipant) = 1;
+ void updateParticipants(in ParticipantParcelable[] participants) = 0;
+ void updateActiveParticipant(in String activeParticipantId) = 1;
// V1 - Updates for supported actions
- void updateRaisedHandsAction(in int[] participants) = 2;
+ void updateRaisedHandsAction(in String[] participantIds) = 2;
// Finish synchronization and start listening for actions updates
void finishSync(in IParticipantActions cb) = 3;
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/Participant.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ParticipantParcelable.aidl
similarity index 81%
rename from core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/Participant.aidl
rename to core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ParticipantParcelable.aidl
index 5a453e1..207291a 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/Participant.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ParticipantParcelable.aidl
@@ -19,11 +19,9 @@
@JavaDerive(equals = true, toString = true)
@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
-parcelable Participant {
- // ID of the participant (must be unique for each)
- int id;
- // Participant name
- String name;
- // Call icon associated with the participant
- Uri speakerIconUri;
+parcelable ParticipantParcelable {
+ // ID of the participant (must be unique for each call and NOT reused)
+ String id;
+ // User visible Participant name
+ CharSequence name;
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
index bbf7bf6..999fa0f 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
@@ -1,7 +1,7 @@
package androidx.core.telecom.test;
import androidx.core.telecom.extensions.Capability;
-import androidx.core.telecom.extensions.Participant;
+import androidx.core.telecom.extensions.ParticipantParcelable;
import androidx.core.telecom.test.ITestAppControlCallback;
// NOTE: only supports one voip call at a time right now + suspend functions are not supported by
@@ -10,7 +10,7 @@
interface ITestAppControl {
void setCallback(in ITestAppControlCallback callback);
String addCall(in List<Capability> capabilities, boolean isOutgoing);
- void updateParticipants(in List<Participant> participants);
- void updateActiveParticipant(in Participant participant);
- void updateRaisedHands(in List<Participant> raisedHandsParticipants);
+ void updateParticipants(in List<ParticipantParcelable> participants);
+ void updateActiveParticipant(in ParticipantParcelable participant);
+ void updateRaisedHands(in List<ParticipantParcelable> raisedHandsParticipants);
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
index 803c74ee..d9bc951 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
@@ -1,9 +1,9 @@
package androidx.core.telecom.test;
-import androidx.core.telecom.extensions.Participant;
+import androidx.core.telecom.extensions.ParticipantParcelable;
@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
oneway interface ITestAppControlCallback {
void raiseHandStateAction(in String callId, boolean isHandRaised);
- void kickParticipantAction(in String callId, in Participant participant);
+ void kickParticipantAction(in String callId, in ParticipantParcelable participant);
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
index d53ddbc..865dcdd 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
@@ -35,14 +35,18 @@
* @param callType Information related to data being transmitted (voice, video, etc. )
* @param callCapabilities Allows a package to opt into capabilities on the telecom side, on a
* per-call basis
+ * @param preferredStartingCallEndpoint allows clients to specify a [CallEndpointCompat] to start a
+ * new call on. The [preferredStartingCallEndpoint] should be a value returned from
+ * [CallsManager.getAvailableStartingCallEndpoints]. Once the call is started, Core-Telecom will
+ * switch to the [preferredStartingCallEndpoint] before running the [CallControlScope].
*/
-public class CallAttributesCompat
-constructor(
+public class CallAttributesCompat(
public val displayName: CharSequence,
public val address: Uri,
@Direction public val direction: Int,
@CallType public val callType: Int = CALL_TYPE_AUDIO_CALL,
- @CallCapability public val callCapabilities: Int = SUPPORTS_SET_INACTIVE
+ @CallCapability public val callCapabilities: Int = SUPPORTS_SET_INACTIVE,
+ public var preferredStartingCallEndpoint: CallEndpointCompat? = null
) {
internal var mHandle: PhoneAccountHandle? = null
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt
index 408ef98..d884c4d 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallEndpointCompat.kt
@@ -39,7 +39,7 @@
public val name: CharSequence,
public val type: Int,
public val identifier: ParcelUuid
-) {
+) : Comparable<CallEndpointCompat> {
internal var mMackAddress: String = "-1"
override fun toString(): String {
@@ -49,6 +49,29 @@
"identifier=[$identifier])"
}
+ /**
+ * Compares this [CallEndpointCompat] to the other [CallEndpointCompat] for order. Returns a
+ * positive number if this type rank is greater than the other value. Returns a negative number
+ * if this type rank is less than the other value. Sort the CallEndpoint by type. Ranking them
+ * by:
+ * 1. TYPE_WIRED_HEADSET
+ * 2. TYPE_BLUETOOTH
+ * 3. TYPE_SPEAKER
+ * 4. TYPE_EARPIECE
+ * 5. TYPE_STREAMING
+ * 6. TYPE_UNKNOWN If two endpoints have the same type, the name is compared to determine the
+ * value.
+ */
+ override fun compareTo(other: CallEndpointCompat): Int {
+ // sort by type
+ val res = this.getTypeRank().compareTo(other.getTypeRank())
+ if (res != 0) {
+ return res
+ }
+ // break ties using alphabetic order
+ return this.name.toString().compareTo(other.name.toString())
+ }
+
override fun equals(other: Any?): Boolean {
return other is CallEndpointCompat &&
name == other.name &&
@@ -107,4 +130,20 @@
) : this(name, type) {
mMackAddress = address
}
+
+ /** Internal helper to determine if this [CallEndpointCompat] is EndpointType#TYPE_BLUETOOTH */
+ internal fun isBluetoothType(): Boolean {
+ return type == TYPE_BLUETOOTH
+ }
+
+ private fun getTypeRank(): Int {
+ return when (this.type) {
+ TYPE_WIRED_HEADSET -> return 0
+ TYPE_BLUETOOTH -> return 1
+ TYPE_SPEAKER -> return 2
+ TYPE_EARPIECE -> return 3
+ TYPE_STREAMING -> return 4
+ else -> 5
+ }
+ }
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index 0cf8388..71ef177 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -18,6 +18,9 @@
import android.content.ComponentName
import android.content.Context
+import android.media.AudioDeviceCallback
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.os.OutcomeReceiver
@@ -37,12 +40,17 @@
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.core.telecom.CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL
+import androidx.core.telecom.extensions.CallsManagerExtensions
import androidx.core.telecom.extensions.ExtensionInitializationScope
+import androidx.core.telecom.extensions.ExtensionInitializationScopeImpl
import androidx.core.telecom.internal.AddCallResult
import androidx.core.telecom.internal.CallChannels
import androidx.core.telecom.internal.CallSession
import androidx.core.telecom.internal.CallSessionLegacy
import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.core.telecom.internal.PreCallEndpoints
+import androidx.core.telecom.internal.utils.AudioManagerUtil.Companion.getAvailableAudioDevices
+import androidx.core.telecom.internal.utils.EndpointUtils.Companion.getEndpointsFromAudioDeviceInfo
import androidx.core.telecom.internal.utils.Utils
import androidx.core.telecom.internal.utils.Utils.Companion.remapJetpackCapsToPlatformCaps
import androidx.core.telecom.util.ExperimentalAppActions
@@ -54,8 +62,11 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@@ -73,7 +84,7 @@
* descriptions.
*/
@RequiresApi(VERSION_CODES.O)
-public class CallsManager public constructor(context: Context) {
+public class CallsManager(context: Context) : CallsManagerExtensions {
private val mContext: Context = context
private var mPhoneAccount: PhoneAccount? = null
private val mTelecomManager: TelecomManager =
@@ -83,6 +94,9 @@
// A single declared constant for a direct [Executor], since the coroutines primitives we invoke
// from the associated callbacks will perform their own dispatch as needed.
private val mDirectExecutor = Executor { it.run() }
+ // This list is modified in [getAvailableStartingCallEndpoints] and used to store the
+ // mappings of jetpack call endpoint UUIDs
+ private var mPreCallEndpointsList: MutableList<PreCallEndpoints> = mutableListOf()
public companion object {
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -279,67 +293,40 @@
* actions that go beyond the scope of a call, such as information about meeting participants
* and icons.
*
- * Supported Extensions:
- * - The ability to show meeting participants and information about those participants using
- * [ExtensionInitializationScope.addParticipantExtension]
- *
- * For example, using Participants as an example of extensions:
- * ```
- * scope.launch {
- * mCallsManager.addCallWithExtensions(attributes,
- * onAnswerLambda,
- * onDisconnectLambda,
- * onSetActiveLambda,
- * onSetInactiveLambda) {
- * // Initialize extensions ...
- * // Example: add participants support & associated actions
- * val participantExtension = addParticipantExtension(initialParticipants)
- * val raiseHandState = participantExtension.addRaiseHandSupport(
- * initialRaisedHands) { onHandRaisedStateChanged ->
- * // handle raised hand state changed
- * }
- * participantExtension.addKickParticipantSupport {
- * participant ->
- * // handle kicking the requested participant
- * }
- * // Call has been set up, perform in-call actions
- * onCall {
- * // Example: collect call state updates
- * callStateFlow.onEach { newState ->
- * // handle call state updates
- * }.launchIn(this)
- * // update participant extensions
- * participantsFlow.onEach { newParticipants ->
- * participantExtension.updateParticipants(newParticipants)
- * }.launchIn(this)
- * raisedHandsFlow.onEach { newRaisedHands ->
- * raiseHandState.updateRaisedHands(newRaisedHands)
- * }.launchIn(this)
- * }
- * }
- * }
- * }
- * ```
- *
+ * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
+ * @param onAnswer where callType is the audio/video state the call should be answered as.
+ * Telecom is informing your VoIP application to answer an incoming call and set it to active.
+ * Telecom is requesting this on behalf of an system service (e.g. Automotive service) or a
+ * device (e.g. Wearable).
+ * @param onDisconnect where disconnectCause represents the cause for disconnecting the call.
+ * Telecom is informing your VoIP application to disconnect the incoming call. Telecom is
+ * requesting this on behalf of an system service (e.g. Automotive service) or a device (e.g.
+ * Wearable).
+ * @param onSetActive Telecom is informing your VoIP application to set the call active. Telecom
+ * is requesting this on behalf of an system service (e.g. Automotive service) or a device
+ * (e.g. Wearable).
+ * @param onSetInactive Telecom is informing your VoIP application to set the call inactive.
+ * This is the same as holding a call for two endpoints but can be extended to setting a
+ * meeting inactive. Telecom is requesting this on behalf of an system service (e.g.
+ * Automotive service) or a device (e.g.Wearable). Note: Your app must stop using the
+ * microphone and playing incoming media when returning.
* @param init The scope used to first initialize Extensions that will be used when the call is
* first notified to the platform and UX surfaces. Once the call is set up, the user's
* implementation of [ExtensionInitializationScope.onCall] will be called.
- * @see CallsManager.addCall
+ * @see CallsManagerExtensions.addCallWithExtensions
*/
- // TODO: Refactor to Public API
- @RequiresApi(VERSION_CODES.O)
@ExperimentalAppActions
- internal suspend fun addCallWithExtensions(
+ override suspend fun addCallWithExtensions(
callAttributes: CallAttributesCompat,
onAnswer: suspend (callType: @CallAttributesCompat.Companion.CallType Int) -> Unit,
onDisconnect: suspend (disconnectCause: DisconnectCause) -> Unit,
onSetActive: suspend () -> Unit,
onSetInactive: suspend () -> Unit,
init: suspend ExtensionInitializationScope.() -> Unit
- ) = coroutineScope {
+ ): Unit = coroutineScope {
Log.v(TAG, "addCall: begin")
val eventFlow = MutableSharedFlow<CallEvent>()
- val scope = ExtensionInitializationScope()
+ val scope = ExtensionInitializationScopeImpl()
scope.init()
val extensionJob = launch {
Log.d(TAG, "addCall: connecting extensions")
@@ -359,10 +346,70 @@
}
// Ensure that when the call ends, we also cancel any ongoing coroutines/flows as part of
// extension work
+ Log.d(TAG, "addCall: cancelling extension job")
extensionJob.cancelAndJoin()
}
/**
+ * Fetch the current available call audio endpoints that can be used for a new call session. The
+ * callback flow will be continuously updated until the call session is established via
+ * [addCall]. Once [addCall] is invoked with a
+ * [CallAttributesCompat.preferredStartingCallEndpoint], the callback containing the
+ * [CallEndpointCompat] will be forced closed on behalf of the client. If the flow is canceled
+ * before adding the call, the [CallAttributesCompat.preferredStartingCallEndpoint] will be
+ * voided. If a call session isn't started, the flow should be cleaned up client-side by calling
+ * cancel() from the same [kotlinx.coroutines.CoroutineScope] the [callbackFlow] is collecting
+ * in.
+ *
+ * Note: The endpoints emitted will be sorted by the [CallEndpointCompat.type] . See
+ * [CallEndpointCompat.compareTo] for the ordering. The first element in the list will be the
+ * recommended call endpoint to default to for the user.
+ *
+ * @return a flow of [CallEndpointCompat]s that can be used for a new call session
+ */
+ public fun getAvailableStartingCallEndpoints(): Flow<List<CallEndpointCompat>> = callbackFlow {
+ val audioManager = mContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ // [AudioDeviceInfo] <-- AudioManager / platform
+ val initialAudioDevices = getAvailableAudioDevices(audioManager)
+ // [AudioDeviceInfo] --> [CallEndpoints]
+ val initialEndpoints = getEndpointsFromAudioDeviceInfo(mContext, initialAudioDevices)
+
+ val preCallEndpoints = PreCallEndpoints(initialEndpoints.toMutableList(), this.channel)
+ mPreCallEndpointsList.add(preCallEndpoints)
+
+ val audioDeviceCallback =
+ object : AudioDeviceCallback() {
+ override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
+ if (addedDevices != null) {
+ preCallEndpoints.endpointsAddedUpdate(
+ getEndpointsFromAudioDeviceInfo(mContext, addedDevices.toList())
+ )
+ }
+ }
+
+ override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
+ if (removedDevices != null) {
+ preCallEndpoints.endpointsRemovedUpdate(
+ getEndpointsFromAudioDeviceInfo(mContext, removedDevices.toList())
+ )
+ }
+ }
+ }
+ // The following callback is needed in the event the user connects or disconnects
+ // and audio device after this API is called.
+ audioManager.registerAudioDeviceCallback(audioDeviceCallback, null /*handler*/)
+ // Send the initial list of pre-call [CallEndpointCompat]s out to the client. They
+ // will be emitted and cached in the Flow & only consumed once the client has
+ // collected it.
+ trySend(initialEndpoints)
+ awaitClose {
+ Log.i(TAG, "getAvailableStartingCallEndpoints: awaitClose")
+ audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
+ mPreCallEndpointsList.remove(preCallEndpoints)
+ }
+ }
+
+ /**
* Internal version of addCall, which also allows components in the library to consume generic
* events generated from the remote InCallServices. This facilitates the creation of Jetpack
* defined extensions.
@@ -392,6 +439,11 @@
// exception, addCall will unblock.
val blockingSessionExecution = CompletableDeferred<Unit>(parent = coroutineContext.job)
+ val preCallEndpoints: PreCallEndpoints? =
+ mPreCallEndpointsList.find {
+ it.isCallEndpointBeingTracked(callAttributes.preferredStartingCallEndpoint)
+ }
+
// create a call session based off the build version
@RequiresApi(34)
if (Utils.hasPlatformV2Apis()) {
@@ -408,6 +460,7 @@
onDisconnect,
onSetActive,
onSetInactive,
+ preCallEndpoints,
callChannels,
onEvent,
blockingSessionExecution
@@ -442,8 +495,6 @@
pauseExecutionUntilCallIsReadyOrTimeout(openResult, blockingSessionExecution)
- callSession.maybeSwitchToSpeakerOnCallStart()
-
/* at this point in time we have CallControl object */
val scope =
CallSession.CallControlScopeImpl(
@@ -453,6 +504,8 @@
coroutineContext
)
+ callSession.maybeSwitchStartingEndpoint(callAttributes.preferredStartingCallEndpoint)
+
// Run the clients code with the session active and exposed via the CallControlScope
// interface implementation declared above.
scope.block()
@@ -473,6 +526,8 @@
onSetActive,
onSetInactive,
onEvent,
+ callAttributes.preferredStartingCallEndpoint,
+ preCallEndpoints,
blockingSessionExecution
)
@@ -493,6 +548,7 @@
// CallControlScope interface implementation declared above.
scope.block()
}
+ preCallEndpoints?.mSendChannel?.close()
blockingSessionExecution.await()
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/InCallServiceCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/InCallServiceCompat.kt
index 835d31e..e9a14fe 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/InCallServiceCompat.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/InCallServiceCompat.kt
@@ -24,7 +24,9 @@
import android.util.Log
import androidx.annotation.CallSuper
import androidx.annotation.RequiresApi
-import androidx.core.telecom.extensions.CallExtensionsScope
+import androidx.core.telecom.extensions.CallExtensionScope
+import androidx.core.telecom.extensions.CallExtensionScopeImpl
+import androidx.core.telecom.extensions.CallExtensions
import androidx.core.telecom.util.ExperimentalAppActions
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -37,13 +39,14 @@
* [LifecycleOwner]
*/
@RequiresApi(Build.VERSION_CODES.O)
-internal open class InCallServiceCompat : InCallService(), LifecycleOwner {
+@Suppress("ContextNameSuffix")
+public open class InCallServiceCompat : InCallService(), LifecycleOwner, CallExtensions {
// Since we define this service as a LifecycleOwner, we need to implement this dispatcher as
// well. See [LifecycleService] for the example used to implement [LifecycleOwner].
private val dispatcher = ServiceLifecycleDispatcher(this)
- companion object {
- private val TAG = InCallServiceCompat::class.simpleName
+ private companion object {
+ private val TAG = InCallService::class.simpleName
}
override val lifecycle: Lifecycle
@@ -56,7 +59,8 @@
}
@CallSuper
- override fun onBind(intent: Intent): IBinder? {
+ @Suppress("InvalidNullabilityOverride")
+ override fun onBind(intent: Intent?): IBinder? {
dispatcher.onServicePreSuperOnBind()
return super.onBind(intent)
}
@@ -81,7 +85,6 @@
@CallSuper
override fun onDestroy() {
dispatcher.onServicePreSuperOnDestroy()
- // Todo: invoke CapabilityExchangeListener#onRemoveExtensions to inform the VOIP app
super.onDestroy()
}
@@ -89,42 +92,16 @@
* Connects extensions to the provided [Call], allowing the call to support additional optional
* behaviors beyond the traditional call state management.
*
- * The following extension is supported on a call:
- * - [CallExtensionsScope.addParticipantExtension] - Adds the ability to represent the
- * participants in the call.
- *
- * For example, an extension may allow the participants of a meeting to be surfaced to this
- * application so that the user can view and manage the participants in the meeting on different
- * surfaces:
- * ```
- * class InCallServiceImpl : InCallServiceCompat() {
- * ...
- * override fun onCallAdded(call: Call) {
- * lifecycleScope.launch {
- * connectExtensions(context, call) {
- * // Initialize extensions
- * onConnected { call ->
- * // change call states & listen/update extensions
- * }
- * }
- * // Once the call is destroyed, control flow will resume again
- * }
- * }
- * ...
- * }
- * ```
- *
* @param call The Call to connect extensions on.
* @param init The scope used to initialize and manage extensions in the scope of the Call.
+ * @see CallExtensions.connectExtensions
*/
- // TODO: Refactor to Public API
@ExperimentalAppActions
- @RequiresApi(Build.VERSION_CODES.O)
- suspend fun connectExtensions(call: Call, init: CallExtensionsScope.() -> Unit) {
+ override suspend fun connectExtensions(call: Call, init: CallExtensionScope.() -> Unit) {
// Attach this to the scope of the InCallService so it does not outlive its lifecycle
lifecycleScope
.launch {
- val scope = CallExtensionsScope(applicationContext, this, call)
+ val scope = CallExtensionScopeImpl(applicationContext, this, call)
Log.v(TAG, "connectExtensions: calling init")
scope.init()
Log.v(TAG, "connectExtensions: connecting extensions")
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ActionsResultCallback.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ActionsResultCallback.kt
index 1d5f6df..2f088c4 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ActionsResultCallback.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ActionsResultCallback.kt
@@ -53,10 +53,13 @@
)
) {
Log.i(TAG, "waitForResponse: VoIP app returned a result")
+ } else {
+ Log.i(TAG, "waitForResponse: latch timeout reached")
+ result = CallControlResult.Error(CallException.ERROR_OPERATION_TIMED_OUT)
}
}
} catch (e: TimeoutCancellationException) {
- Log.i(TAG, "waitForResponse: timeout reached")
+ Log.i(TAG, "waitForResponse: coroutine timeout reached")
result = CallControlResult.Error(CallException.ERROR_OPERATION_TIMED_OUT)
}
return result
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt
new file mode 100644
index 0000000..349ab3f
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.telecom.Call
+import androidx.core.telecom.util.ExperimentalAppActions
+
+/**
+ * Provides a scope where extensions can be first initialized and next managed for a [Call] once
+ * [onConnected] is called.
+ *
+ * The following extension is supported on a call:
+ * - [addParticipantExtension] - Show the user more information about the [Participant]s in the
+ * call.
+ *
+ * ```
+ * class InCallServiceImpl : InCallServiceCompat() {
+ * ...
+ * override fun onCallAdded(call: Call) {
+ * lifecycleScope.launch {
+ * connectExtensions(context, call) {
+ * // Initialize extensions
+ * onConnected { call ->
+ * // change call states & listen for extension updates/send extension actions
+ * }
+ * }
+ * // Once the call is destroyed, control flow will resume again
+ * }
+ * }
+ * ...
+ * }
+ * ```
+ */
+@ExperimentalAppActions
+public interface CallExtensionScope {
+
+ /**
+ * Called when the [Call] extensions have been successfully set up and are ready to be used.
+ *
+ * @param block Called when the [Call] and initialized extensions are ready to be used.
+ */
+ public fun onConnected(block: suspend (Call) -> Unit)
+
+ /**
+ * Add support for this remote surface to display information related to the [Participant]s in
+ * this call.
+ *
+ * ```
+ * connectExtensions(call) {
+ * val participantExtension = addParticipantExtension(
+ * // consume participant changed events
+ * )
+ * onConnected {
+ * // extensions have been negotiated and actions are ready to be used
+ * }
+ * }
+ * ```
+ *
+ * @param onActiveParticipantChanged Called with the active [Participant] in the call has
+ * changed. If this method is called with a `null` [Participant], there is no active
+ * [Participant]. The active [Participant] in the call is the [Participant] that should take
+ * focus and be either more prominent on the screen or otherwise featured as active in UI. For
+ * example, this could be the [Participant] that is actively talking or presenting.
+ * @param onParticipantsUpdated Called when the [Participant]s in the [Call] have changed and
+ * the UI should be updated.
+ * @return The interface that is used to set up additional actions for this extension.
+ */
+ public fun addParticipantExtension(
+ onActiveParticipantChanged: suspend (Participant?) -> Unit,
+ onParticipantsUpdated: suspend (Set<Participant>) -> Unit
+ ): ParticipantExtensionRemote
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionsScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
similarity index 92%
rename from core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionsScope.kt
rename to core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
index 8f24c80..7b44167 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionsScope.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
@@ -57,7 +57,7 @@
* [onExchangeComplete], which is called when capability exchange has completed and the extension
* should be initialized.
*/
-@ExperimentalAppActions
+@OptIn(ExperimentalAppActions::class)
internal data class CallExtensionCreator(
val extensionCapability: Capability,
val onExchangeComplete: suspend (Capability?, CapabilityExchangeListenerRemote?) -> Unit
@@ -68,7 +68,7 @@
* Contains the capabilities that the VOIP app supports and the remote binder implementation used to
* communicate with the remote process.
*/
-@ExperimentalAppActions
+@OptIn(ExperimentalAppActions::class)
private data class CapabilityExchangeResult(
val voipCapabilities: Set<Capability>,
val extensionInitializationBinder: CapabilityExchangeListenerRemote
@@ -89,15 +89,13 @@
* }
* ```
*/
-// TODO: Refactor to Public API
+@OptIn(ExperimentalAppActions::class)
@RequiresApi(Build.VERSION_CODES.O)
-@ExperimentalAppActions
-internal class CallExtensionsScope(
+internal class CallExtensionScopeImpl(
private val applicationContext: Context,
private val callScope: CoroutineScope,
private val call: Call
-) {
-
+) : CallExtensionScope {
companion object {
internal const val TAG = "CallExtensions"
@@ -126,46 +124,26 @@
// need to query the Capability after CallExtensionScope initialization has completed.
private val callExtensionCreators = HashSet<() -> CallExtensionCreator>()
- /**
- * Called when the [Call] extensions have been successfully set up and are ready to be used.
- *
- * @param block Called when extensions are ready to be used
- */
- fun onConnected(block: suspend (Call) -> Unit) {
+ override fun onConnected(block: suspend (Call) -> Unit) {
delegate = block
}
- /**
- * Add support for representing Participants in this call.
- *
- * ```
- * connectExtensions(call) {
- * val participantExtension = addParticipantExtension(
- * // consume participant changed events
- * )
- * onConnected {
- * // extensions have been negotiated and actions are ready to be used
- * }
- * }
- * ```
- *
- * @param onActiveParticipantChanged Called with the new active Participant any time it changes.
- * If this method is called with `null`, there is no active Participant.
- * @param onParticipantsUpdated Called when the Participants in the call have changed.
- * @return The extension connection that should be used to set up additional actions.
- */
- fun addParticipantExtension(
+ override fun addParticipantExtension(
onActiveParticipantChanged: suspend (Participant?) -> Unit,
onParticipantsUpdated: suspend (Set<Participant>) -> Unit
- ): ParticipantExtensionRemote {
+ ): ParticipantExtensionRemoteImpl {
val extension =
- ParticipantExtensionRemote(callScope, onActiveParticipantChanged, onParticipantsUpdated)
+ ParticipantExtensionRemoteImpl(
+ callScope,
+ onActiveParticipantChanged,
+ onParticipantsUpdated
+ )
registerExtension {
CallExtensionCreator(
extensionCapability =
Capability().apply {
featureId = Extensions.PARTICIPANT
- featureVersion = ParticipantExtension.VERSION
+ featureVersion = ParticipantExtensionImpl.VERSION
supportedActions = extension.actions
},
onExchangeComplete = extension::onExchangeComplete
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensions.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensions.kt
new file mode 100644
index 0000000..accbce2
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensions.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build.VERSION_CODES
+import android.telecom.Call
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.util.ExperimentalAppActions
+
+/**
+ * Provides the capability for a remote surface (automotive, watch, etc...) to connect to extensions
+ * provided by calling applications.
+ *
+ * Extensions allow a calling application to support additional optional features beyond the Android
+ * platform provided features defined in [Call]. When a new [Call] has been created, this interface
+ * allows the remote surface to also define which extensions that it supports in its UI. If the
+ * calling application providing the [Call] also supports the extension, the extension will be
+ * marked as supported. At that point, the remote surface can receive state updates and send action
+ * requests to the calling application to change state.
+ */
+public interface CallExtensions {
+ /**
+ * Connects extensions to the provided [call], allowing the [call] to support additional
+ * optional behaviors beyond the traditional call state management provided by [Call].
+ *
+ * @param call The [Call] to connect extensions on.
+ * @param init The scope used to initialize and manage extensions in the scope of the [Call].
+ * @see CallExtensionScope
+ */
+ @RequiresApi(VERSION_CODES.O)
+ @ExperimentalAppActions
+ public suspend fun connectExtensions(call: Call, init: CallExtensionScope.() -> Unit)
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallsManagerExtensions.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallsManagerExtensions.kt
new file mode 100644
index 0000000..2cccd8a
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallsManagerExtensions.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build.VERSION_CODES
+import android.telecom.DisconnectCause
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.util.ExperimentalAppActions
+
+/**
+ * Provide the ability for [CallsManager] to support extensions on a call.
+ *
+ * Extensions allow an application to support optional features beyond the scope of call state
+ * management and audio routing. These optional features provide the application with the ability to
+ * describe additional information about the call, which allows remote surfaces (automotive,
+ * watches, etc..) to provide UX related to this additional information. Additionally, remote
+ * surfaces can perform actions using a configured extension to notify this application of a remote
+ * user request.
+ *
+ * @see ExtensionInitializationScope
+ */
+public interface CallsManagerExtensions {
+ /**
+ * Adds a call with extensions support using [ExtensionInitializationScope], which allows an app
+ * to implement optional additional actions that go beyond the scope of a call, such as
+ * information about meeting participants and icons.
+ *
+ * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
+ * @param onAnswer where callType is the audio/video state the call should be answered as.
+ * Telecom is informing your VoIP application to answer an incoming call and set it to active.
+ * Telecom is requesting this on behalf of an system service (e.g. Automotive service) or a
+ * device (e.g. Wearable).
+ * @param onDisconnect where disconnectCause represents the cause for disconnecting the call.
+ * Telecom is informing your VoIP application to disconnect the incoming call. Telecom is
+ * requesting this on behalf of an system service (e.g. Automotive service) or a device (e.g.
+ * Wearable).
+ * @param onSetActive Telecom is informing your VoIP application to set the call active. Telecom
+ * is requesting this on behalf of an system service (e.g. Automotive service) or a device
+ * (e.g. Wearable).
+ * @param onSetInactive Telecom is informing your VoIP application to set the call inactive.
+ * This is the same as holding a call for two endpoints but can be extended to setting a
+ * meeting inactive. Telecom is requesting this on behalf of an system service (e.g.
+ * Automotive service) or a device (e.g.Wearable). Note: Your app must stop using the
+ * microphone and playing incoming media when returning.
+ * @param init The scope used to first initialize Extensions that will be used when the call is
+ * first notified to the platform and UX surfaces. Once the call is set up, the user's
+ * implementation of [ExtensionInitializationScope.onCall] will be called.
+ * @see CallsManager.addCall
+ */
+ @RequiresApi(VERSION_CODES.O)
+ @ExperimentalAppActions
+ public suspend fun addCallWithExtensions(
+ callAttributes: CallAttributesCompat,
+ onAnswer: suspend (callType: @CallAttributesCompat.Companion.CallType Int) -> Unit,
+ onDisconnect: suspend (disconnectCause: DisconnectCause) -> Unit,
+ onSetActive: suspend () -> Unit,
+ onSetInactive: suspend () -> Unit,
+ init: suspend ExtensionInitializationScope.() -> Unit
+ )
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
index dd5813d..63c9e5b 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
@@ -16,39 +16,62 @@
package androidx.core.telecom.extensions
-import android.os.Build
-import android.os.Bundle
-import android.os.RemoteException
-import android.util.Log
-import androidx.annotation.RequiresApi
import androidx.core.telecom.CallControlScope
-import androidx.core.telecom.CallsManager
-import androidx.core.telecom.internal.CapabilityExchangeRemote
-import androidx.core.telecom.internal.CapabilityExchangeRepository
import androidx.core.telecom.util.ExperimentalAppActions
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.launch
/**
- * The scope used to initialize extensions that will be used during the call and manage extensions
- * during the call.
+ * The scope used to initialize extensions on a call as well as manage initialized extensions
+ * associated with the call once the call has been set up.
*
- * Extensions should first be initialized in this scope. Once the call is set up, the user provided
- * implementation of [onCall] will be run, which should manage the call and extension states during
- * the lifetime of when the call is active.
+ * Extensions contain state and optional actions that are used to support additional features on a
+ * call, such as information about the participants in the call.
+ *
+ * Supported Extensions:
+ * - The ability to describe meeting participant information as well as actions on those
+ * participants using [addParticipantExtension]
+ *
+ * For example, to add participant support, the participant extension can be created during
+ * initialization and then used as part of [onCall] to update participant state and listen to action
+ * requests from remote surfaces:
+ * ```
+ * scope.launch {
+ * mCallsManager.addCallWithExtensions(attributes,
+ * onAnswerLambda,
+ * onDisconnectLambda,
+ * onSetActiveLambda,
+ * onSetInactiveLambda) {
+ * // Initialize extensions ...
+ * // Example: add participants support & associated actions
+ * val participantExtension = addParticipantExtension(initialParticipants)
+ * val raiseHandState = participantExtension.addRaiseHandSupport(
+ * initialRaisedHands) { onHandRaisedStateChanged ->
+ * // handle raised hand state changed
+ * }
+ * participantExtension.addKickParticipantSupport {
+ * participant ->
+ * // handle kicking the requested participant
+ * }
+ * // Call has been set up, perform in-call actions
+ * onCall {
+ * // Example: collect call state updates
+ * callStateFlow.onEach { newState ->
+ * // handle call state updates
+ * }.launchIn(this)
+ * // update participant extensions
+ * participantsFlow.onEach { newParticipants ->
+ * participantExtension.updateParticipants(newParticipants)
+ * }.launchIn(this)
+ * raisedHandsFlow.onEach { newRaisedHands ->
+ * raiseHandState.updateRaisedHands(newRaisedHands)
+ * }.launchIn(this)
+ * }
+ * }
+ * }
+ * }
+ * ```
*/
-// TODO: Refactor to Public API
-@RequiresApi(Build.VERSION_CODES.O)
@ExperimentalAppActions
-internal class ExtensionInitializationScope {
- private companion object {
- const val LOG_TAG = Extensions.LOG_TAG + "(EIS)"
- }
-
- private var onCreateDelegate: (suspend CallControlScope.() -> Unit)? = null
- private val extensionCreators = HashSet<(CapabilityExchangeRepository) -> Capability>()
+public interface ExtensionInitializationScope {
/**
* User provided callback implementation that is run when the call is ready using the provided
@@ -57,130 +80,21 @@
* @param onCall callback invoked when the call has been notified to the framework and the call
* is ready
*/
- fun onCall(onCall: suspend CallControlScope.() -> Unit) {
- Log.v(LOG_TAG, "onCall: storing delegate")
- // Capture onCall impl
- onCreateDelegate = onCall
- }
+ public fun onCall(onCall: suspend CallControlScope.() -> Unit)
/**
- * Adds the participant extension to a call, which provides the ability to specify participant
- * related information.
+ * Adds the participant extension to a call, which provides the ability for this application to
+ * specify participant related information, which will be shared with remote surfaces that
+ * support displaying that information (automotive, watch, etc...).
*
- * @param initialParticipants The initial participants in the call
- * @param initialActiveParticipant The initial participant that is active in the call
- * @return The interface used to update the participant state to remote InCallServices
+ * @param initialParticipants The initial [Set] of [Participant]s in the call
+ * @param initialActiveParticipant The initial [Participant] that is active in the call or
+ * `null` if there is no active participant.
+ * @return The interface used by this application to further update the participant extension
+ * state to remote surfaces
*/
- // TODO: Refactor to Public API
- fun addParticipantExtension(
+ public fun addParticipantExtension(
initialParticipants: Set<Participant> = emptySet(),
initialActiveParticipant: Participant? = null
- ): ParticipantExtension {
- val participant = ParticipantExtension(initialParticipants, initialActiveParticipant)
- registerExtension(onExchangeStarted = participant::onExchangeStarted)
- return participant
- }
-
- /**
- * Register an extension to be created once capability exchange begins.
- *
- * @param onExchangeStarted The capability exchange procedure has begun and the extension needs
- * to register the callbacks it will be handling as well as return the [Capability] of the
- * extension, which will be used during capability exchange.
- */
- private fun registerExtension(onExchangeStarted: (CapabilityExchangeRepository) -> Capability) {
- extensionCreators.add(onExchangeStarted)
- }
-
- /**
- * Collects [CallsManager.CallEvent]s that were received from connected InCallServices on the
- * provided CoroutineScope and optionally consumes the events. If we recognize and consume a
- * [CallsManager.CallEvent], this will create a Coroutine as a child of the [CoroutineScope]
- * provided here to manage the lifecycle of the task.
- *
- * @param scope The CoroutineScope that will be launched to perform the collection of events
- * @param eventFlow The [SharedFlow] representing the incoming [CallsManager.CallEvent]s from
- * the framework.
- */
- internal fun collectEvents(
- scope: CoroutineScope,
- eventFlow: SharedFlow<CallsManager.CallEvent>
- ) {
- scope.launch {
- Log.i(LOG_TAG, "collectEvents: starting collection")
- eventFlow
- .onCompletion { Log.i(LOG_TAG, "collectEvents: finishing...") }
- .collect {
- Log.v(LOG_TAG, "collectEvents: received ${it.event}")
- onEvent(it)
- }
- }
- }
-
- /**
- * Invokes the user provided implementation of [CallControlScope] when the call is ready.
- *
- * @param scope The enclosing [CallControlScope] passed in by [CallsManager.addCall] to be used
- * to call [onCall].
- */
- internal fun invokeDelegate(scope: CallControlScope) {
- scope.launch {
- Log.i(LOG_TAG, "invokeDelegate")
- onCreateDelegate?.invoke(scope)
- }
- }
-
- /**
- * Consumes [CallsManager.CallEvent]s received from remote InCallService implementations.
- *
- * Provides a [CoroutineScope] for events to use to handle the event and set up a session for
- * the lifecycle of the call.
- *
- * @param callEvent The event that we received from an InCallService.
- */
- private fun CoroutineScope.onEvent(callEvent: CallsManager.CallEvent) {
- when (callEvent.event) {
- Extensions.EVENT_JETPACK_CAPABILITY_EXCHANGE -> {
- handleCapabilityExchangeEvent(callEvent.extras)
- }
- }
- }
-
- /**
- * Starts a Coroutine to handle CapabilityExchange and extensions for the lifecycle of the call.
- *
- * @param extras The extras included as part of the Capability Exchange event.
- */
- private fun CoroutineScope.handleCapabilityExchangeEvent(extras: Bundle) {
- val version = extras.getInt(Extensions.EXTRA_CAPABILITY_EXCHANGE_VERSION)
- val capExchange =
- ICapabilityExchange.Stub.asInterface(
- extras.getBinder(Extensions.EXTRA_CAPABILITY_EXCHANGE_BINDER)
- )
- ?.let { CapabilityExchangeRemote(it) }
- if (capExchange == null) {
- Log.w(
- LOG_TAG,
- "handleCapabilityExchangeEvent: capExchange binder is null, can" +
- " not complete cap exchange"
- )
- return
- }
- Log.i(LOG_TAG, "handleCapabilityExchangeEvent: received CE request, v=#$version")
- // Create a child scope for setting up and running the extensions so that we can cancel
- // the child scope when the remote ICS disconnects without affecting the parent scope.
- val connectionScope = CoroutineScope(coroutineContext)
- // Create a new repository for each new connection
- val callbackRepository = CapabilityExchangeRepository(connectionScope)
- val capabilities = extensionCreators.map { it.invoke(callbackRepository) }
-
- Log.i(LOG_TAG, "handleCapabilityExchangeEvent: beginning exchange, caps=$capabilities")
- try {
- capExchange.beginExchange(capabilities, callbackRepository.listener)
- } catch (e: RemoteException) {
- Log.w(LOG_TAG, "handleCapabilityExchangeEvent: Remote could not be reached: $e")
- // This will cancel the surrounding coroutineScope
- throw e
- }
- }
+ ): ParticipantExtension
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
new file mode 100644
index 0000000..ed3156e4
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build
+import android.os.Bundle
+import android.os.RemoteException
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlScope
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.internal.CapabilityExchangeRemote
+import androidx.core.telecom.internal.CapabilityExchangeRepository
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.launch
+
+/**
+ * The scope used to initialize extensions that will be used during the call and manage extensions
+ * during the call.
+ *
+ * Extensions should first be initialized in this scope. Once the call is set up, the user provided
+ * implementation of [onCall] will be run, which should manage the call and extension states during
+ * the lifetime of when the call is active.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalAppActions::class)
+internal class ExtensionInitializationScopeImpl : ExtensionInitializationScope {
+ private companion object {
+ const val LOG_TAG = Extensions.LOG_TAG + "(EIS)"
+ }
+
+ private var onCreateDelegate: (suspend CallControlScope.() -> Unit)? = null
+ private val extensionCreators = HashSet<(CapabilityExchangeRepository) -> Capability>()
+
+ override fun onCall(onCall: suspend CallControlScope.() -> Unit) {
+ Log.v(LOG_TAG, "onCall: storing delegate")
+ // Capture onCall impl
+ onCreateDelegate = onCall
+ }
+
+ override fun addParticipantExtension(
+ initialParticipants: Set<Participant>,
+ initialActiveParticipant: Participant?
+ ): ParticipantExtension {
+ val participant = ParticipantExtensionImpl(initialParticipants, initialActiveParticipant)
+ registerExtension(onExchangeStarted = participant::onExchangeStarted)
+ return participant
+ }
+
+ /**
+ * Register an extension to be created once capability exchange begins.
+ *
+ * @param onExchangeStarted The capability exchange procedure has begun and the extension needs
+ * to register the callbacks it will be handling as well as return the [Capability] of the
+ * extension, which will be used during capability exchange.
+ */
+ private fun registerExtension(onExchangeStarted: (CapabilityExchangeRepository) -> Capability) {
+ extensionCreators.add(onExchangeStarted)
+ }
+
+ /**
+ * Collects [CallsManager.CallEvent]s that were received from connected InCallServices on the
+ * provided CoroutineScope and optionally consumes the events. If we recognize and consume a
+ * [CallsManager.CallEvent], this will create a Coroutine as a child of the [CoroutineScope]
+ * provided here to manage the lifecycle of the task.
+ *
+ * @param scope The CoroutineScope that will be launched to perform the collection of events
+ * @param eventFlow The [SharedFlow] representing the incoming [CallsManager.CallEvent]s from
+ * the framework.
+ */
+ internal fun collectEvents(
+ scope: CoroutineScope,
+ eventFlow: SharedFlow<CallsManager.CallEvent>
+ ) {
+ scope.launch {
+ Log.i(LOG_TAG, "collectEvents: starting collection")
+ eventFlow
+ .onCompletion { Log.i(LOG_TAG, "collectEvents: finishing...") }
+ .collect {
+ Log.v(LOG_TAG, "collectEvents: received ${it.event}")
+ onEvent(it)
+ }
+ }
+ }
+
+ /**
+ * Invokes the user provided implementation of [CallControlScope] when the call is ready.
+ *
+ * @param scope The enclosing [CallControlScope] passed in by [CallsManager.addCall] to be used
+ * to call [onCall].
+ */
+ internal fun invokeDelegate(scope: CallControlScope) {
+ scope.launch {
+ Log.i(LOG_TAG, "invokeDelegate")
+ onCreateDelegate?.invoke(scope)
+ }
+ }
+
+ /**
+ * Consumes [CallsManager.CallEvent]s received from remote InCallService implementations.
+ *
+ * Provides a [CoroutineScope] for events to use to handle the event and set up a session for
+ * the lifecycle of the call.
+ *
+ * @param callEvent The event that we received from an InCallService.
+ */
+ private fun CoroutineScope.onEvent(callEvent: CallsManager.CallEvent) {
+ when (callEvent.event) {
+ Extensions.EVENT_JETPACK_CAPABILITY_EXCHANGE -> {
+ handleCapabilityExchangeEvent(callEvent.extras)
+ }
+ }
+ }
+
+ /**
+ * Starts a Coroutine to handle CapabilityExchange and extensions for the lifecycle of the call.
+ *
+ * @param extras The extras included as part of the Capability Exchange event.
+ */
+ private fun CoroutineScope.handleCapabilityExchangeEvent(extras: Bundle) {
+ val version = extras.getInt(Extensions.EXTRA_CAPABILITY_EXCHANGE_VERSION)
+ val capExchange =
+ ICapabilityExchange.Stub.asInterface(
+ extras.getBinder(Extensions.EXTRA_CAPABILITY_EXCHANGE_BINDER)
+ )
+ ?.let { CapabilityExchangeRemote(it) }
+ if (capExchange == null) {
+ Log.w(
+ LOG_TAG,
+ "handleCapabilityExchangeEvent: capExchange binder is null, can" +
+ " not complete cap exchange"
+ )
+ return
+ }
+ Log.i(LOG_TAG, "handleCapabilityExchangeEvent: received CE request, v=#$version")
+ // Create a child scope for setting up and running the extensions so that we can cancel
+ // the child scope when the remote ICS disconnects without affecting the parent scope.
+ val connectionScope = CoroutineScope(coroutineContext)
+ // Create a new repository for each new connection
+ val callbackRepository = CapabilityExchangeRepository(connectionScope)
+ val capabilities = extensionCreators.map { it.invoke(callbackRepository) }
+
+ Log.i(LOG_TAG, "handleCapabilityExchangeEvent: beginning exchange, caps=$capabilities")
+ try {
+ capExchange.beginExchange(capabilities, callbackRepository.listener)
+ } catch (e: RemoteException) {
+ Log.w(LOG_TAG, "handleCapabilityExchangeEvent: Remote could not be reached: $e")
+ // This will cancel the surrounding coroutineScope
+ throw e
+ }
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt
index 90bbd27..f58ce43 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt
@@ -16,44 +16,28 @@
package androidx.core.telecom.extensions
-import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
+/** Internal constants related to Extensions that do not need to be exposed as a public API. */
+internal object Extensions {
+ internal const val LOG_TAG = "CallsManagerE"
-@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)
-// TODO: move addCallWithExtensions
-public interface Extensions {
- public companion object {
- internal const val LOG_TAG = "CallsManagerE"
+ /**
+ * EVENT used by InCallService as part of sendCallEvent to notify the VOIP Application that this
+ * InCallService supports jetpack extensions
+ */
+ internal const val EVENT_JETPACK_CAPABILITY_EXCHANGE =
+ "android.telecom.event.CAPABILITY_EXCHANGE"
- /**
- * EVENT used by InCallService as part of sendCallEvent to notify the VOIP Application that
- * this InCallService supports jetpack extensions
- */
- internal const val EVENT_JETPACK_CAPABILITY_EXCHANGE =
- "android.telecom.event.CAPABILITY_EXCHANGE"
+ /** VERSION used for handling future compatibility in capability exchange. */
+ internal const val EXTRA_CAPABILITY_EXCHANGE_VERSION =
+ "androidx.core.telecom.extensions.extra.CAPABILITY_EXCHANGE_VERSION"
- /** VERSION used for handling future compatibility in capability exchange. */
- internal const val EXTRA_CAPABILITY_EXCHANGE_VERSION = "CAPABILITY_EXCHANGE_VERSION"
+ /**
+ * BINDER used for handling capability exchange between the ICS and VOIP app sides, sent as part
+ * of sendCallEvent in the included extras.
+ */
+ internal const val EXTRA_CAPABILITY_EXCHANGE_BINDER =
+ "androidx.core.telecom.extensions.extra.CAPABILITY_EXCHANGE_BINDER"
- /**
- * BINDER used for handling capability exchange between the ICS and VOIP app sides, sent as
- * part of sendCallEvent in the included extras.
- */
- internal const val EXTRA_CAPABILITY_EXCHANGE_BINDER = "CAPABILITY_EXCHANGE_BINDER"
-
- /**
- * Constants used to denote the type of Extension supported by the [Capability] being
- * registered.
- */
- @Target(AnnotationTarget.TYPE)
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(PARTICIPANT)
- public annotation class Extensions
-
- /** Represents the [ParticipantExtension] extension */
- internal const val PARTICIPANT = 1
-
- // Represents a null Participant over Binder
- internal const val NULL_PARTICIPANT_ID = -1
- }
+ /** Represents the [ParticipantExtension] extension */
+ internal const val PARTICIPANT = 1
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantAction.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantAction.kt
index 24d80cb..b1c0da8 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantAction.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantAction.kt
@@ -16,84 +16,42 @@
package androidx.core.telecom.extensions
-import android.os.Build
-import android.util.Log
-import androidx.annotation.RequiresApi
import androidx.core.telecom.CallControlResult
-import androidx.core.telecom.CallException
-import androidx.core.telecom.internal.ParticipantActionsRemote
import androidx.core.telecom.util.ExperimentalAppActions
-import kotlin.properties.Delegates
-import kotlinx.coroutines.flow.StateFlow
/**
- * Implements the action to kick a Participant that part of the call and is being tracked via
- * [CallExtensionsScope.addParticipantExtension]
- *
- * @param participants A [StateFlow] representing the current Set of Participants that are in the
- * call.
+ * The action used to determine if the calling application supports kicking participants and request
+ * to kick [Participant]s in the call.
*/
-// TODO: Refactor to Public API
-@RequiresApi(Build.VERSION_CODES.O)
@ExperimentalAppActions
-internal class KickParticipantAction(
- private val participants: StateFlow<Set<Participant>>,
-) {
- companion object {
- const val TAG = CallExtensionsScope.TAG + "(KPCA)"
- }
+public interface KickParticipantAction {
/**
- * Whether or not kicking participants is supported by the remote.
+ * Whether or not kicking participants is supported by the calling application.
*
- * if `true`, then requests to kick participants will be sent to the remote application. If
- * `false`, then the remote doesn't support this action and requests will fail.
+ * if `true`, then requests to kick participants will be sent to the calling application. If
+ * `false`, then the calling application doesn't support this action and requests will fail.
*
- * Should not be queried until [CallExtensionsScope.onConnected] is called.
+ * Must not be queried until [CallExtensionScope.onConnected] is called.
*/
- var isSupported by Delegates.notNull<Boolean>()
- // The binder interface that allows this action to send events to the remote
- private var remoteActions: ParticipantActionsRemote? = null
+ public var isSupported: Boolean
/**
* Request to kick a [participant] in the call.
*
- * Note: This operation succeeding does not mean that the participant was kicked, it only means
- * that the request was received by the remote application. Any state changes that result from
- * this operation will be represented by the Set of Participants changing to remove the
- * requested participant.
+ * Whether or not the [Participant] is allowed to be kicked is up to the calling application, so
+ * requesting to kick a [Participant] may result in no action being taken. For example, the
+ * calling application may choose not to complete a request to kick the host of the call or kick
+ * the [Participant] representing this user.
*
- * @param participant The participant to kick
+ * Note: This operation succeeding does not mean that the participant was kicked, it only means
+ * that the request was received and processed by the remote application. If the [Participant]
+ * is indeed kicked, the [CallExtensionScope.addParticipantExtension] `onParticipantsUpdated`
+ * callback will be updated to remove the kicked [Participant].
+ *
+ * @param participant The [Participant] to kick from the call.
* @return The result of whether or not this request was successfully sent to the remote
- * application
+ * application and processed.
*/
- suspend fun requestKickParticipant(participant: Participant): CallControlResult {
- Log.d(TAG, "kickParticipant: participant=$participant")
- if (remoteActions == null) {
- Log.w(TAG, "kickParticipant: no binder, isSupported=$isSupported")
- // TODO: This needs to have its own CallException result
- return CallControlResult.Error(CallException.ERROR_UNKNOWN)
- }
- if (!participants.value.contains(participant)) {
- Log.d(TAG, "kickParticipant: couldn't find participant=$participant")
- return CallControlResult.Success()
- }
- val cb = ActionsResultCallback()
- remoteActions?.kickParticipant(participant, cb)
- val result = cb.waitForResponse()
- Log.d(TAG, "kickParticipant: participant=$participant, result=$result")
- return result
- }
-
- /** Called when capability exchange has completed and we can initialize this action */
- internal fun initialize(isSupported: Boolean) {
- Log.d(TAG, "initialize: isSupported=$isSupported")
- this.isSupported = isSupported
- }
-
- /** Called when the remote application has connected and will receive action event requests */
- internal fun connect(remote: ParticipantActionsRemote?) {
- Log.d(TAG, "connect: remote is null=${remote == null}")
- remoteActions = remote
- }
+ public suspend fun requestKickParticipant(participant: Participant): CallControlResult
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantActionImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantActionImpl.kt
new file mode 100644
index 0000000..a3208e6
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantActionImpl.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlResult
+import androidx.core.telecom.CallException
+import androidx.core.telecom.internal.ParticipantActionsRemote
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlin.properties.Delegates
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Implements the action to kick a Participant that is part of the call and is being tracked via
+ * [CallExtensionScope.addParticipantExtension]
+ *
+ * @param participants A [StateFlow] representing the current Set of Participants that are in the
+ * call.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalAppActions::class)
+internal class KickParticipantActionImpl(
+ private val participants: StateFlow<Set<Participant>>,
+) : KickParticipantAction {
+ companion object {
+ const val TAG = CallExtensionScopeImpl.TAG + "(KPCA)"
+ }
+
+ override var isSupported by Delegates.notNull<Boolean>()
+ // The binder interface that allows this action to send events to the remote
+ private var remoteActions: ParticipantActionsRemote? = null
+
+ override suspend fun requestKickParticipant(participant: Participant): CallControlResult {
+ Log.d(TAG, "kickParticipant: participant=$participant")
+ if (remoteActions == null) {
+ Log.w(TAG, "kickParticipant: no binder, isSupported=$isSupported")
+ // TODO: This needs to have its own CallException result
+ return CallControlResult.Error(CallException.ERROR_UNKNOWN)
+ }
+ if (!participants.value.contains(participant)) {
+ Log.d(TAG, "kickParticipant: couldn't find participant=$participant")
+ return CallControlResult.Success()
+ }
+ val cb = ActionsResultCallback()
+ remoteActions?.kickParticipant(participant, cb)
+ val result = cb.waitForResponse()
+ Log.d(TAG, "kickParticipant: participant=$participant, result=$result")
+ return result
+ }
+
+ /** Called when capability exchange has completed and we can initialize this action */
+ internal fun initialize(isSupported: Boolean) {
+ Log.d(TAG, "initialize: isSupported=$isSupported")
+ this.isSupported = isSupported
+ }
+
+ /** Called when the remote application has connected and will receive action event requests */
+ internal fun connect(remote: ParticipantActionsRemote?) {
+ Log.d(TAG, "connect: remote is null=${remote == null}")
+ remoteActions = remote
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantState.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantState.kt
index a41b816..ea7758e 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantState.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/KickParticipantState.kt
@@ -30,7 +30,7 @@
* @param onKickParticipant The action to perform when a request comes in from the remote
* InCallService to kick a participant.
*/
-@ExperimentalAppActions
+@OptIn(ExperimentalAppActions::class)
internal class KickParticipantState(
val participants: StateFlow<Set<Participant>>,
private val onKickParticipant: suspend (Participant) -> Unit
@@ -51,13 +51,15 @@
}
/**
- * Registered to be called when the remote InCallService has requested to kick a Participant.
+ * Registers to be called when the remote InCallService has requested to kick a Participant.
*
- * @param participant The participant to kick
+ * @param participantId The id of the participant to kick
*/
- private suspend fun kickParticipant(participant: Participant) {
- if (!participants.value.contains(participant)) {
- Log.w(LOG_TAG, "kickParticipant: $participant can not be found")
+ private suspend fun kickParticipant(participantId: String) {
+ val participant =
+ participants.value.firstOrNull { participant -> participant.id == participantId }
+ if (participant == null) {
+ Log.w(LOG_TAG, "kickParticipant: $participantId can not be found")
return
}
Log.d(LOG_TAG, "kickParticipant: kicking $participant")
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Participant.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Participant.kt
new file mode 100644
index 0000000..8ec3275
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Participant.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.core.telecom.extensions
+
+import androidx.core.telecom.util.ExperimentalAppActions
+
+@ExperimentalAppActions
+internal fun ParticipantParcelable.toParticipant(): Participant {
+ return Participant(id, name)
+}
+
+/**
+ * A representation of aspects of a participant in a Call.
+ *
+ * @param id A unique identifier shared with remote surfaces that represents this participant. This
+ * value MUST be unique and stable for the life of the call, meaning that this ID should not
+ * change or be reused for the lifetime of the call.
+ * @param name The name of the Participant, which remote surfaces will display to users.
+ */
+@ExperimentalAppActions
+public class Participant(
+ public val id: String,
+ public val name: CharSequence,
+) {
+
+ internal fun toParticipantParcelable(): ParticipantParcelable {
+ return ParticipantParcelable().also { parcelable ->
+ parcelable.id = id
+ parcelable.name = name
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Participant
+
+ return id == other.id
+ }
+
+ override fun hashCode(): Int {
+ return id.hashCode()
+ }
+
+ override fun toString(): String {
+ return "Participant[$id]: name=$name"
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtension.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtension.kt
index dcc9b1c..04ad0635 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtension.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtension.kt
@@ -16,215 +16,64 @@
package androidx.core.telecom.extensions
-import android.os.Build.VERSION_CODES
-import android.util.Log
-import androidx.annotation.IntDef
-import androidx.annotation.RequiresApi
-import androidx.core.telecom.internal.CapabilityExchangeRepository
-import androidx.core.telecom.internal.ParticipantActionCallbackRepository
-import androidx.core.telecom.internal.ParticipantStateListenerRemote
import androidx.core.telecom.util.ExperimentalAppActions
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
/**
- * Called when a new remove connection to an action is being established. The
- * [ParticipantStateListenerRemote] contains the remote interface used to send both the initial and
- * ongoing updates to the state tracked by the action. Any collection of flows related to updating
- * the remote session should use the provided [CoroutineScope]. For event callbacks from the remote,
- * [ParticipantActionCallbackRepository] should be used to register the callbacks that the action
- * should handle.
- */
-@OptIn(ExperimentalAppActions::class)
-internal typealias ActionConnector =
- (CoroutineScope, ParticipantActionCallbackRepository, ParticipantStateListenerRemote) -> Unit
-
-/**
- * The participant extension that manages the state of Participants associated with this call as
- * well as allowing participant related actions to register themselves with this extension.
+ * The extension interface used to support notifying remote surfaces (automotive, watch, etc...) of
+ * state related to the [Participant]s in the call.
*
- * Along with updating the participants in a call to remote surfaces, this extension also allows the
- * following optional actions to be supported:
- * - [addRaiseHandSupport] - Support for allowing a remote surface to show which participants have
- * their hands raised to the user as well as update the raised hand state of the user.
- * - [addKickParticipantSupport] = Support for allowing a user on a remote surface to kick a
- * participant.
+ * This interface allows an application to notify remote surfaces of changes to [Participant] state.
+ * Additionally, this interface allows the application to support optional actions that use the
+ * participant state. These actions provide remote surfaces with the ability to request participant
+ * state updates based on user input and provide additional information about the state of specific
+ * participants.
*
- * @param initialParticipants The initial set of Participants that are associated with this call.
- * @param initialActiveParticipant The initial active Participant that is associated with this call.
+ * @see ExtensionInitializationScope.addParticipantExtension
*/
-// TODO: Refactor to Public API
@ExperimentalAppActions
-@RequiresApi(VERSION_CODES.O)
-internal class ParticipantExtension(
- initialParticipants: Set<Participant>,
- initialActiveParticipant: Participant?
-) {
- public companion object {
- /**
- * The version of this ParticipantExtension used for capability exchange. Should be updated
- * whenever there is an API change to this extension or an existing action.
- */
- internal const val VERSION = 1
-
- /**
- * Constants used to denote the type of action supported by the [Capability] being
- * registered.
- */
- @Target(AnnotationTarget.TYPE)
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(RAISE_HAND_ACTION, KICK_PARTICIPANT_ACTION)
- annotation class ExtensionActions
-
- /** Identifier for the raise hand action */
- internal const val RAISE_HAND_ACTION = 1
- /** Identifier for the kick participant action */
- internal const val KICK_PARTICIPANT_ACTION = 2
-
- private const val LOG_TAG = Extensions.LOG_TAG + "(PE)"
- }
-
- /** StateFlow of the current set of Participants associated with the call */
- internal val participants: MutableStateFlow<Set<Participant>> =
- MutableStateFlow(initialParticipants)
-
- /** StateFlow containing the active participant of the call if it exists */
- private val activeParticipant: MutableStateFlow<Participant?> =
- MutableStateFlow(initialActiveParticipant)
-
- /** Maps an action to its [ActionConnector], which will be called during capability exchange */
- private val actionRemoteConnector: HashMap<Int, ActionConnector> = HashMap()
-
+public interface ParticipantExtension {
/**
- * Update all remote listeners that the Participants of this call have changed
+ * Update all of the remote surfaces that the [Participant]s of this call have changed.
*
- * @param newParticipants The new set of [Participant]s associated with this call
+ * @param newParticipants The new set of [Participant]s associated with this call.
*/
- public suspend fun updateParticipants(newParticipants: Set<Participant>) {
- participants.emit(newParticipants)
- }
+ public suspend fun updateParticipants(newParticipants: Set<Participant>)
/**
- * The active participant associated with this call, if it exists
+ * Update all of the remote surfaces that the active participant associated with this call has
+ * changed, if it exists.
*
- * @param participant the participant that is marked as active or `null` if there is no active
- * participant
- */
- public suspend fun updateActiveParticipant(participant: Participant?) {
- activeParticipant.emit(participant)
- }
-
- /**
- * Adds support for notifying remote InCallServices of the raised hand state of all Participants
- * in the call and listening for changes to this user's hand raised state.
+ * The "active" participant is the participant that is currently taking focus and should be
+ * marked in UX as active or take a more prominent view to the user.
*
- * @param onHandRaisedChanged Called when the raised hand state of this user has changed. If
- * `true`, the user has raised their hand. If `false`, the user has lowered their hand.
- * @return The interface used to update the current raised hand state of all participants in the
- * call.
+ * @param participant the [Participant] that is marked as the active participant or `null` if
+ * there is no active participant
*/
- fun addRaiseHandSupport(onHandRaisedChanged: suspend (Boolean) -> Unit): RaiseHandState {
- val state = RaiseHandState(participants, onHandRaisedChanged)
- registerAction(RAISE_HAND_ACTION, connector = state::connect)
- return state
- }
+ public suspend fun updateActiveParticipant(participant: Participant?)
/**
- * Adds support for allowing the user to kick participants in the call.
+ * Adds support for notifying remote surfaces of the "raised hand" state of all [Participant]s
+ * in the call.
*
- * @param onKickParticipant The action to perform when the user requests to kick a participant
- * @return The interface used to update the state related to this action. This action contains
- * no state today, but is included for forward compatibility
+ * @param initialRaisedHands The initial List of [Participant]s whose hands are raised, ordered
+ * from earliest raised hand to newest raised hand.
+ * @param onHandRaisedChanged This is called when the user has requested to change their "raised
+ * hand" state on a remote surface. If `true`, this user has raised their hand. If `false`,
+ * this user has lowered their hand. This operation should not return until the request has
+ * been processed.
+ * @return The interface used to update the current raised hand state of all [Participant]s in
+ * the call.
*/
- fun addKickParticipantSupport(onKickParticipant: suspend (Participant) -> Unit) {
- val state = KickParticipantState(participants, onKickParticipant)
- registerAction(KICK_PARTICIPANT_ACTION) { _, repo, _ -> state.connect(repo) }
- }
+ public fun addRaiseHandSupport(
+ initialRaisedHands: List<Participant> = emptyList(),
+ onHandRaisedChanged: suspend (Boolean) -> Unit
+ ): RaiseHandState
/**
- * Setup the participant extension creation callback receiver and return the Capability of this
- * extension to be shared with the remote.
- */
- internal fun onExchangeStarted(callbacks: CapabilityExchangeRepository): Capability {
- callbacks.onCreateParticipantExtension = ::onCreateParticipantExtension
- return Capability().apply {
- featureId = Extensions.PARTICIPANT
- featureVersion = VERSION
- supportedActions = actionRemoteConnector.keys.toIntArray()
- }
- }
-
- /**
- * Register an action to this extension
+ * Adds support for allowing the user to kick participants in the call using the remote surface.
*
- * @param action The identifier of the action, which will be shared with the remote
- * @param connector The method that is called every time a new remote connects to the action in
- * order to facilitate connecting this action to the remote.
+ * @param onKickParticipant The action to perform when the user requests to kick a participant.
+ * This operation should not return until the request has been processed.
*/
- private fun registerAction(action: Int, connector: ActionConnector) {
- actionRemoteConnector[action] = connector
- }
-
- /**
- * Function registered to [ExtensionInitializationScope] in order to handle the creation of the
- * participant extension.
- *
- * @param coroutineScope the CoroutineScope used to launch tasks associated with participants
- * @param remoteActions the actions reported as supported from the remote InCallService side
- * @param binder the interface used to communicate with the remote InCallService.
- */
- private fun onCreateParticipantExtension(
- coroutineScope: CoroutineScope,
- remoteActions: Set<Int>,
- binder: ParticipantStateListenerRemote
- ) {
- Log.i(LOG_TAG, "onCreatePE: actions=$remoteActions")
-
- // Synchronize initial state with remote
- val initParticipants = participants.value.toTypedArray()
- val initActiveParticipant = activeParticipant.value
- binder.updateParticipants(initParticipants)
- if (initActiveParticipant != null && initParticipants.contains(initActiveParticipant)) {
- binder.updateActiveParticipant(initActiveParticipant.id)
- } else {
- binder.updateActiveParticipant(Extensions.NULL_PARTICIPANT_ID)
- }
-
- // Setup listeners for changes to state
- participants
- .onEach { updatedParticipants ->
- Log.i(LOG_TAG, "to remote: updateParticipants: $updatedParticipants")
- binder.updateParticipants(updatedParticipants.toTypedArray())
- }
- .combine(activeParticipant) { p, a ->
- val result = if (a != null && p.contains(a)) a else null
- Log.d(LOG_TAG, "combine: $p + $a = $result")
- result
- }
- .distinctUntilChanged()
- .onEach {
- Log.d(LOG_TAG, "to remote: updateActiveParticipant=$it")
- binder.updateActiveParticipant(it?.id ?: Extensions.NULL_PARTICIPANT_ID)
- }
- .launchIn(coroutineScope)
- Log.d(LOG_TAG, "onCreatePE: finished state update")
-
- // Setup actions
- coroutineScope.launch {
- // Setup one callback repository per connection to remote
- val callbackRepository = ParticipantActionCallbackRepository(this)
- // Set up actions (only where the remote side supports it)
- actionRemoteConnector
- .filter { entry -> remoteActions.contains(entry.key) }
- .map { entry -> entry.value }
- .forEach { initializer -> initializer(this, callbackRepository, binder) }
- Log.d(LOG_TAG, "onCreatePE: calling finishSync")
- binder.finishSync(callbackRepository.eventListener)
- }
- }
+ public fun addKickParticipantSupport(onKickParticipant: suspend (Participant) -> Unit)
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionImpl.kt
new file mode 100644
index 0000000..e3d1caa
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionImpl.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build.VERSION_CODES
+import android.util.Log
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.CapabilityExchangeRepository
+import androidx.core.telecom.internal.ParticipantActionCallbackRepository
+import androidx.core.telecom.internal.ParticipantStateListenerRemote
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Called when a new remove connection to an action is being established. The
+ * [ParticipantStateListenerRemote] contains the remote interface used to send both the initial and
+ * ongoing updates to the state tracked by the action. Any collection of flows related to updating
+ * the remote session should use the provided [CoroutineScope]. For event callbacks from the remote,
+ * [ParticipantActionCallbackRepository] should be used to register the callbacks that the action
+ * should handle.
+ */
+@OptIn(ExperimentalAppActions::class)
+internal typealias ActionConnector =
+ (CoroutineScope, ParticipantActionCallbackRepository, ParticipantStateListenerRemote) -> Unit
+
+/**
+ * The participant extension that manages the state of Participants associated with this call as
+ * well as allowing participant related actions to register themselves with this extension.
+ *
+ * Along with updating the participants in a call to remote surfaces, this extension also allows the
+ * following optional actions to be supported:
+ * - [addRaiseHandSupport] - Support for allowing a remote surface to show which participants have
+ * their hands raised to the user as well as update the raised hand state of the user.
+ * - [addKickParticipantSupport] = Support for allowing a user on a remote surface to kick a
+ * participant.
+ *
+ * @param initialParticipants The initial set of Participants that are associated with this call.
+ * @param initialActiveParticipant The initial active Participant that is associated with this call.
+ */
+@OptIn(ExperimentalAppActions::class)
+@RequiresApi(VERSION_CODES.O)
+internal class ParticipantExtensionImpl(
+ initialParticipants: Set<Participant>,
+ initialActiveParticipant: Participant?
+) : ParticipantExtension {
+ companion object {
+ /**
+ * The version of this ParticipantExtension used for capability exchange. Should be updated
+ * whenever there is an API change to this extension or an existing action.
+ */
+ internal const val VERSION = 1
+
+ /**
+ * Constants used to denote the type of action supported by the [Capability] being
+ * registered.
+ */
+ @Target(AnnotationTarget.TYPE)
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(RAISE_HAND_ACTION, KICK_PARTICIPANT_ACTION)
+ annotation class ExtensionActions
+
+ /** Identifier for the raise hand action */
+ internal const val RAISE_HAND_ACTION = 1
+ /** Identifier for the kick participant action */
+ internal const val KICK_PARTICIPANT_ACTION = 2
+
+ private const val LOG_TAG = Extensions.LOG_TAG + "(PE)"
+ }
+
+ /** StateFlow of the current set of Participants associated with the call */
+ internal val participants: MutableStateFlow<Set<Participant>> =
+ MutableStateFlow(initialParticipants)
+
+ /** StateFlow containing the active participant of the call if it exists */
+ private val activeParticipant: MutableStateFlow<Participant?> =
+ MutableStateFlow(initialActiveParticipant)
+
+ /** Maps an action to its [ActionConnector], which will be called during capability exchange */
+ private val actionRemoteConnector: HashMap<Int, ActionConnector> = HashMap()
+
+ override suspend fun updateParticipants(newParticipants: Set<Participant>) {
+ participants.emit(newParticipants)
+ }
+
+ override suspend fun updateActiveParticipant(participant: Participant?) {
+ activeParticipant.emit(participant)
+ }
+
+ override fun addRaiseHandSupport(
+ initialRaisedHands: List<Participant>,
+ onHandRaisedChanged: suspend (Boolean) -> Unit
+ ): RaiseHandState {
+ val state = RaiseHandStateImpl(participants, initialRaisedHands, onHandRaisedChanged)
+ registerAction(RAISE_HAND_ACTION, connector = state::connect)
+ return state
+ }
+
+ override fun addKickParticipantSupport(onKickParticipant: suspend (Participant) -> Unit) {
+ val state = KickParticipantState(participants, onKickParticipant)
+ registerAction(KICK_PARTICIPANT_ACTION) { _, repo, _ -> state.connect(repo) }
+ }
+
+ /**
+ * Setup the participant extension creation callback receiver and return the Capability of this
+ * extension to be shared with the remote.
+ */
+ internal fun onExchangeStarted(callbacks: CapabilityExchangeRepository): Capability {
+ callbacks.onCreateParticipantExtension = ::onCreateParticipantExtension
+ return Capability().apply {
+ featureId = Extensions.PARTICIPANT
+ featureVersion = VERSION
+ supportedActions = actionRemoteConnector.keys.toIntArray()
+ }
+ }
+
+ /**
+ * Register an action to this extension
+ *
+ * @param action The identifier of the action, which will be shared with the remote
+ * @param connector The method that is called every time a new remote connects to the action in
+ * order to facilitate connecting this action to the remote.
+ */
+ private fun registerAction(action: Int, connector: ActionConnector) {
+ actionRemoteConnector[action] = connector
+ }
+
+ /**
+ * Function registered to [ExtensionInitializationScope] in order to handle the creation of the
+ * participant extension.
+ *
+ * @param coroutineScope the CoroutineScope used to launch tasks associated with participants
+ * @param remoteActions the actions reported as supported from the remote InCallService side
+ * @param binder the interface used to communicate with the remote InCallService.
+ */
+ private fun onCreateParticipantExtension(
+ coroutineScope: CoroutineScope,
+ remoteActions: Set<Int>,
+ binder: ParticipantStateListenerRemote
+ ) {
+ Log.i(LOG_TAG, "onCreatePE: actions=$remoteActions")
+
+ // Synchronize initial state with remote
+ val initParticipants = participants.value
+ val initActiveParticipant = activeParticipant.value
+ binder.updateParticipants(initParticipants)
+ if (initActiveParticipant != null && initParticipants.contains(initActiveParticipant)) {
+ binder.updateActiveParticipant(initActiveParticipant)
+ } else {
+ binder.updateActiveParticipant(null)
+ }
+
+ // Setup listeners for changes to state
+ participants
+ .onEach { updatedParticipants ->
+ Log.i(LOG_TAG, "to remote: updateParticipants: $updatedParticipants")
+ binder.updateParticipants(updatedParticipants)
+ }
+ .combine(activeParticipant) { p, a ->
+ val result = if (a != null && p.contains(a)) a else null
+ Log.d(LOG_TAG, "combine: $p + $a = $result")
+ result
+ }
+ .distinctUntilChanged()
+ .onEach {
+ Log.d(LOG_TAG, "to remote: updateActiveParticipant=$it")
+ binder.updateActiveParticipant(it)
+ }
+ .launchIn(coroutineScope)
+ Log.d(LOG_TAG, "onCreatePE: finished state update")
+
+ // Setup one callback repository per connection to remote
+ val callbackRepository = ParticipantActionCallbackRepository(coroutineScope)
+ // Set up actions (only where the remote side supports it)
+ actionRemoteConnector
+ .filter { entry -> remoteActions.contains(entry.key) }
+ .map { entry -> entry.value }
+ .forEach { initializer -> initializer(coroutineScope, callbackRepository, binder) }
+ Log.d(LOG_TAG, "onCreatePE: calling finishSync")
+ binder.finishSync(callbackRepository.eventListener)
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemote.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemote.kt
index f0b2482..f7346c7 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemote.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemote.kt
@@ -16,99 +16,42 @@
package androidx.core.telecom.extensions
-import android.os.Build
-import android.util.Log
-import androidx.annotation.RequiresApi
-import androidx.core.telecom.internal.CapabilityExchangeListenerRemote
-import androidx.core.telecom.internal.ParticipantActionsRemote
-import androidx.core.telecom.internal.ParticipantStateListener
import androidx.core.telecom.util.ExperimentalAppActions
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-import kotlin.properties.Delegates
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-
-/** Repository containing the callbacks associated with the Participant extension state changes */
-@ExperimentalAppActions
-internal class ParticipantStateCallbackRepository {
- var raisedHandsStateCallback: (suspend (Set<Int>) -> Unit)? = null
-}
/**
- * Contains the callbacks used by Actions during creation. [onInitialization] is called when
- * capability exchange has completed and Actions should be initialized and [onRemoteConnected] is
- * called when the remote has connected, finished sending initial state, and is ready to handle
- * Participant action updates.
- */
-@ExperimentalAppActions
-private data class ActionExchangeResult(
- val onInitialization: (Boolean) -> Unit,
- val onRemoteConnected: (ParticipantActionsRemote?) -> Unit
-)
-
-/**
- * Implements the Participant extension and provides a method for actions to use to register
- * themselves.
+ * Interface used to allow the remote surface (automotive, watch, etc...) to know if the connected
+ * calling application supports the participant extension and optionally set up additional actions
+ * for the [Participant]s in the call.
*
- * @param callScope The CoroutineScope of the underlying call
- * @param onActiveParticipantChanged The update callback used whenever the active participants
- * change
- * @param onParticipantsUpdated The update callback used whenever the participants in the call
- * change
+ * Actions allow the remote surface to display additional optional state regarding the
+ * [Participant]s in the call and send action requests to the calling application to modify the
+ * state of supported actions.
*/
-// TODO: Refactor to Public API
-// TODO: Remove old version of ParticipantClientExtension in a follow up CL with this impl.
-@RequiresApi(Build.VERSION_CODES.O)
@ExperimentalAppActions
-internal class ParticipantExtensionRemote(
- private val callScope: CoroutineScope,
- private val onActiveParticipantChanged: suspend (Participant?) -> Unit,
- private val onParticipantsUpdated: suspend (Set<Participant>) -> Unit
-) {
- companion object {
- internal const val TAG = CallExtensionsScope.TAG + "(PCE)"
- }
+public interface ParticipantExtensionRemote {
/**
- * Whether or not the participants extension is supported by the remote.
+ * Whether or not the participants extension is supported by the calling application.
*
- * if `true`, then updates about call participants will be notified. If `false`, then the remote
- * doesn't support this extension and participants will not be notified to the caller nor will
- * associated actions receive state updates.
+ * If `true`, then updates about [Participant]s in the call will be notified. If `false`, then
+ * the remote doesn't support this extension and participants will not be notified to the caller
+ * nor will associated actions receive state updates.
*
- * Should not be queried until [CallExtensionsScope.onConnected] is called.
+ * Note: Must not be queried until after [CallExtensionScope.onConnected] is called.
*/
- var isSupported by Delegates.notNull<Boolean>()
-
- /** The actions that are registered with the Participant extension */
- internal val actions
- get() = actionInitializers.keys.toIntArray()
-
- // Maps a Capability to a receiver that allows the action to register itself with a listener
- // and then return a Receiver that gets called when Cap exchange completes.
- private val actionInitializers = HashMap<Int, ActionExchangeResult>()
- // Manages callbacks that are applicable to sub-actions of the Participants
- private val callbacks = ParticipantStateCallbackRepository()
-
- // Participant specific state
- private val participants = MutableStateFlow<Set<Participant>>(emptySet())
- private val activeParticipant = MutableStateFlow<Int?>(null)
+ public val isSupported: Boolean
/**
- * Adds the ability for participants to raise their hands.
+ * Adds the "raise hand" action and provides the remote surface with the ability to display
+ * which [Participant]s have their hands raised and an action to request to raise and lower
+ * their own hand.
*
* ```
* connectExtensions(call) {
* val participantExtension = addParticipantExtension(
* // consume participant changed events
* )
+ * // Initialize the raise hand action
* val raiseHandAction = participantExtension.addRaiseHandAction { raisedHands ->
* // consume changes of participants with their hands raised
* }
@@ -121,25 +64,19 @@
* }
* ```
*
- * @param onRaisedHandsChanged Called when the Set of Participants with their hands raised has
- * changed.
- * @return The action that is used to send raise hand event requests to the remote Call.
+ * Note: Must be called during initialization before [CallExtensionScope.onConnected] is called.
+ *
+ * @param onRaisedHandsChanged Called when the List of [Participant]s with their hands raised
+ * has changed, ordered from oldest raised hand to newest raised hand.
+ * @return The action that is used to determine support of this action and send raise hand event
+ * requests to the calling application.
*/
- fun addRaiseHandAction(
- onRaisedHandsChanged: suspend (Set<Participant>) -> Unit
- ): RaiseHandAction {
- val action = RaiseHandAction(participants, onRaisedHandsChanged)
- registerAction(
- ParticipantExtension.RAISE_HAND_ACTION,
- onRemoteConnected = action::connect
- ) { isSupported ->
- action.initialize(callScope, isSupported, callbacks)
- }
- return action
- }
+ public fun addRaiseHandAction(
+ onRaisedHandsChanged: suspend (List<Participant>) -> Unit
+ ): RaiseHandAction
/**
- * Adds the ability for the user to kick participants.
+ * Adds the ability for the user to request to kick [Participant]s in the call.
*
* ```
* connectExtensions(call) {
@@ -159,158 +96,5 @@
*
* @return The action that is used to send kick Participant event requests to the remote Call.
*/
- fun addKickParticipantAction(): KickParticipantAction {
- val action = KickParticipantAction(participants)
- registerAction(
- ParticipantExtension.KICK_PARTICIPANT_ACTION,
- onRemoteConnected = action::connect,
- onInitialization = action::initialize
- )
- return action
- }
-
- /**
- * Register an Action on the Participant extension that will be initialized and connected if the
- * action is supported by the remote Call before [CallExtensionsScope.onConnected] is called.
- *
- * @param action A unique identifier for the action that will be used by the remote side to
- * identify this action.
- * @param onRemoteConnected The callback called when the remote has connected and action events
- * can be sent to the remote via [ParticipantActionsRemote].
- * @param onInitialization The Action initializer, which allows the action to setup callbacks.
- * via [ParticipantStateCallbackRepository] and determine if the action is supported by the
- * remote Call.
- */
- private fun registerAction(
- action: Int,
- onRemoteConnected: (ParticipantActionsRemote?) -> Unit,
- onInitialization: (Boolean) -> Unit
- ) {
- actionInitializers[action] = ActionExchangeResult(onInitialization, onRemoteConnected)
- }
-
- /**
- * Capability exchange has completed and the [Capability] of the Participant extension has been
- * negotiated with the remote call.
- *
- * @param negotiatedCapability The negotiated Participant capability or null if the remote
- * doesn't support this capability.
- * @param remote The remote interface which must be used by this extension to create the
- * Participant extension on the remote side using the negotiated capability.
- */
- internal suspend fun onExchangeComplete(
- negotiatedCapability: Capability?,
- remote: CapabilityExchangeListenerRemote?
- ) {
- if (negotiatedCapability == null || remote == null) {
- Log.i(TAG, "onNegotiated: remote is not capable")
- isSupported = false
- initializeNotSupportedActions()
- return
- }
- Log.d(TAG, "onNegotiated: setup updates")
- initializeParticipantUpdates()
- initializeActionsLocally(negotiatedCapability)
- val remoteBinder = connectActionsToRemote(negotiatedCapability, remote)
- actionInitializers.forEach { connector -> connector.value.onRemoteConnected(remoteBinder) }
- }
-
- /**
- * Connect Participant action Flows to the remote interface so we can start receiving changes to
- * the Participant and associated action state.
- *
- * When [CapabilityExchangeListenerRemote.onCreateParticipantExtension] is called, the remote
- * will send the initial state of each of the supported actions and then call
- * [ParticipantStateListener.finishSync], which will provide us an interface to allow us to send
- * participant action event requests.
- *
- * @param negotiatedCapability The negotiated Participant capability that contains a negotiated
- * version and actions supported by both the local and remote Call.
- * @param remote The interface used by the local call to create the Participant extension with
- * the remote party if supported and allow for Participant state updates.
- * @return The interface used by the local Call to send Participant action event requests.
- */
- private suspend fun connectActionsToRemote(
- negotiatedCapability: Capability,
- remote: CapabilityExchangeListenerRemote
- ): ParticipantActionsRemote? = suspendCoroutine { continuation ->
- val participantStateListener =
- ParticipantStateListener(
- updateParticipants = { newParticipants ->
- callScope.launch {
- Log.v(TAG, "updateParticipants: $newParticipants")
- participants.emit(newParticipants)
- }
- },
- updateActiveParticipant = { newActiveParticipant ->
- callScope.launch {
- Log.v(TAG, "activeParticipant=$newActiveParticipant")
- activeParticipant.emit(newActiveParticipant)
- }
- },
- updateRaisedHands = { newRaisedHands ->
- callScope.launch {
- Log.v(TAG, "raisedHands=$newRaisedHands")
- callbacks.raisedHandsStateCallback?.invoke(newRaisedHands)
- }
- },
- finishSync = { remoteBinder ->
- callScope.launch {
- Log.v(TAG, "finishSync complete, isNull=${remoteBinder == null}")
- continuation.resume(remoteBinder)
- }
- }
- )
- remote.onCreateParticipantExtension(
- negotiatedCapability.featureVersion,
- negotiatedCapability.supportedActions,
- participantStateListener
- )
- }
-
- /** Setup callback updates when [participants] or [activeParticipant] changes */
- private fun initializeParticipantUpdates() {
- participants
- .onEach { participantsState -> onParticipantsUpdated(participantsState) }
- .combine(activeParticipant) { p, ap ->
- ap?.let { p.firstOrNull { participant -> participant.id == ap } }
- }
- .distinctUntilChanged()
- .onEach { activeParticipant -> onActiveParticipantChanged(activeParticipant) }
- .onCompletion { Log.d(TAG, "participant flow complete") }
- .launchIn(callScope)
- }
-
- /**
- * Calls the [ActionExchangeResult.onInitialization] callback on each registered action
- * (registered via [registerAction]) to initialize. Initialization uses the negotiated
- * [Capability] to determine whether or not the registered action is supported by the remote and
- * provides the ability for the action to register for remote state callbacks.
- *
- * @param negotiatedCapability The negotiated Participant [Capability] containing the
- * Participant extension version and actions supported by both the local and remote Call.
- */
- private fun initializeActionsLocally(negotiatedCapability: Capability) {
- for (action in actionInitializers) {
- Log.d(TAG, "initializeActions: setup action=${action.key}")
- if (negotiatedCapability.supportedActions.contains(action.key)) {
- Log.d(TAG, "initializeActions: action=${action.key} supported")
- action.value.onInitialization(true)
- } else {
- Log.d(TAG, "initializeActions: action=${action.key} not supported")
- action.value.onInitialization(false)
- }
- }
- }
-
- /**
- * In the case that participants are not supported, notify all actions that they are also not
- * supported.
- */
- private fun initializeNotSupportedActions() {
- Log.d(TAG, "initializeActions: no actions supported")
- for (action in actionInitializers) {
- action.value.onInitialization(false)
- }
- }
+ public fun addKickParticipantAction(): KickParticipantAction
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt
new file mode 100644
index 0000000..012e7bc
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.CapabilityExchangeListenerRemote
+import androidx.core.telecom.internal.ParticipantActionsRemote
+import androidx.core.telecom.internal.ParticipantStateListener
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlin.properties.Delegates
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/** Repository containing the callbacks associated with the Participant extension state changes */
+@ExperimentalAppActions
+internal class ParticipantStateCallbackRepository {
+ var raisedHandIdsStateCallback: (suspend (List<String>) -> Unit)? = null
+}
+
+/**
+ * Contains the callbacks used by Actions during creation. [onInitialization] is called when
+ * capability exchange has completed and Actions should be initialized and [onRemoteConnected] is
+ * called when the remote has connected, finished sending initial state, and is ready to handle
+ * Participant action updates.
+ */
+@ExperimentalAppActions
+private data class ActionExchangeResult(
+ val onInitialization: (Boolean) -> Unit,
+ val onRemoteConnected: (ParticipantActionsRemote?) -> Unit
+)
+
+/**
+ * Implements the Participant extension and provides a method for actions to use to register
+ * themselves.
+ *
+ * @param callScope The CoroutineScope of the underlying call
+ * @param onActiveParticipantChanged The update callback used whenever the active participants
+ * change
+ * @param onParticipantsUpdated The update callback used whenever the participants in the call
+ * change
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalAppActions::class)
+internal class ParticipantExtensionRemoteImpl(
+ private val callScope: CoroutineScope,
+ private val onActiveParticipantChanged: suspend (Participant?) -> Unit,
+ private val onParticipantsUpdated: suspend (Set<Participant>) -> Unit
+) : ParticipantExtensionRemote {
+ companion object {
+ internal const val TAG = CallExtensionScopeImpl.TAG + "(PCE)"
+ }
+
+ override var isSupported by Delegates.notNull<Boolean>()
+
+ /** The actions that are registered with the Participant extension */
+ internal val actions
+ get() = actionInitializers.keys.toIntArray()
+
+ // Maps a Capability to a receiver that allows the action to register itself with a listener
+ // and then return a Receiver that gets called when Cap exchange completes.
+ private val actionInitializers = HashMap<Int, ActionExchangeResult>()
+ // Manages callbacks that are applicable to sub-actions of the Participants
+ private val callbacks = ParticipantStateCallbackRepository()
+
+ // Participant specific state
+ private val participants = MutableStateFlow<Set<Participant>>(emptySet())
+ private val activeParticipantId = MutableStateFlow<String?>(null)
+
+ override fun addRaiseHandAction(
+ onRaisedHandsChanged: suspend (List<Participant>) -> Unit
+ ): RaiseHandAction {
+ val action = RaiseHandActionImpl(participants, onRaisedHandsChanged)
+ registerAction(
+ ParticipantExtensionImpl.RAISE_HAND_ACTION,
+ onRemoteConnected = action::connect
+ ) { isSupported ->
+ action.initialize(callScope, isSupported, callbacks)
+ }
+ return action
+ }
+
+ override fun addKickParticipantAction(): KickParticipantAction {
+ val action = KickParticipantActionImpl(participants)
+ registerAction(
+ ParticipantExtensionImpl.KICK_PARTICIPANT_ACTION,
+ onRemoteConnected = action::connect,
+ onInitialization = action::initialize
+ )
+ return action
+ }
+
+ /**
+ * Register an Action on the Participant extension that will be initialized and connected if the
+ * action is supported by the remote Call before [CallExtensionScope.onConnected] is called.
+ *
+ * @param action A unique identifier for the action that will be used by the remote side to
+ * identify this action.
+ * @param onRemoteConnected The callback called when the remote has connected and action events
+ * can be sent to the remote via [ParticipantActionsRemote].
+ * @param onInitialization The Action initializer, which allows the action to setup callbacks.
+ * via [ParticipantStateCallbackRepository] and determine if the action is supported by the
+ * remote Call.
+ */
+ private fun registerAction(
+ action: Int,
+ onRemoteConnected: (ParticipantActionsRemote?) -> Unit,
+ onInitialization: (Boolean) -> Unit
+ ) {
+ actionInitializers[action] = ActionExchangeResult(onInitialization, onRemoteConnected)
+ }
+
+ /**
+ * Capability exchange has completed and the [Capability] of the Participant extension has been
+ * negotiated with the remote call.
+ *
+ * @param negotiatedCapability The negotiated Participant capability or null if the remote
+ * doesn't support this capability.
+ * @param remote The remote interface which must be used by this extension to create the
+ * Participant extension on the remote side using the negotiated capability.
+ */
+ internal suspend fun onExchangeComplete(
+ negotiatedCapability: Capability?,
+ remote: CapabilityExchangeListenerRemote?
+ ) {
+ if (negotiatedCapability == null || remote == null) {
+ Log.i(TAG, "onNegotiated: remote is not capable")
+ isSupported = false
+ initializeNotSupportedActions()
+ return
+ }
+ Log.d(TAG, "onNegotiated: setup updates")
+ initializeParticipantUpdates()
+ initializeActionsLocally(negotiatedCapability)
+ val remoteBinder = connectActionsToRemote(negotiatedCapability, remote)
+ actionInitializers.forEach { connector -> connector.value.onRemoteConnected(remoteBinder) }
+ }
+
+ /**
+ * Connect Participant action Flows to the remote interface so we can start receiving changes to
+ * the Participant and associated action state.
+ *
+ * When [CapabilityExchangeListenerRemote.onCreateParticipantExtension] is called, the remote
+ * will send the initial state of each of the supported actions and then call
+ * [ParticipantStateListener.finishSync], which will provide us an interface to allow us to send
+ * participant action event requests.
+ *
+ * @param negotiatedCapability The negotiated Participant capability that contains a negotiated
+ * version and actions supported by both the local and remote Call.
+ * @param remote The interface used by the local call to create the Participant extension with
+ * the remote party if supported and allow for Participant state updates.
+ * @return The interface used by the local Call to send Participant action event requests.
+ */
+ private suspend fun connectActionsToRemote(
+ negotiatedCapability: Capability,
+ remote: CapabilityExchangeListenerRemote
+ ): ParticipantActionsRemote? = suspendCoroutine { continuation ->
+ val participantStateListener =
+ ParticipantStateListener(
+ updateParticipants = { newParticipants ->
+ callScope.launch {
+ Log.v(TAG, "updateParticipants: $newParticipants")
+ participants.emit(newParticipants)
+ }
+ },
+ updateActiveParticipantId = { newActiveParticipant ->
+ callScope.launch {
+ Log.v(TAG, "activeParticipant=$newActiveParticipant")
+ activeParticipantId.emit(newActiveParticipant)
+ }
+ },
+ updateRaisedHandIds = { newRaisedHands ->
+ callScope.launch {
+ Log.v(TAG, "raisedHands=$newRaisedHands")
+ callbacks.raisedHandIdsStateCallback?.invoke(newRaisedHands)
+ }
+ },
+ finishSync = { remoteBinder ->
+ callScope.launch {
+ Log.v(TAG, "finishSync complete, isNull=${remoteBinder == null}")
+ continuation.resume(remoteBinder)
+ }
+ }
+ )
+ remote.onCreateParticipantExtension(
+ negotiatedCapability.featureVersion,
+ negotiatedCapability.supportedActions,
+ participantStateListener
+ )
+ }
+
+ /** Setup callback updates when [participants] or [activeParticipantId] changes */
+ private fun initializeParticipantUpdates() {
+ participants
+ .onEach { participantsState -> onParticipantsUpdated(participantsState) }
+ .combine(activeParticipantId) { p, ap ->
+ ap?.let { p.firstOrNull { participant -> participant.id == ap } }
+ }
+ .distinctUntilChanged()
+ .onEach { activeParticipant -> onActiveParticipantChanged(activeParticipant) }
+ .onCompletion { Log.d(TAG, "participant flow complete") }
+ .launchIn(callScope)
+ }
+
+ /**
+ * Calls the [ActionExchangeResult.onInitialization] callback on each registered action
+ * (registered via [registerAction]) to initialize. Initialization uses the negotiated
+ * [Capability] to determine whether or not the registered action is supported by the remote and
+ * provides the ability for the action to register for remote state callbacks.
+ *
+ * @param negotiatedCapability The negotiated Participant [Capability] containing the
+ * Participant extension version and actions supported by both the local and remote Call.
+ */
+ private fun initializeActionsLocally(negotiatedCapability: Capability) {
+ for (action in actionInitializers) {
+ Log.d(TAG, "initializeActions: setup action=${action.key}")
+ if (negotiatedCapability.supportedActions.contains(action.key)) {
+ Log.d(TAG, "initializeActions: action=${action.key} supported")
+ action.value.onInitialization(true)
+ } else {
+ Log.d(TAG, "initializeActions: action=${action.key} not supported")
+ action.value.onInitialization(false)
+ }
+ }
+ }
+
+ /**
+ * In the case that participants are not supported, notify all actions that they are also not
+ * supported.
+ */
+ private fun initializeNotSupportedActions() {
+ Log.d(TAG, "initializeActions: no actions supported")
+ for (action in actionInitializers) {
+ action.value.onInitialization(false)
+ }
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandAction.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandAction.kt
index 4328d83..5082d4e 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandAction.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandAction.kt
@@ -16,110 +16,43 @@
package androidx.core.telecom.extensions
-import android.os.Build
-import android.util.Log
-import androidx.annotation.RequiresApi
import androidx.core.telecom.CallControlResult
-import androidx.core.telecom.CallException
-import androidx.core.telecom.internal.ParticipantActionsRemote
import androidx.core.telecom.util.ExperimentalAppActions
-import kotlin.properties.Delegates
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
/**
- * Implements the ability for the user to raise/lower their hand as well as allow the user to listen
- * to the hand raised states of all other participants
- *
- * @param participants The StateFlow containing the current set of participants in the call at any
- * given time.
- * @param onRaisedHandsChanged The callback that allows the user to listen to the state of
- * participants that have their hand raised
+ * The action used to determine if the raise hand action is supported by the calling application and
+ * notify the calling application when the user requests to raise or lower their hand.
*/
-// TODO: Refactor to Public API
-@RequiresApi(Build.VERSION_CODES.O)
@ExperimentalAppActions
-internal class RaiseHandAction(
- private val participants: StateFlow<Set<Participant>>,
- private val onRaisedHandsChanged: suspend (Set<Participant>) -> Unit
-) {
- companion object {
- const val TAG = CallExtensionsScope.TAG + "(RHCA)"
- }
-
+public interface RaiseHandAction {
/**
- * Whether or not raising/lowering hands is supported by the remote.
+ * Whether or not raising/lowering hands is supported by the calling application.
*
- * if `true`, then updates about raised hands will be notified. If `false`, then the remote
- * doesn't support this action this state will not be notified to the caller.
+ * if `true`, then updates about raised hands from the calling application will be notified. If
+ * `false`, then the calling application doesn't support this action and state changes will not
+ * be notified to the caller and [requestRaisedHandStateChange] requests will fail.
*
- * Should not be queried until [CallExtensionsScope.onConnected] is called.
+ * Must not be queried until [CallExtensionScope.onConnected] is called or an error will be
+ * thrown.
*/
- var isSupported by Delegates.notNull<Boolean>()
-
- // Contains the remote Binder interface used to notify the remote application of events
- private var remoteActions: ParticipantActionsRemote? = null
- // Contains the current state of participants that have their hands raised
- private val raisedHandState = MutableStateFlow<Set<Int>>(emptySet())
+ public var isSupported: Boolean
/**
- * Request the remote application to raise or lower this user's hand.
+ * Request the calling application to raise or lower this user's hand.
*
- * Note: This operation succeeding does not mean that the raised hand state of the user has
- * changed. It only means that the request was received by the remote application.
+ * Whether or not this user's hand is currently raised is determined by inspecting whether or
+ * not this [Participant] is currently included in the
+ * [ParticipantExtensionRemote.addRaiseHandAction] `onRaisedHandsChanged` callback `List`.
+ *
+ * Note: A [CallControlResult.Success] result does not mean that the raised hand state of the
+ * user has changed. It only means that the request was received by the remote application and
+ * processed. This can be used to gray out UI until the request has processed.
*
* @param isRaised `true` if this user has raised their hand, `false` if they have lowered their
* hand
* @return Whether or not the remote application received this event. This does not mean that
- * the operation succeeded, but rather the remote received the event successfully.
+ * the operation succeeded, but rather the remote received and processed the event
+ * successfully.
*/
- suspend fun requestRaisedHandStateChange(isRaised: Boolean): CallControlResult {
- Log.d(TAG, "setRaisedHandState: isRaised=$isRaised")
- if (remoteActions == null) {
- Log.w(TAG, "setRaisedHandState: no binder, isSupported=$isSupported")
- // TODO: This needs to have its own CallException result
- return CallControlResult.Error(CallException.ERROR_UNKNOWN)
- }
- val cb = ActionsResultCallback()
- remoteActions?.setHandRaised(isRaised, cb)
- val result = cb.waitForResponse()
- Log.d(TAG, "setRaisedHandState: isRaised=$isRaised, result=$result")
- return result
- }
-
- /** Called when the remote application has changed the raised hands state */
- private suspend fun raisedHandsStateChanged(raisedHands: Set<Int>) {
- Log.d(TAG, "raisedHandsStateChanged to $raisedHands")
- raisedHandState.emit(raisedHands)
- }
-
- /** Called when capability exchange has completed and we should setup the action */
- internal fun initialize(
- callScope: CoroutineScope,
- isSupported: Boolean,
- callbacks: ParticipantStateCallbackRepository
- ) {
- Log.d(TAG, "initialize, isSupported=$isSupported")
- this.isSupported = isSupported
- if (!isSupported) return
- callbacks.raisedHandsStateCallback = ::raisedHandsStateChanged
- participants
- .combine(raisedHandState) { p, rhs -> p.filter { rhs.contains(it.id) } }
- .distinctUntilChanged()
- .onEach { filtered -> onRaisedHandsChanged(filtered.toSet()) }
- .onCompletion { Log.d(TAG, "raised hands flow complete") }
- .launchIn(callScope)
- }
-
- /** Called when the remote has connected for Actions and events are available */
- internal fun connect(remote: ParticipantActionsRemote?) {
- Log.d(TAG, "connect: remote is null=${remote == null}")
- remoteActions = remote
- }
+ public suspend fun requestRaisedHandStateChange(isRaised: Boolean): CallControlResult
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandActionImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandActionImpl.kt
new file mode 100644
index 0000000..3523081
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandActionImpl.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlResult
+import androidx.core.telecom.CallException
+import androidx.core.telecom.internal.ParticipantActionsRemote
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlin.properties.Delegates
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Implements the ability for the user to raise/lower their hand as well as allow the user to listen
+ * to the hand raised states of all other participants
+ *
+ * @param participants The StateFlow containing the current set of participants in the call at any
+ * given time.
+ * @param onRaisedHandsChanged The callback that allows the user to listen to the state of
+ * participants that have their hand raised
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalAppActions::class)
+internal class RaiseHandActionImpl(
+ private val participants: StateFlow<Set<Participant>>,
+ private val onRaisedHandsChanged: suspend (List<Participant>) -> Unit
+) : RaiseHandAction {
+ companion object {
+ const val TAG = CallExtensionScopeImpl.TAG + "(RHCA)"
+ }
+
+ // Contains the remote Binder interface used to notify the remote application of events
+ private var remoteActions: ParticipantActionsRemote? = null
+ // Contains the current state of participant ids that have their hands raised
+ private val raisedHandIdsState = MutableStateFlow<List<String>>(emptyList())
+
+ override var isSupported by Delegates.notNull<Boolean>()
+
+ override suspend fun requestRaisedHandStateChange(isRaised: Boolean): CallControlResult {
+ Log.d(TAG, "setRaisedHandState: isRaised=$isRaised")
+ if (remoteActions == null) {
+ Log.w(TAG, "setRaisedHandState: no binder, isSupported=$isSupported")
+ // TODO: This needs to have its own CallException result
+ return CallControlResult.Error(CallException.ERROR_UNKNOWN)
+ }
+ val cb = ActionsResultCallback()
+ remoteActions?.setHandRaised(isRaised, cb)
+ val result = cb.waitForResponse()
+ Log.d(TAG, "setRaisedHandState: isRaised=$isRaised, result=$result")
+ return result
+ }
+
+ /** Called when the remote application has changed the raised hands state */
+ private suspend fun raisedHandIdsStateChanged(raisedHands: List<String>) {
+ Log.d(TAG, "raisedHandsStateChanged to $raisedHands")
+ raisedHandIdsState.emit(raisedHands)
+ }
+
+ /** Called when capability exchange has completed and we should setup the action */
+ internal fun initialize(
+ callScope: CoroutineScope,
+ isSupported: Boolean,
+ callbacks: ParticipantStateCallbackRepository
+ ) {
+ Log.d(TAG, "initialize, isSupported=$isSupported")
+ this.isSupported = isSupported
+ if (!isSupported) return
+ callbacks.raisedHandIdsStateCallback = ::raisedHandIdsStateChanged
+ participants
+ .combine(raisedHandIdsState) { p, rhs ->
+ rhs.mapNotNull { rh -> p.firstOrNull { it.id == rh } }
+ }
+ .distinctUntilChanged()
+ .onEach { filtered -> onRaisedHandsChanged(filtered) }
+ .onCompletion { Log.d(TAG, "raised hands flow complete") }
+ .launchIn(callScope)
+ }
+
+ /** Called when the remote has connected for Actions and events are available */
+ internal fun connect(remote: ParticipantActionsRemote?) {
+ Log.d(TAG, "connect: remote is null=${remote == null}")
+ remoteActions = remote
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandState.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandState.kt
index 2ef6793..48252b2 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandState.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandState.kt
@@ -16,81 +16,23 @@
package androidx.core.telecom.extensions
-import android.util.Log
-import androidx.core.telecom.internal.ParticipantActionCallbackRepository
-import androidx.core.telecom.internal.ParticipantStateListenerRemote
import androidx.core.telecom.util.ExperimentalAppActions
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
/**
- * Tracks the current raised hand state of all of the Participants of this call and notifies the
- * listener if a remote requests to change the user's raised hand state.
- *
- * @param participants The StateFlow containing the current set of Participants in the call
- * @param onHandRaisedChanged The action to perform when the remote InCallService requests to change
- * this user's raised hand state.
+ * Provides this application with the ability to notify remote surfaces (automotive, watch, etc..)
+ * when [Participant]s in the call have raised or lowered their hands.
*/
@ExperimentalAppActions
-internal class RaiseHandState(
- val participants: StateFlow<Set<Participant>>,
- private val onHandRaisedChanged: suspend (Boolean) -> Unit
-) {
- companion object {
- const val LOG_TAG = Extensions.LOG_TAG + "(RHAR)"
- }
-
- private val raisedHandsState: MutableStateFlow<Set<Participant>> = MutableStateFlow(emptySet())
-
+public interface RaiseHandState {
/**
- * Notify the remote InCallService of an update to the participants that have their hands raised
+ * Notify the remote surfaces of an update to the [Participant]s that have their hands raised at
+ * the current time. Any [Participant] that is in the call and is in [raisedHands] is considered
+ * to have their hand raised and any [Participant] that is in the call that is not in
+ * [raisedHands] is considered to have their hand lowered. The order of the [Participant]s in
+ * [raisedHands] MUST be in the order that the hands were raised, earliest raised hand first.
*
- * @param raisedHands The new set of Participants that have their hands raised.
+ * @param raisedHands The updated List of [Participant]s that have their hands raised, ordered
+ * as earliest raised hand to newest raised hand.
*/
- suspend fun updateRaisedHands(raisedHands: Set<Participant>) {
- raisedHandsState.emit(raisedHands)
- }
-
- /**
- * Connect this Action to a new remote that supports listening to this action's state updates.
- *
- * @param scope The CoroutineScope to use to update the remote
- * @param repository The event repository used to listen to state updates from the remote.
- * @param remote The interface used to communicate with the remote.
- */
- internal fun connect(
- scope: CoroutineScope,
- repository: ParticipantActionCallbackRepository,
- remote: ParticipantStateListenerRemote
- ) {
- Log.i(LOG_TAG, "initialize: sync state")
- repository.raiseHandStateCallback = ::raiseHandStateChanged
- // Send current state
- remote.updateRaisedHandsAction(raisedHandsState.value.map { it.id }.toIntArray())
- // Set up updates to the remote when the state changes
- participants
- .combine(raisedHandsState) { p, rhs -> p.intersect(rhs) }
- .distinctUntilChanged()
- .onEach {
- Log.i(LOG_TAG, "to remote: updateRaisedHands=$it")
- remote.updateRaisedHandsAction(it.map { p -> p.id }.toIntArray())
- }
- .launchIn(scope)
- }
-
- /**
- * Registered to be called when the remote InCallService has requested to change the raised hand
- * state of the user.
- *
- * @param state The new raised hand state, true if hand is raised, false if it is not.
- */
- private suspend fun raiseHandStateChanged(state: Boolean) {
- Log.d(LOG_TAG, "raisedHandStateChanged: updated state: $state")
- onHandRaisedChanged(state)
- }
+ public suspend fun updateRaisedHands(raisedHands: List<Participant>)
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandStateImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandStateImpl.kt
new file mode 100644
index 0000000..2c6cf4c
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/RaiseHandStateImpl.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.util.Log
+import androidx.core.telecom.internal.ParticipantActionCallbackRepository
+import androidx.core.telecom.internal.ParticipantStateListenerRemote
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Tracks the current raised hand state of all of the Participants of this call and notifies the
+ * listener if a remote requests to change the user's raised hand state.
+ *
+ * @param participants The StateFlow containing the current set of Participants in the call
+ * @param initialRaisedHands The initial List of [Participant]s that have their hands raised in
+ * priority order from first raised to last raised.
+ * @param onHandRaisedChanged The action to perform when the remote InCallService requests to change
+ * this user's raised hand state.
+ */
+@OptIn(ExperimentalAppActions::class)
+internal class RaiseHandStateImpl(
+ private val participants: StateFlow<Set<Participant>>,
+ initialRaisedHands: List<Participant>,
+ private val onHandRaisedChanged: suspend (Boolean) -> Unit
+) : RaiseHandState {
+ companion object {
+ const val LOG_TAG = Extensions.LOG_TAG + "(RHSI)"
+ }
+
+ private val raisedHandsState: MutableStateFlow<List<Participant>> =
+ MutableStateFlow(initialRaisedHands)
+
+ override suspend fun updateRaisedHands(raisedHands: List<Participant>) {
+ raisedHandsState.emit(raisedHands)
+ }
+
+ /**
+ * Connect this Action to a new remote that supports listening to this action's state updates.
+ *
+ * @param scope The CoroutineScope to use to update the remote
+ * @param repository The event repository used to listen to state updates from the remote.
+ * @param remote The interface used to communicate with the remote.
+ */
+ internal fun connect(
+ scope: CoroutineScope,
+ repository: ParticipantActionCallbackRepository,
+ remote: ParticipantStateListenerRemote
+ ) {
+ Log.i(LOG_TAG, "initialize: sync state")
+ repository.raiseHandStateCallback = ::raiseHandStateChanged
+ // Send current state
+ remote.updateRaisedHandsAction(raisedHandsState.value)
+ // Set up updates to the remote when the state changes
+ participants
+ .combine(raisedHandsState) { p, rhs -> rhs.filter { it in p } }
+ .distinctUntilChanged()
+ .onEach {
+ Log.i(LOG_TAG, "to remote: updateRaisedHands=$it")
+ remote.updateRaisedHandsAction(it)
+ }
+ .launchIn(scope)
+ }
+
+ /**
+ * Registered to be called when the remote InCallService has requested to change the raised hand
+ * state of the user.
+ *
+ * @param state The new raised hand state, true if hand is raised, false if it is not.
+ */
+ private suspend fun raiseHandStateChanged(state: Boolean) {
+ Log.d(LOG_TAG, "raisedHandStateChanged: updated state: $state")
+ onHandRaisedChanged(state)
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt
index fab3151..a08a7a0 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt
@@ -25,7 +25,7 @@
import androidx.core.telecom.extensions.IParticipantActions
import androidx.core.telecom.extensions.IParticipantStateListener
import androidx.core.telecom.extensions.Participant
-import androidx.core.telecom.extensions.ParticipantExtension
+import androidx.core.telecom.extensions.ParticipantParcelable
import androidx.core.telecom.util.ExperimentalAppActions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
@@ -52,8 +52,11 @@
*/
var raiseHandStateCallback: (suspend (Boolean) -> Unit)? = null
- /** The callback that is called when the remote InCallService requests to kick a participant. */
- var kickParticipantCallback: (suspend (Participant) -> Unit)? = null
+ /**
+ * The callback that is called when the remote InCallService requests to kick a participant
+ * using its id.
+ */
+ var kickParticipantCallback: (suspend (String) -> Unit)? = null
/** Listener used to handle event callbacks from the remote. */
val eventListener =
@@ -68,11 +71,11 @@
}
}
- override fun kickParticipant(participant: Participant, cb: IActionsResultCallback?) {
+ override fun kickParticipant(participantId: String, cb: IActionsResultCallback?) {
cb?.let {
coroutineScope.launch {
- Log.i(LOG_TAG, "from remote: kickParticipant=$participant")
- kickParticipantCallback?.invoke(participant)
+ Log.i(LOG_TAG, "from remote: kickParticipant=$participantId")
+ kickParticipantCallback?.invoke(participantId)
ActionsResultCallbackRemote(cb).onSuccess()
}
}
@@ -83,7 +86,11 @@
/** Remote interface used by InCallServices to send action events to the VOIP application. */
@ExperimentalAppActions
internal class ParticipantActionsRemote(binder: IParticipantActions) :
- IParticipantActions by binder
+ IParticipantActions by binder {
+ fun kickParticipant(participant: Participant, cb: IActionsResultCallback?) {
+ kickParticipant(participant.id, cb)
+ }
+}
/**
* Remote interface used to notify the ICS of participant state information
@@ -91,8 +98,25 @@
* @param binder The remote binder interface to wrap
*/
@ExperimentalAppActions
-internal class ParticipantStateListenerRemote(binder: IParticipantStateListener) :
- IParticipantStateListener by binder
+internal class ParticipantStateListenerRemote(private val binder: IParticipantStateListener) {
+ fun updateParticipants(participants: Set<Participant>) {
+ binder.updateParticipants(
+ participants.map(Participant::toParticipantParcelable).toTypedArray()
+ )
+ }
+
+ fun updateActiveParticipant(activeParticipant: Participant?) {
+ binder.updateActiveParticipant(activeParticipant?.id)
+ }
+
+ fun updateRaisedHandsAction(participants: List<Participant>) {
+ binder.updateRaisedHandsAction(participants.map { it.id }.toTypedArray())
+ }
+
+ fun finishSync(actions: IParticipantActions) {
+ binder.finishSync(actions)
+ }
+}
/**
* The remote interface used to begin capability exchange with the InCallService.
@@ -117,24 +141,22 @@
@ExperimentalAppActions
internal class ParticipantStateListener(
private val updateParticipants: (Set<Participant>) -> Unit,
- private val updateActiveParticipant: (Int?) -> Unit,
- private val updateRaisedHands: (Set<Int>) -> Unit,
+ private val updateActiveParticipantId: (String?) -> Unit,
+ private val updateRaisedHandIds: (List<String>) -> Unit,
private val finishSync: (ParticipantActionsRemote?) -> Unit
) : IParticipantStateListener.Stub() {
- override fun updateParticipants(participants: Array<out Participant>?) {
- updateParticipants.invoke(participants?.toSet() ?: emptySet())
+ override fun updateParticipants(participants: Array<out ParticipantParcelable>?) {
+ updateParticipants.invoke(
+ participants?.map { Participant(it.id, it.name) }?.toSet() ?: emptySet()
+ )
}
- override fun updateActiveParticipant(activeParticipant: Int) {
- if (activeParticipant < 0) {
- updateActiveParticipant.invoke(null)
- } else {
- updateActiveParticipant.invoke(activeParticipant)
- }
+ override fun updateActiveParticipant(activeParticipantId: String?) {
+ updateActiveParticipantId.invoke(activeParticipantId)
}
- override fun updateRaisedHandsAction(participants: IntArray?) {
- updateRaisedHands.invoke(participants?.toSet() ?: emptySet())
+ override fun updateRaisedHandsAction(participants: Array<out String>?) {
+ updateRaisedHandIds.invoke(participants?.toList() ?: emptyList())
}
override fun finishSync(cb: IParticipantActions?) {
@@ -154,12 +176,9 @@
* torn down.
*/
@ExperimentalAppActions
-internal class CapabilityExchangeRepository(connectionScope: CoroutineScope) {
- companion object {
- private const val LOG_TAG = Extensions.LOG_TAG + "(CER)"
- }
+internal class CapabilityExchangeRepository(private val connectionScope: CoroutineScope) {
- /** A request to create the [ParticipantExtension] has been received */
+ /** A request to create the ParticipantExtension has been received */
var onCreateParticipantExtension:
((CoroutineScope, Set<Int>, ParticipantStateListenerRemote) -> Unit)? =
null
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
index 8777b6c..e96ccbd 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSession.kt
@@ -56,6 +56,7 @@
val onDisconnectCallback: suspend (disconnectCause: DisconnectCause) -> Unit,
val onSetActiveCallback: suspend () -> Unit,
val onSetInactiveCallback: suspend () -> Unit,
+ private val preCallEndpointMapping: PreCallEndpoints? = null,
private val callChannels: CallChannels,
private val onEventCallback: suspend (event: String, extras: Bundle) -> Unit,
private val blockingSessionExecution: CompletableDeferred<Unit>
@@ -70,17 +71,22 @@
private val mIsCurrentEndpointSet = CompletableDeferred<Unit>()
private val mIsAvailableEndpointsSet = CompletableDeferred<Unit>()
private val mIsCurrentlyDisplayingVideo = attributes.isVideoCall()
+ internal val mJetpackToPlatformCallEndpoint: HashMap<ParcelUuid, CallEndpoint> = HashMap()
companion object {
private val TAG: String = CallSession::class.java.simpleName
private const val WAIT_FOR_BT_TO_CONNECT_TIMEOUT: Long = 1000L
private const val SWITCH_TO_SPEAKER_TIMEOUT: Long = WAIT_FOR_BT_TO_CONNECT_TIMEOUT + 1000L
+ private const val INITIAL_ENDPOINT_SWITCH_TIMEOUT: Long = 3000L
+ private const val DELAY_INITIAL_ENDPOINT_SWITCH: Long = 1000L
}
+ @VisibleForTesting
fun getIsCurrentEndpointSet(): CompletableDeferred<Unit> {
return mIsCurrentEndpointSet
}
+ @VisibleForTesting
fun getIsAvailableEndpointsSet(): CompletableDeferred<Unit> {
return mIsAvailableEndpointsSet
}
@@ -95,25 +101,67 @@
mAvailableEndpoints = endpoints
}
+ /**
+ * =========================================================================================
+ * Audio Updates
+ * =========================================================================================
+ */
+ @VisibleForTesting
+ internal fun toRemappedCallEndpointCompat(platformEndpoint: CallEndpoint): CallEndpointCompat {
+ if (platformEndpoint.endpointType == CallEndpoint.TYPE_BLUETOOTH) {
+ val key = platformEndpoint.endpointName
+ val btEndpointMapping = preCallEndpointMapping?.mBluetoothEndpoints
+ return if (btEndpointMapping != null && btEndpointMapping.containsKey(key)) {
+ val existingEndpoint = btEndpointMapping[key]!!
+ mJetpackToPlatformCallEndpoint[existingEndpoint.identifier] = platformEndpoint
+ existingEndpoint
+ } else {
+ EndpointUtils.Api34PlusImpl.toCallEndpointCompat(platformEndpoint)
+ }
+ } else {
+ val key = platformEndpoint.endpointType
+ val nonBtEndpointMapping = preCallEndpointMapping?.mNonBluetoothEndpoints
+ return if (nonBtEndpointMapping != null && nonBtEndpointMapping.containsKey(key)) {
+ val existingEndpoint = nonBtEndpointMapping[key]!!
+ mJetpackToPlatformCallEndpoint[existingEndpoint.identifier] = platformEndpoint
+ existingEndpoint
+ } else {
+ EndpointUtils.Api34PlusImpl.toCallEndpointCompat(platformEndpoint)
+ }
+ }
+ }
+
override fun onCallEndpointChanged(endpoint: CallEndpoint) {
+ // cache the previous call endpoint for maybeSwitchToSpeakerOnHeadsetDisconnect. This
+ // is used to determine if the last endpoint was BT and the new endpoint is EARPIECE.
val previousCallEndpoint = mCurrentCallEndpoint
- mCurrentCallEndpoint = EndpointUtils.Api34PlusImpl.toCallEndpointCompat(endpoint)
+ // due to the [CallsManager#getAvailableStartingCallEndpoints] API, endpoints the client
+ // has can be different from the ones coming from the platform. Hence, a remapping is needed
+ mCurrentCallEndpoint = toRemappedCallEndpointCompat(endpoint)
+ // send the current call endpoint out to the client
callChannels.currentEndpointChannel.trySend(mCurrentCallEndpoint!!).getOrThrow()
Log.i(TAG, "onCallEndpointChanged: endpoint=[$endpoint]")
+ // maybeSwitchToSpeakerOnCallStart needs to know when the initial current endpoint is set
if (!mIsCurrentEndpointSet.isCompleted) {
mIsCurrentEndpointSet.complete(Unit)
Log.i(TAG, "onCallEndpointChanged: mCurrentCallEndpoint was set")
}
maybeSwitchToSpeakerOnHeadsetDisconnect(mCurrentCallEndpoint!!, previousCallEndpoint)
// clear out the last user requested CallEndpoint. It's only used to determine if the
- // change in current endpoints was intentional.
- mLastClientRequestedEndpoint = null
+ // change in current endpoints was intentional for maybeSwitchToSpeakerOnHeadsetDisconnect
+ if (mLastClientRequestedEndpoint?.type == endpoint.endpointType) {
+ mLastClientRequestedEndpoint = null
+ }
}
override fun onAvailableCallEndpointsChanged(endpoints: List<CallEndpoint>) {
- mAvailableEndpoints = EndpointUtils.Api34PlusImpl.toCallEndpointsCompat(endpoints)
+ // due to the [CallsManager#getAvailableStartingCallEndpoints] API, endpoints the client
+ // has can be different from the ones coming from the platform. Hence, a remapping is needed
+ mAvailableEndpoints = endpoints.map { toRemappedCallEndpointCompat(it) }.sorted()
+ // send the current call endpoints out to the client
callChannels.availableEndpointChannel.trySend(mAvailableEndpoints).getOrThrow()
Log.i(TAG, "onAvailableCallEndpointsChanged: endpoints=[$endpoints]")
+ // maybeSwitchToSpeakerOnCallStart needs to know when the initial current endpoints are set
if (!mIsAvailableEndpointsSet.isCompleted) {
mIsAvailableEndpointsSet.complete(Unit)
Log.i(TAG, "onAvailableCallEndpointsChanged: mAvailableEndpoints was set")
@@ -124,13 +172,16 @@
callChannels.isMutedChannel.trySend(isMuted).getOrThrow()
}
- override fun onCallStreamingFailed(reason: Int) {
- TODO("Implement with the CallStreaming code")
- }
-
- override fun onEvent(event: String, extras: Bundle) {
- Log.i(TAG, "onEvent: received $event")
- CoroutineScope(coroutineContext).launch { onEventCallback(event, extras) }
+ /**
+ * This function should only be run once at the start of CallSession to determine if the
+ * starting CallEndpointCompat should be switched based on the call properties or user request.
+ */
+ suspend fun maybeSwitchStartingEndpoint(preferredStartingCallEndpoint: CallEndpointCompat?) {
+ if (preferredStartingCallEndpoint != null) {
+ switchStartingCallEndpointOnCallStart(preferredStartingCallEndpoint)
+ } else {
+ maybeSwitchToSpeakerOnCallStart()
+ }
}
/**
@@ -173,7 +224,7 @@
// only switch to speaker if BT did not connect
if (!isBluetoothConnected()) {
Log.i(TAG, "maybeDelaySwitchToSpeaker: BT did not connect in time!")
- switchToEndpoint(speakerCompat)
+ requestEndpointChange(speakerCompat)
return true
}
Log.i(TAG, "maybeDelaySwitchToSpeaker: BT connected! voiding speaker switch.")
@@ -182,7 +233,7 @@
// otherwise, immediately change from earpiece to speaker because the platform is
// not in the process of connecting a BT device.
Log.i(TAG, "maybeDelaySwitchToSpeaker: no BT route available.")
- switchToEndpoint(speakerCompat)
+ requestEndpointChange(speakerCompat)
return true
}
}
@@ -192,12 +243,27 @@
mCurrentCallEndpoint!!.type == CallEndpoint.TYPE_BLUETOOTH
}
- private fun switchToEndpoint(endpoint: CallEndpointCompat) {
- mPlatformInterface?.requestCallEndpointChange(
- EndpointUtils.Api34PlusImpl.toCallEndpoint(endpoint),
- Runnable::run,
- {}
- )
+ suspend fun switchStartingCallEndpointOnCallStart(startingCallEndpoint: CallEndpointCompat) {
+ try {
+ withTimeout(INITIAL_ENDPOINT_SWITCH_TIMEOUT) {
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: before awaitAll")
+ awaitAll(mIsAvailableEndpointsSet)
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: after awaitAll")
+ launch {
+ // Delay the switch to a new [CallEndpointCompat] if there is a BT device
+ // because the request will be overridden once the BT device connects!
+ if (mAvailableEndpoints.any { it.isBluetoothType() }) {
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: BT delay START")
+ delay(DELAY_INITIAL_ENDPOINT_SWITCH)
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: BT delay END")
+ }
+ val res = requestEndpointChange(startingCallEndpoint)
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: result=$res")
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "switchStartingCallEndpointOnCallStart: hit exception=[$e]")
+ }
}
/**
@@ -240,6 +306,26 @@
}
/**
+ * =========================================================================================
+ * Call Event Updates
+ * =========================================================================================
+ */
+ override fun onCallStreamingFailed(reason: Int) {
+ TODO("Implement with the CallStreaming code")
+ }
+
+ override fun onEvent(event: String, extras: Bundle) {
+ Log.i(TAG, "onEvent: received $event")
+ CoroutineScope(coroutineContext).launch { onEventCallback(event, extras) }
+ }
+
+ /**
+ * =========================================================================================
+ * CallControl
+ * =========================================================================================
+ */
+
+ /**
* CallControl is set by CallsManager#addCall when the CallControl object is returned by the
* platform
*/
@@ -290,19 +376,34 @@
return result.getCompleted()
}
- suspend fun requestEndpointChange(endpoint: CallEndpoint): CallControlResult {
- val result: CompletableDeferred<CallControlResult> = CompletableDeferred()
+ suspend fun requestEndpointChange(endpoint: CallEndpointCompat): CallControlResult {
+ val job: CompletableDeferred<CallControlResult> = CompletableDeferred()
// cache the last CallEndpoint the user requested to reference in
// onCurrentCallEndpointChanged. This is helpful for determining if the user intentionally
// requested a CallEndpoint switch or a headset was disconnected ...
- mLastClientRequestedEndpoint = EndpointUtils.Api34PlusImpl.toCallEndpointCompat(endpoint)
- mPlatformInterface?.requestCallEndpointChange(
- endpoint,
+ mLastClientRequestedEndpoint = endpoint
+ val potentiallyRemappedEndpoint: CallEndpoint =
+ if (mJetpackToPlatformCallEndpoint.containsKey(endpoint.identifier)) {
+ mJetpackToPlatformCallEndpoint[endpoint.identifier]!!
+ } else {
+ EndpointUtils.Api34PlusImpl.toCallEndpoint(endpoint)
+ }
+
+ if (mPlatformInterface == null) {
+ return CallControlResult.Error(androidx.core.telecom.CallException.ERROR_UNKNOWN)
+ }
+
+ mPlatformInterface!!.requestCallEndpointChange(
+ potentiallyRemappedEndpoint,
Runnable::run,
- CallControlReceiver(result)
+ CallControlReceiver(job)
)
- result.await()
- return result.getCompleted()
+ job.await()
+ val platformResult = job.getCompleted()
+ if (platformResult != CallControlResult.Success()) {
+ mLastClientRequestedEndpoint = null
+ }
+ return platformResult
}
suspend fun disconnect(disconnectCause: DisconnectCause): CallControlResult {
@@ -409,9 +510,7 @@
override suspend fun requestEndpointChange(
endpoint: CallEndpointCompat
): CallControlResult {
- return session.requestEndpointChange(
- EndpointUtils.Api34PlusImpl.toCallEndpoint(endpoint)
- )
+ return session.requestEndpointChange(endpoint)
}
// Send these events out to the client to collect
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt
index b450a38..64e199d 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallSessionLegacy.kt
@@ -38,6 +38,8 @@
import androidx.core.telecom.internal.utils.EndpointUtils.Companion.isBluetoothAvailable
import androidx.core.telecom.internal.utils.EndpointUtils.Companion.isEarpieceEndpoint
import androidx.core.telecom.internal.utils.EndpointUtils.Companion.isWiredHeadsetOrBtEndpoint
+import androidx.core.telecom.internal.utils.EndpointUtils.Companion.toCallEndpointCompat
+import androidx.core.telecom.internal.utils.EndpointUtils.Companion.toCallEndpointsCompat
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -57,18 +59,23 @@
val onSetActiveCallback: suspend () -> Unit,
val onSetInactiveCallback: suspend () -> Unit,
val onEventCallback: suspend (event: String, extras: Bundle) -> Unit,
+ private val preferredStartingCallEndpoint: CallEndpointCompat? = null,
+ private val preCallEndpointMapping: PreCallEndpoints? = null,
private val blockingSessionExecution: CompletableDeferred<Unit>
) : android.telecom.Connection() {
// instance vars
private val TAG: String = CallSessionLegacy::class.java.simpleName
private var mCachedBluetoothDevices: ArrayList<BluetoothDevice> = ArrayList()
+ private var mAlreadyRequestedStartingEndpointSwitch: Boolean = false
private var mAlreadyRequestedSpeaker: Boolean = false
+ private var mPreviousCallEndpoint: CallEndpointCompat? = null
private var mCurrentCallEndpoint: CallEndpointCompat? = null
+ private var mAvailableCallEndpoints: List<CallEndpointCompat>? = null
private var mLastClientRequestedEndpoint: CallEndpointCompat? = null
companion object {
- private val TAG: String = CallSessionLegacy::class.java.simpleName
private const val WAIT_FOR_BT_TO_CONNECT_TIMEOUT: Long = 1000L
+ private const val DELAY_INITIAL_ENDPOINT_SWITCH: Long = 1000L
// CallStates. All these states mirror the values in the platform.
const val STATE_INITIALIZING = 0
const val STATE_NEW = 1
@@ -106,28 +113,82 @@
* Audio Updates
* =========================================================================================
*/
+ @VisibleForTesting
+ internal fun toRemappedCallEndpointCompat(endpoint: CallEndpointCompat): CallEndpointCompat {
+ if (endpoint.isBluetoothType()) {
+ val key = endpoint.name.toString()
+ val btEndpointMapping = preCallEndpointMapping?.mBluetoothEndpoints
+ return if (btEndpointMapping != null && btEndpointMapping.containsKey(key)) {
+ btEndpointMapping[key]!!
+ } else {
+ endpoint
+ }
+ } else {
+ val key = endpoint.type
+ val nonBtEndpointMapping = preCallEndpointMapping?.mNonBluetoothEndpoints
+ return if (nonBtEndpointMapping != null && nonBtEndpointMapping.containsKey(key)) {
+ nonBtEndpointMapping[key]!!
+ } else {
+ endpoint
+ }
+ }
+ }
+
+ private fun setCurrentCallEndpoint(state: CallAudioState) {
+ mPreviousCallEndpoint = mCurrentCallEndpoint
+ mCurrentCallEndpoint = toRemappedCallEndpointCompat(toCallEndpointCompat(state))
+ callChannels.currentEndpointChannel.trySend(mCurrentCallEndpoint!!).getOrThrow()
+ }
+
+ private fun setAvailableCallEndpoints(state: CallAudioState) {
+ val availableEndpoints =
+ toCallEndpointsCompat(state).map { toRemappedCallEndpointCompat(it) }.sorted()
+ mAvailableCallEndpoints = availableEndpoints
+ callChannels.availableEndpointChannel.trySend(availableEndpoints).getOrThrow()
+ }
+
override fun onCallAudioStateChanged(state: CallAudioState) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
Api28PlusImpl.refreshBluetoothDeviceCache(mCachedBluetoothDevices, state)
}
- val previousCallEndpoint = mCurrentCallEndpoint
- mCurrentCallEndpoint = EndpointUtils.toCallEndpointCompat(state)
- callChannels.currentEndpointChannel.trySend(mCurrentCallEndpoint!!).getOrThrow()
-
- val availableEndpoints = EndpointUtils.toCallEndpointsCompat(state)
- callChannels.availableEndpointChannel.trySend(availableEndpoints).getOrThrow()
-
+ setCurrentCallEndpoint(state)
+ setAvailableCallEndpoints(state)
callChannels.isMutedChannel.trySend(state.isMuted).getOrThrow()
-
- maybeSwitchToSpeakerOnCallStart(mCurrentCallEndpoint!!, availableEndpoints)
+ // On the first call audio state change, determine if the platform started on the correct
+ // audio route. Otherwise, request an endpoint switch.
+ switchStartingCallEndpointOnCallStart(mAvailableCallEndpoints!!)
+ // In the event the users headset disconnects, they will likely want to continue the call
+ // via the speakerphone
maybeSwitchToSpeakerOnHeadsetDisconnect(
mCurrentCallEndpoint!!,
- previousCallEndpoint,
- availableEndpoints
+ mPreviousCallEndpoint,
+ mAvailableCallEndpoints!!,
)
// clear out the last user requested CallEndpoint. It's only used to determine if the
// change in current endpoints was intentional.
- mLastClientRequestedEndpoint = null
+ if (mLastClientRequestedEndpoint?.type == mCurrentCallEndpoint?.type) {
+ mLastClientRequestedEndpoint = null
+ }
+ }
+
+ private fun switchStartingCallEndpointOnCallStart(endpoints: List<CallEndpointCompat>) {
+ if (preferredStartingCallEndpoint != null) {
+ if (!mAlreadyRequestedStartingEndpointSwitch) {
+ CoroutineScope(coroutineContext).launch {
+ // Delay the switch to a new [CallEndpointCompat] if there is a BT device
+ // because the request will be overridden once the BT device connects!
+ if (endpoints.any { it.isBluetoothType() }) {
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: BT delay START")
+ delay(DELAY_INITIAL_ENDPOINT_SWITCH)
+ Log.i(TAG, "switchStartingCallEndpointOnCallStart: BT delay END")
+ }
+ requestEndpointChange(preferredStartingCallEndpoint)
+ }
+ }
+ } else {
+ maybeSwitchToSpeakerOnCallStart(mCurrentCallEndpoint!!, endpoints)
+ }
+ mAlreadyRequestedStartingEndpointSwitch = true
}
/**
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
index d002d1d..00e5be4 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
@@ -30,6 +30,7 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.CallsManager
import androidx.core.telecom.internal.utils.Utils
import java.util.UUID
@@ -61,6 +62,8 @@
val onSetActive: suspend () -> Unit,
val onSetInactive: suspend () -> Unit,
val onEvent: suspend (event: String, extras: Bundle) -> Unit,
+ val preferredStartingCallEndpoint: CallEndpointCompat? = null,
+ val preCallEndpointMapping: PreCallEndpoints? = null,
val execution: CompletableDeferred<Unit>
)
@@ -222,6 +225,8 @@
targetRequest.onSetActive,
targetRequest.onSetInactive,
targetRequest.onEvent,
+ targetRequest.preferredStartingCallEndpoint,
+ targetRequest.preCallEndpointMapping,
targetRequest.execution
)
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpoints.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpoints.kt
new file mode 100644
index 0000000..0371d2e
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/PreCallEndpoints.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.internal
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.core.telecom.CallEndpointCompat
+import kotlinx.coroutines.channels.SendChannel
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class PreCallEndpoints(
+ var mCurrentDevices: MutableList<CallEndpointCompat>,
+ var mSendChannel: SendChannel<List<CallEndpointCompat>>
+) {
+ // earpiece, speaker, unknown, wired_headset
+ val mNonBluetoothEndpoints: HashMap<Int, CallEndpointCompat> = HashMap()
+
+ // all bt endpoints
+ val mBluetoothEndpoints: HashMap<String, CallEndpointCompat> = HashMap()
+
+ companion object {
+ private val TAG: String = PreCallEndpoints::class.java.simpleName.toString()
+
+ // endpoints added constants
+ const val ALREADY_TRACKING_ENDPOINT: Int = 0
+ const val START_TRACKING_NEW_ENDPOINT: Int = 1
+
+ // endpoints removed constants
+ const val NOT_TRACKING_REMOVED_ENDPOINT: Int = 0
+ const val STOP_TRACKING_REMOVED_ENDPOINT: Int = 1
+ }
+
+ init {
+ for (device in mCurrentDevices) {
+ if (device.isBluetoothType()) {
+ mBluetoothEndpoints[device.name.toString()] = device
+ } else {
+ mNonBluetoothEndpoints[device.type] = device
+ }
+ }
+ }
+
+ fun endpointsAddedUpdate(addedCallEndpoints: List<CallEndpointCompat>) {
+ var addedDevicesCount = 0
+ for (maybeNewEndpoint in addedCallEndpoints) {
+ addedDevicesCount += maybeAddCallEndpoint(maybeNewEndpoint)
+ }
+ if (addedDevicesCount > 0) {
+ updateClient()
+ } else {
+ Log.d(TAG, "endpointsAddedUpdate: no new added endpoints, not updating client!")
+ }
+ }
+
+ fun endpointsRemovedUpdate(removedCallEndpoints: List<CallEndpointCompat>) {
+ var removedDevicesCount = 0
+ for (maybeRemovedDevice in removedCallEndpoints) {
+ removedDevicesCount += maybeRemoveCallEndpoint(maybeRemovedDevice)
+ }
+ if (removedDevicesCount > 0) {
+ mCurrentDevices =
+ (mBluetoothEndpoints.values + mNonBluetoothEndpoints.values).toMutableList()
+ updateClient()
+ } else {
+ Log.d(TAG, "endpointsRemovedUpdate: no removed endpoints, not updating client!")
+ }
+ }
+
+ internal fun isCallEndpointBeingTracked(endpoint: CallEndpointCompat?): Boolean {
+ return mCurrentDevices.contains(endpoint)
+ }
+
+ @VisibleForTesting
+ internal fun maybeAddCallEndpoint(endpoint: CallEndpointCompat): Int {
+ if (endpoint.isBluetoothType()) {
+ if (!mBluetoothEndpoints.containsKey(endpoint.name.toString())) {
+ mBluetoothEndpoints[endpoint.name.toString()] = endpoint
+ mCurrentDevices.add(endpoint)
+ return START_TRACKING_NEW_ENDPOINT
+ } else {
+ return ALREADY_TRACKING_ENDPOINT
+ }
+ } else {
+ if (!mNonBluetoothEndpoints.containsKey(endpoint.type)) {
+ mNonBluetoothEndpoints[endpoint.type] = endpoint
+ mCurrentDevices.add(endpoint)
+ return START_TRACKING_NEW_ENDPOINT
+ } else {
+ return ALREADY_TRACKING_ENDPOINT
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun maybeRemoveCallEndpoint(endpoint: CallEndpointCompat): Int {
+ if (endpoint.isBluetoothType()) {
+ if (mBluetoothEndpoints.containsKey(endpoint.name.toString())) {
+ mBluetoothEndpoints.remove(endpoint.name.toString())
+ return STOP_TRACKING_REMOVED_ENDPOINT
+ } else {
+ return NOT_TRACKING_REMOVED_ENDPOINT
+ }
+ } else {
+ if (mNonBluetoothEndpoints.containsKey(endpoint.type)) {
+ mNonBluetoothEndpoints.remove(endpoint.type)
+ return STOP_TRACKING_REMOVED_ENDPOINT
+ } else {
+ return NOT_TRACKING_REMOVED_ENDPOINT
+ }
+ }
+ }
+
+ private fun updateClient() {
+ // Sort by endpoint type. The first element has the highest priority!
+ mCurrentDevices.sort()
+ mSendChannel.trySend(mCurrentDevices)
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/AudioManagerUtil.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/AudioManagerUtil.kt
new file mode 100644
index 0000000..2777997
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/AudioManagerUtil.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.internal.utils
+
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+@RequiresApi(23)
+internal class AudioManagerUtil {
+ companion object {
+ fun getAvailableAudioDevices(audioManager: AudioManager): List<AudioDeviceInfo> {
+ return if (Build.VERSION.SDK_INT >= 31) {
+ AudioManager31PlusImpl.getDevices(audioManager)
+ } else {
+ AudioManager23PlusImpl.getDevices(audioManager)
+ }
+ }
+ }
+
+ @RequiresApi(31)
+ object AudioManager31PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun getDevices(audioManager: AudioManager): List<AudioDeviceInfo> {
+ return audioManager.availableCommunicationDevices
+ }
+ }
+
+ @RequiresApi(23)
+ object AudioManager23PlusImpl {
+ @JvmStatic
+ @DoNotInline
+ fun getDevices(audioManager: AudioManager): List<AudioDeviceInfo> {
+ return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).toList()
+ }
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt
index d2ea85a..54b3f6d 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/utils/EndpointUtils.kt
@@ -17,17 +17,115 @@
package androidx.core.telecom.internal.utils
import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioDeviceInfo
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.P
+import android.os.ParcelUuid
import android.telecom.CallAudioState
+import android.util.Log
+import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.core.telecom.CallEndpointCompat
+import androidx.core.telecom.CallEndpointCompat.Companion.EndpointType
+import androidx.core.telecom.R
+import java.util.UUID
@RequiresApi(Build.VERSION_CODES.O)
internal class EndpointUtils {
companion object {
+ private val TAG: String = EndpointUtils::class.java.simpleName.toString()
+
+ /** [AudioDeviceInfo]s to [CallEndpointCompat]s */
+ fun getEndpointsFromAudioDeviceInfo(
+ c: Context,
+ adiArr: List<AudioDeviceInfo>?
+ ): List<CallEndpointCompat> {
+ if (adiArr == null) {
+ return listOf()
+ }
+ val endpoints: MutableList<CallEndpointCompat> = mutableListOf()
+ val omittedDevices = StringBuilder("omitting devices =[")
+ adiArr.toList().forEach { audioDeviceInfo ->
+ val endpoint = getEndpointFromAudioDeviceInfo(c, audioDeviceInfo)
+ if (endpoint.type != CallEndpointCompat.TYPE_UNKNOWN) {
+ endpoints.add(endpoint)
+ } else {
+ omittedDevices.append(
+ "(type=[${audioDeviceInfo.type}]," +
+ " name=[${audioDeviceInfo.productName}]),"
+ )
+ }
+ }
+ omittedDevices.append("]")
+ Log.i(TAG, omittedDevices.toString())
+ // Sort by endpoint type. The first element has the highest priority!
+ endpoints.sort()
+ return endpoints
+ }
+
+ /** [AudioDeviceInfo] --> [CallEndpointCompat] */
+ private fun getEndpointFromAudioDeviceInfo(
+ c: Context,
+ adi: AudioDeviceInfo
+ ): CallEndpointCompat {
+ val newEndpoint =
+ CallEndpointCompat(
+ remapAudioDeviceNameToEndpointName(c, adi),
+ remapAudioDeviceTypeToCallEndpointType(adi.type),
+ ParcelUuid(UUID.randomUUID())
+ )
+ if (SDK_INT >= P && newEndpoint.isBluetoothType()) {
+ newEndpoint.mMackAddress = adi.address
+ }
+ return newEndpoint
+ }
+
+ private fun remapAudioDeviceNameToEndpointName(
+ c: Context,
+ audioDeviceInfo: AudioDeviceInfo
+ ): String {
+ return when (audioDeviceInfo.type) {
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE ->
+ c.getString(R.string.callendpoint_name_earpiece)
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER ->
+ c.getString(R.string.callendpoint_name_speaker)
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+ AudioDeviceInfo.TYPE_USB_DEVICE,
+ AudioDeviceInfo.TYPE_USB_ACCESSORY,
+ AudioDeviceInfo.TYPE_USB_HEADSET ->
+ c.getString(R.string.callendpoint_name_wiredheadset)
+ else -> audioDeviceInfo.productName.toString()
+ }
+ }
+
+ internal fun remapAudioDeviceTypeToCallEndpointType(
+ audioDeviceInfoType: Int
+ ): (@EndpointType Int) {
+ return when (audioDeviceInfoType) {
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> CallEndpointCompat.TYPE_EARPIECE
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> CallEndpointCompat.TYPE_SPEAKER
+ // Wired Headset Devices
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+ AudioDeviceInfo.TYPE_USB_DEVICE,
+ AudioDeviceInfo.TYPE_USB_ACCESSORY,
+ AudioDeviceInfo.TYPE_USB_HEADSET -> CallEndpointCompat.TYPE_WIRED_HEADSET
+ // Bluetooth Devices
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+ AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+ AudioDeviceInfo.TYPE_HEARING_AID,
+ AudioDeviceInfo.TYPE_BLE_HEADSET,
+ AudioDeviceInfo.TYPE_BLE_SPEAKER,
+ AudioDeviceInfo.TYPE_BLE_BROADCAST -> CallEndpointCompat.TYPE_BLUETOOTH
+ // Everything else is defaulted to TYPE_UNKNOWN
+ else -> CallEndpointCompat.TYPE_UNKNOWN
+ }
+ }
+
fun getSpeakerEndpoint(endpoints: List<CallEndpointCompat>): CallEndpointCompat? {
for (e in endpoints) {
if (e.type == CallEndpointCompat.TYPE_SPEAKER) {
@@ -82,7 +180,7 @@
)
}
if (hasBluetoothType(bitMask)) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if (SDK_INT >= P) {
endpoints.addAll(BluetoothApi28PlusImpl.getBluetoothEndpoints(state))
} else {
endpoints.add(
@@ -187,17 +285,7 @@
}
@JvmStatic
- fun toCallEndpointsCompat(
- endpoints: List<android.telecom.CallEndpoint>
- ): List<CallEndpointCompat> {
- val res = ArrayList<CallEndpointCompat>()
- for (e in endpoints) {
- res.add(CallEndpointCompat(e.endpointName, e.endpointType, e.identifier))
- }
- return res
- }
-
- @JvmStatic
+ @DoNotInline
fun toCallEndpoint(e: CallEndpointCompat): android.telecom.CallEndpoint {
return android.telecom.CallEndpoint(e.name, e.type, e.identifier)
}
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-af/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-af/strings.xml
index e752dd2..e5c55e0 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-af/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Oorstuk"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Kabelkopstuk"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Luidspreker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-am/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-am/strings.xml
index e752dd2..66301fb 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-am/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ማዳመጫ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ባለ ገመድ ማዳመጫ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ድምፅ ማውጫ"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-ar/strings.xml b/core/core-telecom/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..8963293
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-ar/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"سماعة أذن"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"سماعة رأس سلكية"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"مكبِّر صوت"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-as/strings.xml b/core/core-telecom/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..697b692
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-as/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ইয়েৰপিচ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"তাঁৰযুক্ত হেডছেট"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"স্পীকাৰ"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-az/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-az/strings.xml
index e752dd2..e62a4d0 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-az/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Qulaqlıq"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Simli qulaqlıq"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Dinamik"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-b+sr+Latn/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-b+sr+Latn/strings.xml
index e752dd2..6afc996 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-b+sr+Latn/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Slušalica"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Žičane slušalice"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Zvučnik"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-be/strings.xml b/core/core-telecom/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000..cb6122ec
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-be/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Дынамік"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Правадная гарнітура"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Гучная сувязь"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-bg/strings.xml b/core/core-telecom/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000..91d381b
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-bg/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Слушалка"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Слушалки с кабел"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Високоговорител"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-bn/strings.xml b/core/core-telecom/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000..5acdea3
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-bn/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ইয়ারপিস"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ওয়্যার্ড হেডসেট"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"স্পিকার"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-bs/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-bs/strings.xml
index e752dd2..6afc996 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-bs/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Slušalica"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Žičane slušalice"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Zvučnik"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ca/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ca/strings.xml
index e752dd2..4b8b784 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ca/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auricular"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Auriculars amb cable"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Altaveu"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-cs/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-cs/strings.xml
index e752dd2..9bafe17 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-cs/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Sluchátko"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Kabelová náhlavní souprava"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Reproduktor"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-da/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-da/strings.xml
index e752dd2..257ec4e 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-da/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Højttaler"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Headset med ledning"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Højttaler"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-de/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-de/strings.xml
index e752dd2..0ce2c07 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-de/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Kopfhörer"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Kabelgebundenes Headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Lautsprecher"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-el/strings.xml b/core/core-telecom/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000..83770ab
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-el/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Ακουστικό τηλεφώνου"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Ενσύρματα ακουστικά"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Ηχείο"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-en-rAU/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-en-rAU/strings.xml
index e752dd2..ae0ae39 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-en-rAU/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-en-rCA/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-en-rCA/strings.xml
index e752dd2..ae0ae39 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-en-rCA/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-en-rGB/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-en-rGB/strings.xml
index e752dd2..ae0ae39 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-en-rGB/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-en-rIN/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-en-rIN/strings.xml
index e752dd2..ae0ae39 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-en-rIN/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-en-rXC/strings.xml b/core/core-telecom/src/main/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..db0d654
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-en-rXC/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-es-rUS/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-es-rUS/strings.xml
index e752dd2..1b87fa9 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-es-rUS/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auricular"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Auriculares con cable"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Bocina"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-es/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-es/strings.xml
index e752dd2..5af28da 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-es/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auricular"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Auriculares con cable"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Altavoz"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-et/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-et/strings.xml
index e752dd2..3d2cc0c 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-et/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Kuular"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Juhtmega peakomplekt"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Kõlar"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-eu/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-eu/strings.xml
index e752dd2..16cc5e9 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-eu/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Aurikularra"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Entzungailu kableduna"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Bozgorailua"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-fa/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-fa/strings.xml
index e752dd2..e159d7c 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-fa/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"گوشی"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"هدست سیمی"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"بلندگو"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-fi/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-fi/strings.xml
index e752dd2..1772c83 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-fi/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Kaiutin"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Langallinen kuulokemikrofoni"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Kaiutin"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-fr-rCA/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-fr-rCA/strings.xml
index e752dd2..33d0ca4 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-fr-rCA/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Écouteur"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Écouteurs filaires"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Haut-parleur"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-fr/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-fr/strings.xml
index e752dd2..efba6ec 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-fr/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Écouteur"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Casque filaire"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Haut-parleur"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-gl/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-gl/strings.xml
index e752dd2..73f3cf5 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-gl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auricular"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Auriculares con cable"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Altofalante"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-gu/strings.xml b/core/core-telecom/src/main/res/values-gu/strings.xml
new file mode 100644
index 0000000..eb7a9ee
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-gu/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ઇયરપીસ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"વાયર્ડ હૅડસેટ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"સ્પીકર"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-hi/strings.xml b/core/core-telecom/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000..3a847ac
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-hi/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ईयरपीस"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"वायर वाला हेडसेट"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"स्पीकर"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-hr/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-hr/strings.xml
index e752dd2..a812b93 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-hr/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Zvučnik"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Žičane slušalice"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Zvučnik"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-hu/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-hu/strings.xml
index e752dd2..1175ec2 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-hu/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Fülhallgató"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Vezetékes headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Hangszóró"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-hy/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-hy/strings.xml
index e752dd2..0f1a0d2 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-hy/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Լսափող"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Լարով ականջակալ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Բարձրախոս"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-in/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-in/strings.xml
index e752dd2..8e053cc 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-in/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Headset berkabel"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-is/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-is/strings.xml
index e752dd2..c0befd0 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-is/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Hátalari"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Höfuðtól með snúru"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Hátalari"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-it/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-it/strings.xml
index e752dd2..1ffa5ad 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-it/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auricolare"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Cuffie con cavo"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-iw/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-iw/strings.xml
index e752dd2..9ec7ee9 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-iw/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"אוזניה"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"אוזניות חוטיות"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"רמקול"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ja/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ja/strings.xml
index e752dd2..fecacc1 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ja/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"受話口"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"有線ヘッドセット"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"スピーカー"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-ka/strings.xml b/core/core-telecom/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000..d93c507
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-ka/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ყურმილი"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"სადენიანი ყურსაცვამი"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"დინამიკი"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-kk/strings.xml b/core/core-telecom/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000..6867ece
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-kk/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Телефон динамигі"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Сымды гарнитура"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Динамик"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-km/strings.xml b/core/core-telecom/src/main/res/values-km/strings.xml
new file mode 100644
index 0000000..0fc75bb
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-km/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ឧបករណ៍ស្ដាប់សំឡេង"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"កាសមានខ្សែ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ឧបករណ៍បំពងសំឡេង"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-kn/strings.xml b/core/core-telecom/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000..4b006c3
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-kn/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ಇಯರ್ಪೀಸ್"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ವೈಯರ್ಡ್ ಹೆಡ್ಸೆಟ್"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ಸ್ಪೀಕರ್"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ko/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ko/strings.xml
index e752dd2..6dd6a7d 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ko/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"스피커"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"유선 헤드셋"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"스피커"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ky/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ky/strings.xml
index e752dd2..4de5b55 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ky/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Кулакчын"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Зымдуу гарнитура"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Динамик"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-lo/strings.xml b/core/core-telecom/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000..fae8f3f
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-lo/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ຫູຟັງ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ຊຸດຫູຟັງແບບມີສາຍ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ລຳໂພງ"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-lt/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-lt/strings.xml
index e752dd2..ab5b490 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-lt/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Garsiakalbis prie ausies"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Laidinės ausinės"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Garsiakalbis"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-lv/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-lv/strings.xml
index e752dd2..dbbe825 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-lv/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auss skaļrunis"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Vadu austiņas"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Skaļrunis"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-mk/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-mk/strings.xml
index e752dd2..73a43de 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-mk/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Слушалка"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Жичени слушалки"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Звучник"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-ml/strings.xml b/core/core-telecom/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000..7efb254
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-ml/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ഇയർഫോൺ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"വയേർഡ് ഹെഡ്സെറ്റ്"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"സ്പീക്കർ"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-mn/strings.xml b/core/core-telecom/src/main/res/values-mn/strings.xml
new file mode 100644
index 0000000..3f929e9
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-mn/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Чихний спикер"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Утастай чихэвч"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Чанга яригч"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-mr/strings.xml b/core/core-telecom/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000..343c6c2
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-mr/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"इअरपीस"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"वायर्ड हेडसेट"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"स्पीकर"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ms/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ms/strings.xml
index e752dd2..1ffead0 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ms/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Alat dengar"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Set kepala berwayar"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Pembesar suara"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-my/strings.xml b/core/core-telecom/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000..a62aea0
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-my/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"တယ်လီဖုန်းနားခွက်"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ကြိုးတပ် မိုက်ခွက်ပါနားကြပ်"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"စပီကာ"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-nb/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-nb/strings.xml
index e752dd2..387722e 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-nb/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Ørehøyttaler"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Hodetelefoner med ledning"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Høyttaler"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-ne/strings.xml b/core/core-telecom/src/main/res/values-ne/strings.xml
new file mode 100644
index 0000000..30d613e
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-ne/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"इयरपिस"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"तारसहितको हेडसेट"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"स्पिकर"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-nl/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-nl/strings.xml
index e752dd2..c496439 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-nl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Oortelefoon"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Bedrade headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-or/strings.xml b/core/core-telecom/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..2a480c7
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-or/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ଇୟରପିସ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ତାରଯୁକ୍ତ ହେଡସେଟ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ସ୍ପିକର"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-pa/strings.xml b/core/core-telecom/src/main/res/values-pa/strings.xml
new file mode 100644
index 0000000..ebc4300
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-pa/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ਈਅਰਪੀਸ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ਤਾਰ ਵਾਲਾ ਹੈੱਡਸੈੱਟ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ਸਪੀਕਰ"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-pl/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-pl/strings.xml
index e752dd2..768b794 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-pl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Słuchawka"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Przewodowy zestaw słuchawkowy"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Głośnik"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-pt-rBR/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-pt-rBR/strings.xml
index e752dd2..de52295 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-pt-rBR/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Minifone de ouvido"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Fone de ouvido com fio"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Alto-falante"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-pt-rPT/strings.xml b/core/core-telecom/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..ee657a4
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Auricular"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Auscultadores com microfone integrado com fios"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Altifalante"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-pt/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-pt/strings.xml
index e752dd2..de52295 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-pt/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Minifone de ouvido"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Fone de ouvido com fio"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Alto-falante"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ro/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ro/strings.xml
index e752dd2..a1b63da 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ro/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Cască"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Set de căști-microfon cu fir"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Difuzor"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-ru/strings.xml b/core/core-telecom/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..9f1c287
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-ru/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Наушник"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Проводная гарнитура"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Колонка"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-si/strings.xml b/core/core-telecom/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000..748d5c2
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-si/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"සවන් කඩ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"රැහැන්ගත කළ හෙඩ්සෙට්"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ස්පීකරය"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-sk/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-sk/strings.xml
index e752dd2..b6075f4 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-sk/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Slúchadlo"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Káblové slúchadlá s mikrofónom"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Reproduktor"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-sl/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-sl/strings.xml
index e752dd2..5ca1886 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-sl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Slušalka"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Žične slušalke z mikrofonom"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Zvočnik"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-sq/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-sq/strings.xml
index e752dd2..c932e8d 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-sq/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Receptor"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Kufje me tel"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Altoparlant"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-sr/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-sr/strings.xml
index e752dd2..199a682 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-sr/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Слушалица"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Жичане слушалице"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Звучник"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-sv/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-sv/strings.xml
index e752dd2..aec1523 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-sv/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Lur"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Kabelanslutet headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Högtalare"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-sw/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-sw/strings.xml
index e752dd2..af27136d 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-sw/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Spika ya sikioni"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Vifaa vya sauti vyenye waya"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Spika"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-ta/strings.xml b/core/core-telecom/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000..b3678bb
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-ta/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ஒலி கேட்கும் பகுதி"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"வயர் ஹெட்செட்"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ஸ்பீக்கர்"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-te/strings.xml b/core/core-telecom/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000..e387597
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-te/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ఇయర్పీస్"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"వైర్ ఉన్న హెడ్సెట్"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"స్పీకర్"</string>
+</resources>
diff --git a/core/core-telecom/src/main/res/values-th/strings.xml b/core/core-telecom/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000..103ceb1
--- /dev/null
+++ b/core/core-telecom/src/main/res/values-th/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"หูฟังโทรศัพท์"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"ชุดหูฟังแบบมีสาย"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"ลำโพง"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-tl/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-tl/strings.xml
index e752dd2..50ac56c 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-tl/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Earpiece"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Wired na headset"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Speaker"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-tr/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-tr/strings.xml
index e752dd2..600c6d2 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-tr/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Kulaklık"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Kablolu mikrofonlu kulaklık"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Hoparlör"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-uk/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-uk/strings.xml
index e752dd2..d8580f9 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-uk/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Динамік"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Дротова гарнітура"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Колонка"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-ur/strings.xml
similarity index 60%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-ur/strings.xml
index e752dd2..d577f0b 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-ur/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"ایئر پیس"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"تار والا ہیڈ سیٹ"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"اسپیکر"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-uz/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-uz/strings.xml
index e752dd2..679f5dd 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-uz/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Quloq karnaychasi"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Simli garnitura"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Karnay"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-vi/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-vi/strings.xml
index e752dd2..a4c82f9 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-vi/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Loa tai nghe"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Tai nghe có dây"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Loa ngoài"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-zh-rCN/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-zh-rCN/strings.xml
index e752dd2..0d4efb7 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-zh-rCN/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"手机听筒"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"有线头戴式耳机"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"扬声器"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-zh-rHK/strings.xml
similarity index 62%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-zh-rHK/strings.xml
index e752dd2..4e99591 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-zh-rHK/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"聽筒"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"有線耳機"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"喇叭"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-zh-rTW/strings.xml
similarity index 62%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-zh-rTW/strings.xml
index e752dd2..4c586c7 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-zh-rTW/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"耳機"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"有線耳機"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"喇叭"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/core/core-telecom/src/main/res/values-zu/strings.xml
similarity index 61%
copy from pdf/pdf-viewer/src/main/res/values/styles.xml
copy to core/core-telecom/src/main/res/values-zu/strings.xml
index e752dd2..7aab3c5 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/core/core-telecom/src/main/res/values-zu/strings.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +13,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- -->
+ -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
-</resources>
\ No newline at end of file
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="callendpoint_name_earpiece" msgid="4519059065203201851">"Isipikha sendlebe"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6723516311603411573">"Iheadset enentambo"</string>
+ <string name="callendpoint_name_speaker" msgid="623806810712383295">"Isipikha"</string>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-w840dp/dimens.xml b/core/core-telecom/src/main/res/values/strings.xml
similarity index 60%
rename from pdf/pdf-viewer/src/main/res/values-w840dp/dimens.xml
rename to core/core-telecom/src/main/res/values/strings.xml
index 077536c..98e241b 100644
--- a/pdf/pdf-viewer/src/main/res/values-w840dp/dimens.xml
+++ b/core/core-telecom/src/main/res/values/strings.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 The Android Open Source Project
@@ -16,5 +15,10 @@
-->
<resources>
- <dimen name="viewer_doc_padding_x">80dp</dimen>
+ <!-- The user-visible name of the earpiece type CallEndpoint -->
+ <string name="callendpoint_name_earpiece">Earpiece</string>
+ <!-- The user-visible name of the wired headset type CallEndpoint -->
+ <string name="callendpoint_name_wiredheadset">Wired headset</string>
+ <!-- The user-visible name of the speaker type CallEndpoint -->
+ <string name="callendpoint_name_speaker">Speaker</string>
</resources>
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 8d7d895..2c49d7e 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1850,7 +1850,7 @@
package androidx.core.os {
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public abstract sealed class BufferFillPolicy {
+ @RequiresApi(api=35) public abstract sealed class BufferFillPolicy {
field public static final androidx.core.os.BufferFillPolicy.Companion Companion;
field public static final androidx.core.os.BufferFillPolicy DISCARD;
field public static final androidx.core.os.BufferFillPolicy RING_BUFFER;
@@ -1926,7 +1926,7 @@
method public static boolean postDelayed(android.os.Handler, Runnable, Object?, long);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class HeapProfileRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.HeapProfileRequestBuilder> {
+ @RequiresApi(api=35) public final class HeapProfileRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.HeapProfileRequestBuilder> {
ctor public HeapProfileRequestBuilder();
method public androidx.core.os.HeapProfileRequestBuilder setBufferSizeKb(int bufferSizeKb);
method public androidx.core.os.HeapProfileRequestBuilder setDurationMs(int durationMs);
@@ -1934,7 +1934,7 @@
method public androidx.core.os.HeapProfileRequestBuilder setTrackJavaAllocations(boolean traceJavaAllocations);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class JavaHeapDumpRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.JavaHeapDumpRequestBuilder> {
+ @RequiresApi(api=35) public final class JavaHeapDumpRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.JavaHeapDumpRequestBuilder> {
ctor public JavaHeapDumpRequestBuilder();
method public androidx.core.os.JavaHeapDumpRequestBuilder setBufferSizeKb(int bufferSizeKb);
}
@@ -1998,13 +1998,13 @@
}
public final class Profiling {
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static kotlinx.coroutines.flow.Flow<android.os.ProfilingResult> registerForAllProfilingResults(android.content.Context context);
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static void registerForAllProfilingResults(android.content.Context context, java.util.concurrent.Executor executor, java.util.function.Consumer<android.os.ProfilingResult> listener);
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static void requestProfiling(android.content.Context context, androidx.core.os.ProfilingRequest profilingRequest, java.util.concurrent.Executor? executor, java.util.function.Consumer<android.os.ProfilingResult>? listener);
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static void unregisterForAllProfilingResults(android.content.Context context, java.util.function.Consumer<android.os.ProfilingResult> listener);
+ method @RequiresApi(api=35) public static kotlinx.coroutines.flow.Flow<android.os.ProfilingResult> registerForAllProfilingResults(android.content.Context context);
+ method @RequiresApi(api=35) public static void registerForAllProfilingResults(android.content.Context context, java.util.concurrent.Executor executor, java.util.function.Consumer<android.os.ProfilingResult> listener);
+ method @RequiresApi(api=35) public static void requestProfiling(android.content.Context context, androidx.core.os.ProfilingRequest profilingRequest, java.util.concurrent.Executor? executor, java.util.function.Consumer<android.os.ProfilingResult>? listener);
+ method @RequiresApi(api=35) public static void unregisterForAllProfilingResults(android.content.Context context, java.util.function.Consumer<android.os.ProfilingResult> listener);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class ProfilingRequest {
+ @RequiresApi(api=35) public final class ProfilingRequest {
method public android.os.CancellationSignal? getCancellationSignal();
method public android.os.Bundle getParams();
method public int getProfilingType();
@@ -2015,20 +2015,20 @@
property public final String? tag;
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public abstract class ProfilingRequestBuilder<T extends androidx.core.os.ProfilingRequestBuilder<T>> {
+ @RequiresApi(api=35) public abstract class ProfilingRequestBuilder<T extends androidx.core.os.ProfilingRequestBuilder<T>> {
method public final androidx.core.os.ProfilingRequest build();
method public final T setCancellationSignal(android.os.CancellationSignal cancellationSignal);
method public final T setTag(String tag);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class StackSamplingRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.StackSamplingRequestBuilder> {
+ @RequiresApi(api=35) public final class StackSamplingRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.StackSamplingRequestBuilder> {
ctor public StackSamplingRequestBuilder();
method public androidx.core.os.StackSamplingRequestBuilder setBufferSizeKb(int bufferSizeKb);
method public androidx.core.os.StackSamplingRequestBuilder setDurationMs(int durationMs);
method public androidx.core.os.StackSamplingRequestBuilder setSamplingFrequencyHz(int samplingFrequencyHz);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class SystemTraceRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.SystemTraceRequestBuilder> {
+ @RequiresApi(api=35) public final class SystemTraceRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.SystemTraceRequestBuilder> {
ctor public SystemTraceRequestBuilder();
method public androidx.core.os.SystemTraceRequestBuilder setBufferFillPolicy(androidx.core.os.BufferFillPolicy bufferFillPolicy);
method public androidx.core.os.SystemTraceRequestBuilder setBufferSizeKb(int bufferSizeKb);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index c60ec0c..4f16f2d 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2243,7 +2243,7 @@
package androidx.core.os {
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public abstract sealed class BufferFillPolicy {
+ @RequiresApi(api=35) public abstract sealed class BufferFillPolicy {
field public static final androidx.core.os.BufferFillPolicy.Companion Companion;
field public static final androidx.core.os.BufferFillPolicy DISCARD;
field public static final androidx.core.os.BufferFillPolicy RING_BUFFER;
@@ -2319,7 +2319,7 @@
method public static boolean postDelayed(android.os.Handler, Runnable, Object?, long);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class HeapProfileRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.HeapProfileRequestBuilder> {
+ @RequiresApi(api=35) public final class HeapProfileRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.HeapProfileRequestBuilder> {
ctor public HeapProfileRequestBuilder();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected android.os.Bundle getParams();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected int getProfilingType();
@@ -2330,7 +2330,7 @@
method public androidx.core.os.HeapProfileRequestBuilder setTrackJavaAllocations(boolean traceJavaAllocations);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class JavaHeapDumpRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.JavaHeapDumpRequestBuilder> {
+ @RequiresApi(api=35) public final class JavaHeapDumpRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.JavaHeapDumpRequestBuilder> {
ctor public JavaHeapDumpRequestBuilder();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected android.os.Bundle getParams();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected int getProfilingType();
@@ -2397,13 +2397,13 @@
}
public final class Profiling {
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static kotlinx.coroutines.flow.Flow<android.os.ProfilingResult> registerForAllProfilingResults(android.content.Context context);
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static void registerForAllProfilingResults(android.content.Context context, java.util.concurrent.Executor executor, java.util.function.Consumer<android.os.ProfilingResult> listener);
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static void requestProfiling(android.content.Context context, androidx.core.os.ProfilingRequest profilingRequest, java.util.concurrent.Executor? executor, java.util.function.Consumer<android.os.ProfilingResult>? listener);
- method @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public static void unregisterForAllProfilingResults(android.content.Context context, java.util.function.Consumer<android.os.ProfilingResult> listener);
+ method @RequiresApi(api=35) public static kotlinx.coroutines.flow.Flow<android.os.ProfilingResult> registerForAllProfilingResults(android.content.Context context);
+ method @RequiresApi(api=35) public static void registerForAllProfilingResults(android.content.Context context, java.util.concurrent.Executor executor, java.util.function.Consumer<android.os.ProfilingResult> listener);
+ method @RequiresApi(api=35) public static void requestProfiling(android.content.Context context, androidx.core.os.ProfilingRequest profilingRequest, java.util.concurrent.Executor? executor, java.util.function.Consumer<android.os.ProfilingResult>? listener);
+ method @RequiresApi(api=35) public static void unregisterForAllProfilingResults(android.content.Context context, java.util.function.Consumer<android.os.ProfilingResult> listener);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class ProfilingRequest {
+ @RequiresApi(api=35) public final class ProfilingRequest {
method public android.os.CancellationSignal? getCancellationSignal();
method public android.os.Bundle getParams();
method public int getProfilingType();
@@ -2414,7 +2414,7 @@
property public final String? tag;
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public abstract class ProfilingRequestBuilder<T extends androidx.core.os.ProfilingRequestBuilder<T>> {
+ @RequiresApi(api=35) public abstract class ProfilingRequestBuilder<T extends androidx.core.os.ProfilingRequestBuilder<T>> {
method public final androidx.core.os.ProfilingRequest build();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected abstract android.os.Bundle getParams();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected abstract int getProfilingType();
@@ -2423,7 +2423,7 @@
method public final T setTag(String tag);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class StackSamplingRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.StackSamplingRequestBuilder> {
+ @RequiresApi(api=35) public final class StackSamplingRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.StackSamplingRequestBuilder> {
ctor public StackSamplingRequestBuilder();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected android.os.Bundle getParams();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected int getProfilingType();
@@ -2433,7 +2433,7 @@
method public androidx.core.os.StackSamplingRequestBuilder setSamplingFrequencyHz(int samplingFrequencyHz);
}
- @RequiresApi(api=android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public final class SystemTraceRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.SystemTraceRequestBuilder> {
+ @RequiresApi(api=35) public final class SystemTraceRequestBuilder extends androidx.core.os.ProfilingRequestBuilder<androidx.core.os.SystemTraceRequestBuilder> {
ctor public SystemTraceRequestBuilder();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected android.os.Bundle getParams();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.SUBCLASSES) protected int getProfilingType();
diff --git a/core/core/build.gradle b/core/core/build.gradle
index 92490a9..bae3ce7 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -106,4 +106,5 @@
description = "Provides backward-compatible implementations of Android platform APIs and " +
"features."
failOnDeprecationWarnings = false
+ samples(project(":core:core:core-samples"))
}
diff --git a/core/core/samples/build.gradle b/core/core/samples/build.gradle
new file mode 100644
index 0000000..0d008cd
--- /dev/null
+++ b/core/core/samples/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.build.LibraryType
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+dependencies {
+ compileOnly(project(":annotation:annotation-sampled"))
+ implementation(project(":core:core"))
+}
+android {
+ compileSdk 35
+ namespace "androidx.core.samples"
+}
+androidx {
+ name = "AndroidX Core Samples"
+ type = LibraryType.SAMPLES
+ mavenVersion = LibraryVersions.CORE
+ inceptionYear = "2024"
+ description = "Samples for the AndroidX Core Libraries"
+}
diff --git a/core/core/samples/src/main/java/androidx/core/os/ProfilingSamples.kt b/core/core/samples/src/main/java/androidx/core/os/ProfilingSamples.kt
new file mode 100644
index 0000000..716675b
--- /dev/null
+++ b/core/core/samples/src/main/java/androidx/core/os/ProfilingSamples.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.content.Context
+import android.os.CancellationSignal
+import android.os.ProfilingResult
+import androidx.annotation.Sampled
+import java.util.function.Consumer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.flow.flowOn
+
+/** Sample showing how to request a java heap dump with various optional parameters. */
+@Sampled
+fun requestJavaHeapDump(context: Context) {
+ val listener =
+ Consumer<ProfilingResult> { profilingResult ->
+ if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
+ doSomethingWithMyFile(profilingResult.resultFilePath)
+ } else {
+ doSomethingWithFailure(profilingResult.errorCode, profilingResult.errorMessage)
+ }
+ }
+
+ requestProfiling(
+ context,
+ JavaHeapDumpRequestBuilder()
+ .setBufferSizeKb(123 /* Requested buffer size in KB */)
+ .setTag("tag" /* Caller supplied tag for identification */)
+ .build(),
+ Dispatchers.IO.asExecutor(), // Your choice of executor for the callback to occur on.
+ listener
+ )
+}
+
+/**
+ * Sample showing how to request a heap profile with various optional parameters and optional
+ * cancellation after the event of interest was captured.
+ */
+@Sampled
+fun requestHeapProfile(context: Context) {
+ val listener =
+ Consumer<ProfilingResult> { profilingResult ->
+ if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
+ doSomethingWithMyFile(profilingResult.resultFilePath)
+ } else {
+ doSomethingWithFailure(profilingResult.errorCode, profilingResult.errorMessage)
+ }
+ }
+
+ val cancellationSignal = CancellationSignal()
+
+ requestProfiling(
+ context,
+ HeapProfileRequestBuilder()
+ .setBufferSizeKb(1000 /* Requested buffer size in KB */)
+ .setDurationMs(5 * 1000 /* Requested profiling duration in milliseconds */)
+ .setTrackJavaAllocations(true)
+ .setSamplingIntervalBytes(100 /* Requested sampling interval in bytes */)
+ .setTag("tag" /* Caller supplied tag for identification */)
+ .setCancellationSignal(cancellationSignal)
+ .build(),
+ Dispatchers.IO.asExecutor(), // Your choice of executor for the callback to occur on.
+ listener
+ )
+
+ // Optionally, wait for something interesting to happen and then stop the profiling to receive
+ // the result as is.
+ cancellationSignal.cancel()
+}
+
+/**
+ * Sample showing how to request a stack sample with various optional parameters and optional
+ * cancellation after the event of interest was captured.
+ */
+@Sampled
+fun requestStackSampling(context: Context) {
+ val listener =
+ Consumer<ProfilingResult> { profilingResult ->
+ if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
+ doSomethingWithMyFile(profilingResult.resultFilePath)
+ } else {
+ doSomethingWithFailure(profilingResult.errorCode, profilingResult.errorMessage)
+ }
+ }
+
+ val cancellationSignal = CancellationSignal()
+
+ requestProfiling(
+ context,
+ StackSamplingRequestBuilder()
+ .setBufferSizeKb(1000 /* Requested buffer size in KB */)
+ .setDurationMs(10 * 1000 /* Requested profiling duration in millisconds */)
+ .setSamplingFrequencyHz(100 /* Requested sampling frequency */)
+ .setTag("tag" /* Caller supplied tag for identification */)
+ .setCancellationSignal(cancellationSignal)
+ .build(),
+ Dispatchers.IO.asExecutor(), // Your choice of executor for the callback to occur on.
+ listener
+ )
+
+ // Optionally, wait for something interesting to happen and then stop the profiling to receive
+ // the result as is.
+ cancellationSignal.cancel()
+}
+
+/**
+ * Sample showing how to request a system trace with various optional parameters and optional
+ * cancellation after the event of interest was captured.
+ */
+@Sampled
+fun requestSystemTrace(context: Context) {
+ val listener =
+ Consumer<ProfilingResult> { profilingResult ->
+ if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
+ doSomethingWithMyFile(profilingResult.resultFilePath)
+ } else {
+ doSomethingWithFailure(profilingResult.errorCode, profilingResult.errorMessage)
+ }
+ }
+
+ val cancellationSignal = CancellationSignal()
+
+ requestProfiling(
+ context,
+ SystemTraceRequestBuilder()
+ .setBufferSizeKb(1000 /* Requested buffer size in KB */)
+ .setDurationMs(60 * 1000 /* Requested profiling duration in millisconds */)
+ .setBufferFillPolicy(BufferFillPolicy.RING_BUFFER /* Buffer fill policy */)
+ .setTag("tag" /* Caller supplied tag for identification */)
+ .setCancellationSignal(cancellationSignal)
+ .build(),
+ Dispatchers.IO.asExecutor(), // Your choice of executor for the callback to occur on.
+ listener
+ )
+
+ // Optionally, wait for something interesting to happen and then stop the profiling to receive
+ // the result as is.
+ cancellationSignal.cancel()
+}
+
+/** Sample showing how to register a listener for all profiling results from your app. */
+@Sampled
+fun registerForAllProfilingResultsSample(context: Context) {
+ val listener =
+ Consumer<ProfilingResult> { profilingResult ->
+ if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
+ doSomethingWithMyFile(profilingResult.resultFilePath)
+ } else {
+ doSomethingWithFailure(profilingResult.errorCode, profilingResult.errorMessage)
+ }
+ }
+
+ registerForAllProfilingResults(
+ context,
+ Dispatchers.IO.asExecutor(), // Your choice of executor for the callback to occur on.
+ listener
+ )
+}
+
+/** Sample showing how to register a flow for all profiling results from your app. */
+@Sampled
+suspend fun registerForAllProfilingResultsFlowSample(context: Context) {
+ val flow = registerForAllProfilingResults(context)
+
+ flow
+ .flowOn(Dispatchers.IO) // Consume files on a background thread
+ .collect { profilingResult ->
+ if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
+ doSomethingWithMyFile(profilingResult.resultFilePath)
+ } else {
+ doSomethingWithFailure(profilingResult.errorCode, profilingResult.errorMessage)
+ }
+ }
+}
+
+@Suppress("UNUSED_PARAMETER") fun doSomethingWithMyFile(filePath: String?) {}
+
+@Suppress("UNUSED_PARAMETER") fun doSomethingWithFailure(errorCode: Int, errorMessage: String?) {}
diff --git a/core/core/src/main/java/androidx/core/os/Profiling.kt b/core/core/src/main/java/androidx/core/os/Profiling.kt
index 55a4c00..683e0fbb 100644
--- a/core/core/src/main/java/androidx/core/os/Profiling.kt
+++ b/core/core/src/main/java/androidx/core/os/Profiling.kt
@@ -19,7 +19,6 @@
package androidx.core.os
import android.content.Context
-import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal
import android.os.ProfilingManager
@@ -32,9 +31,9 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
-/** Helpers providing simple wrapper APIs for {@link ProfilingManager}. */
+/** Helpers providing simple wrapper APIs for [ProfilingManager]. */
-// Begin section: Keep in sync with {@link ProfilingManager}
+// Begin section: Keep in sync with ProfilingManager
private const val KEY_DURATION_MS: String = "KEY_DURATION_MS"
private const val KEY_SAMPLING_INTERVAL_BYTES: String = "KEY_SAMPLING_INTERVAL_BYTES"
private const val KEY_TRACK_JAVA_ALLOCATIONS: String = "KEY_TRACK_JAVA_ALLOCATIONS"
@@ -47,7 +46,10 @@
// End section: Keep in sync with ProfilingManager
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+// TODO: b/361641325 - Change all RequiresApi values from 35 to
+// Build.VERSION_CODES.VANILLA_ICE_CREAM after docs rendering bug is fixed.
+
+@RequiresApi(api = 35)
public sealed class BufferFillPolicy(internal val value: Int) {
public companion object {
@JvmField @SuppressWarnings("AcronymName") public val DISCARD: BufferFillPolicy = Discard()
@@ -62,8 +64,12 @@
private class RingBuffer : BufferFillPolicy(VALUE_BUFFER_FILL_POLICY_RING_BUFFER)
}
-/** Obtain a flow to be called with all profiling results for this UID. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+/**
+ * Obtain a flow to be called with all profiling results for this UID.
+ *
+ * @sample androidx.core.os.registerForAllProfilingResultsFlowSample
+ */
+@RequiresApi(api = 35)
public fun registerForAllProfilingResults(context: Context): Flow<ProfilingResult> = callbackFlow {
val listener = Consumer<ProfilingResult> { result -> trySend(result) }
@@ -73,8 +79,12 @@
awaitClose { service.unregisterForAllProfilingResults(listener) }
}
-/** Register a listener to be called with all profiling results for this UID. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+/**
+ * Register a listener to be called with all profiling results for this UID.
+ *
+ * @sample androidx.core.os.registerForAllProfilingResultsSample
+ */
+@RequiresApi(api = 35)
public fun registerForAllProfilingResults(
context: Context,
executor: Executor,
@@ -85,19 +95,32 @@
}
/** Unregister a listener that was to be called for all profiling results. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@RequiresApi(api = 35)
public fun unregisterForAllProfilingResults(context: Context, listener: Consumer<ProfilingResult>) {
val service = context.getSystemService(ProfilingManager::class.java)
service.unregisterForAllProfilingResults(listener)
}
/**
- * Request profiling using a {@link ProfilingRequest} generated by one of the provided builders.
+ * Request profiling using a [ProfilingRequest] generated by one of the provided builders.
*
* If the executor and/or listener are null, and if no global listener and executor combinations are
- * registered using {@link registerForAllProfilingResults}, the request will be dropped.
+ * registered using [registerForAllProfilingResults], the request will be dropped.
+ *
+ * Requests will be rate limited and are not guaranteed to be filled.
+ *
+ * There might be a delay before profiling begins. For continuous profiling types (system tracing,
+ * stack sampling, and heap profiling), we recommend starting the collection early and stopping it
+ * with cancellationSignal, set on the [profilingRequest] builder, immediately after the area of
+ * interest to ensure that the section you want profiled is captured. For heap dumps, we recommend
+ * testing locally to ensure that the heap dump is collected at the proper time.
+ *
+ * @sample androidx.core.os.requestJavaHeapDump
+ * @sample androidx.core.os.requestHeapProfile
+ * @sample androidx.core.os.requestStackSampling
+ * @sample androidx.core.os.requestSystemTrace
*/
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@RequiresApi(api = 35)
public fun requestProfiling(
context: Context,
profilingRequest: ProfilingRequest,
@@ -116,7 +139,7 @@
}
/** Base class for request builders. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@RequiresApi(api = 35)
@SuppressWarnings("StaticFinalBuilder", "TopLevelBuilder")
public abstract class ProfilingRequestBuilder<T : ProfilingRequestBuilder<T>>
internal constructor() {
@@ -142,8 +165,8 @@
}
/**
- * Build the {@link ProfilingRequest} object which can be used with {@link requestProfiling} to
- * request profiling.
+ * Build the [ProfilingRequest] object which can be used with [requestProfiling] to request
+ * profiling.
*/
public fun build(): ProfilingRequest {
return ProfilingRequest(getProfilingType(), getParams(), mTag, mCancellationSignal)
@@ -162,8 +185,12 @@
protected abstract fun getParams(): Bundle
}
-/** Request builder to create a request for a java heap dump from {@link ProfilingManager}. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+/**
+ * Request builder to create a request for a java heap dump from [ProfilingManager].
+ *
+ * @sample androidx.core.os.requestJavaHeapDump
+ */
+@RequiresApi(api = 35)
public class JavaHeapDumpRequestBuilder : ProfilingRequestBuilder<JavaHeapDumpRequestBuilder>() {
private val mParams: Bundle = Bundle()
@@ -189,8 +216,12 @@
}
}
-/** Request builder to create a request for a heap profile from {@link ProfilingManager}. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+/**
+ * Request builder to create a request for a heap profile from [ProfilingManager].
+ *
+ * @sample androidx.core.os.requestHeapProfile
+ */
+@RequiresApi(api = 35)
public class HeapProfileRequestBuilder : ProfilingRequestBuilder<HeapProfileRequestBuilder>() {
private val mParams: Bundle = Bundle()
@@ -234,8 +265,12 @@
}
}
-/** Request builder to create a request for stack sampling from {@link ProfilingManager}. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+/**
+ * Request builder to create a request for stack sampling from [ProfilingManager].
+ *
+ * @sample androidx.core.os.requestStackSampling
+ */
+@RequiresApi(api = 35)
public class StackSamplingRequestBuilder : ProfilingRequestBuilder<StackSamplingRequestBuilder>() {
private val mParams: Bundle = Bundle()
@@ -273,8 +308,12 @@
}
}
-/** Request builder to create a request for a system trace from {@link ProfilingManager}. */
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+/**
+ * Request builder to create a request for a system trace from [ProfilingManager].
+ *
+ * @sample androidx.core.os.requestSystemTrace
+ */
+@RequiresApi(api = 35)
public class SystemTraceRequestBuilder : ProfilingRequestBuilder<SystemTraceRequestBuilder>() {
private val mParams: Bundle = Bundle()
@@ -317,9 +356,9 @@
*
* This should be constructed using one of the provided builders.
*
- * This should be used with {@link requestProfiling} to submit a profiling request.
+ * This should be used with [requestProfiling] to submit a profiling request.
*/
-@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@RequiresApi(api = 35)
public class ProfilingRequest
internal constructor(
public val profilingType: Int,
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index abd64a6..30d02c4 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -53,6 +53,11 @@
exclude group: "androidx.core"
}
+ implementation(libs.playServicesIdentityCredentials){
+ exclude group: "androidx.loader"
+ exclude group: "androidx.core"
+ }
+
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/getdigitalcredential/CredentialProviderGetDigitalCredentialControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/getdigitalcredential/CredentialProviderGetDigitalCredentialControllerTest.kt
new file mode 100644
index 0000000..55c7694
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/getdigitalcredential/CredentialProviderGetDigitalCredentialControllerTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.getdigitalcredential
+
+import android.content.ComponentName
+import androidx.credentials.ExperimentalDigitalCredentialApi
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetDigitalCredentialOption
+import androidx.credentials.playservices.TestCredentialsActivity
+import androidx.credentials.playservices.TestUtils
+import androidx.credentials.playservices.controllers.GetRestoreCredential.CredentialProviderGetDigitalCredentialController
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@OptIn(ExperimentalDigitalCredentialApi::class)
+class CredentialProviderGetDigitalCredentialControllerTest {
+ @Test
+ fun convertRequestToPlayServices_success() {
+ val request =
+ GetCredentialRequest(
+ credentialOptions =
+ listOf(
+ GetDigitalCredentialOption("{\"request\":{\"json\":{\"test\":\"val\"}}}"),
+ GetDigitalCredentialOption("{\"request\":\"val\",\"key2\":\"val2\"}"),
+ ),
+ origin = "origin",
+ preferIdentityDocUi = true,
+ preferUiBrandingComponentName = ComponentName("pkg", "cls"),
+ preferImmediatelyAvailableCredentials = true,
+ )
+ val activityScenario = ActivityScenario.launch(TestCredentialsActivity::class.java)
+
+ activityScenario.onActivity { activity: TestCredentialsActivity? ->
+ val convertedRequest =
+ CredentialProviderGetDigitalCredentialController(activity!!)
+ .convertRequestToPlayServices(request)
+
+ assertThat(convertedRequest.origin).isEqualTo(request.origin)
+ TestUtils.equals(
+ convertedRequest.data,
+ GetCredentialRequest.getRequestMetadataBundle(request)
+ )
+ request.credentialOptions.forEachIndexed { idx, expectedOption ->
+ val actualOption = convertedRequest.credentialOptions[idx]
+ assertThat(actualOption.type).isEqualTo(expectedOption.type)
+ if (expectedOption is GetDigitalCredentialOption) {
+ assertThat(actualOption.requestMatcher).isEqualTo(expectedOption.requestJson)
+ }
+ TestUtils.equals(actualOption.credentialRetrievalData, expectedOption.requestData)
+ TestUtils.equals(actualOption.candidateQueryData, expectedOption.candidateQueryData)
+ }
+ }
+ }
+}
diff --git a/credentials/credentials-play-services-auth/src/main/AndroidManifest.xml b/credentials/credentials-play-services-auth/src/main/AndroidManifest.xml
index 24737ae..5a9ac0b 100644
--- a/credentials/credentials-play-services-auth/src/main/AndroidManifest.xml
+++ b/credentials/credentials-play-services-auth/src/main/AndroidManifest.xml
@@ -33,5 +33,13 @@
android:fitsSystemWindows="true"
android:theme="@style/Theme.Hidden">
</activity>
+ <activity
+ android:name="androidx.credentials.playservices.IdentityCredentialApiHiddenActivity"
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+ android:exported="false"
+ android:enabled="true"
+ android:fitsSystemWindows="true"
+ android:theme="@style/Theme.Hidden">
+ </activity>
</application>
</manifest>
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index 8bed458..e702dda 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -30,8 +30,10 @@
import androidx.credentials.CreateRestoreCredentialRequest
import androidx.credentials.CredentialManagerCallback
import androidx.credentials.CredentialProvider
+import androidx.credentials.ExperimentalDigitalCredentialApi
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetDigitalCredentialOption
import androidx.credentials.GetRestoreCredentialOption
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.ClearCredentialProviderConfigurationException
@@ -44,6 +46,7 @@
import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.CredentialProviderCreatePublicKeyCredentialController
import androidx.credentials.playservices.controllers.CreateRestoreCredential.CredentialProviderCreateRestoreCredentialController
+import androidx.credentials.playservices.controllers.GetRestoreCredential.CredentialProviderGetDigitalCredentialController
import androidx.credentials.playservices.controllers.GetRestoreCredential.CredentialProviderGetRestoreCredentialController
import androidx.credentials.playservices.controllers.GetSignInIntent.CredentialProviderGetSignInIntentController
import com.google.android.gms.auth.api.identity.Identity
@@ -58,6 +61,7 @@
/** Entry point of all credential manager requests to the play-services-auth module. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Suppress("deprecation")
+@OptIn(ExperimentalDigitalCredentialApi::class)
class CredentialProviderPlayServicesImpl(private val context: Context) : CredentialProvider {
@VisibleForTesting var googleApiAvailability = GoogleApiAvailability.getInstance()
@@ -72,7 +76,23 @@
if (cancellationReviewer(cancellationSignal)) {
return
}
- if (isGetRestoreCredentialRequest(request)) {
+ if (isDigitalCredentialRequest(request)) {
+ if (!isAvailableOnDevice(MIN_GMS_APK_VERSION_DIGITAL_CRED)) {
+ cancellationReviewerWithCallback(cancellationSignal) {
+ executor.execute {
+ callback.onError(
+ GetCredentialProviderConfigurationException(
+ "this device requires a Google Play Services update for the" +
+ " given feature to be supported"
+ )
+ )
+ }
+ }
+ return
+ }
+ CredentialProviderGetDigitalCredentialController(context)
+ .invokePlayServices(request, callback, executor, cancellationSignal)
+ } else if (isGetRestoreCredentialRequest(request)) {
if (!isAvailableOnDevice(MIN_GMS_APK_VERSION_RESTORE_CRED)) {
cancellationReviewerWithCallback(cancellationSignal) {
executor.execute {
@@ -263,6 +283,8 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) const val MIN_GMS_APK_VERSION = 230815045
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
const val MIN_GMS_APK_VERSION_RESTORE_CRED = 242200000
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val MIN_GMS_APK_VERSION_DIGITAL_CRED = 243100000
internal fun cancellationReviewerWithCallback(
cancellationSignal: CancellationSignal?,
@@ -302,5 +324,14 @@
}
return false
}
+
+ internal fun isDigitalCredentialRequest(request: GetCredentialRequest): Boolean {
+ for (option in request.credentialOptions) {
+ if (option is GetDigitalCredentialOption) {
+ return true
+ }
+ }
+ return false
+ }
}
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
index 03d22ff..93216a1 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
@@ -32,6 +32,8 @@
import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.GET_INTERRUPTED
import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.GET_NO_CREDENTIALS
import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.GET_UNKNOWN
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.reportError
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.reportResult
import com.google.android.gms.auth.api.identity.BeginSignInRequest
import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
import com.google.android.gms.auth.api.identity.Identity
@@ -150,11 +152,7 @@
}
private fun setupFailure(resultReceiver: ResultReceiver, errName: String, errMsg: String) {
- val bundle = Bundle()
- bundle.putBoolean(CredentialProviderBaseController.FAILURE_RESPONSE_TAG, true)
- bundle.putString(CredentialProviderBaseController.EXCEPTION_TYPE_TAG, errName)
- bundle.putString(CredentialProviderBaseController.EXCEPTION_MESSAGE_TAG, errMsg)
- resultReceiver.send(Integer.MAX_VALUE, bundle)
+ resultReceiver.reportError(errName, errMsg)
finish()
}
@@ -336,11 +334,11 @@
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- val bundle = Bundle()
- bundle.putBoolean(CredentialProviderBaseController.FAILURE_RESPONSE_TAG, false)
- bundle.putInt(CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG, requestCode)
- bundle.putParcelable(CredentialProviderBaseController.RESULT_DATA_TAG, data)
- resultReceiver?.send(resultCode, bundle)
+ resultReceiver?.reportResult(
+ requestCode = requestCode,
+ data = data,
+ resultCode = resultCode
+ )
mWaitingForActivityResult = false
finish()
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/IdentityCredentialApiHiddenActivity.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/IdentityCredentialApiHiddenActivity.kt
new file mode 100644
index 0000000..7270a9a
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/IdentityCredentialApiHiddenActivity.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.
+ */
+
+@file:Suppress("Deprecation")
+
+package androidx.credentials.playservices
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import android.os.ResultReceiver
+import androidx.annotation.RestrictTo
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.GET_UNKNOWN
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.reportError
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController.Companion.reportResult
+
+/** An activity used to ensure all required API versions work as intended. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Suppress("ForbiddenSuperClass")
+open class IdentityCredentialApiHiddenActivity : Activity() {
+
+ private var resultReceiver: ResultReceiver? = null
+ private var mWaitingForActivityResult = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ overridePendingTransition(0, 0)
+ resultReceiver =
+ intent.getParcelableExtra(CredentialProviderBaseController.RESULT_RECEIVER_TAG)
+ if (resultReceiver == null) {
+ finish()
+ }
+
+ restoreState(savedInstanceState)
+ if (mWaitingForActivityResult) {
+ return
+ // Past call still active
+ }
+ val pendingIntent: PendingIntent? =
+ intent.getParcelableExtra(CredentialProviderBaseController.EXTRA_GET_CREDENTIAL_INTENT)
+
+ if (pendingIntent != null) {
+ startIntentSenderForResult(
+ pendingIntent.intentSender,
+ /* requestCode= */ CredentialProviderBaseController.CONTROLLER_REQUEST_CODE,
+ /* fillInIntent= */ null,
+ /* flagsMask= */ 0,
+ /* flagsValues= */ 0,
+ /* extraFlags= */ 0,
+ /* options = */ null
+ )
+ } else {
+ resultReceiver?.reportError(errName = GET_UNKNOWN, errMsg = "Internal error")
+ finish()
+ }
+ }
+
+ private fun restoreState(savedInstanceState: Bundle?) {
+ if (savedInstanceState != null) {
+ mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ resultReceiver?.reportResult(
+ requestCode = requestCode,
+ resultCode = resultCode,
+ data = data
+ )
+ mWaitingForActivityResult = false
+ finish()
+ }
+
+ companion object {
+ private const val KEY_AWAITING_RESULT = "androidx.credentials.playservices.AWAITING_RESULT"
+ }
+}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
index 584fc1be..b5719fb 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.content.Intent
+import android.os.Bundle
import android.os.Parcel
import android.os.ResultReceiver
import androidx.credentials.exceptions.CreateCredentialCancellationException
@@ -44,7 +45,7 @@
)
// Generic controller request code used by all controllers
- @JvmStatic protected val CONTROLLER_REQUEST_CODE: Int = 1
+ @JvmStatic internal val CONTROLLER_REQUEST_CODE: Int = 1
/** -- Used to avoid reflection, these constants map errors from HiddenActivity -- */
const val GET_CANCELED = "GET_CANCELED_TAG"
@@ -79,6 +80,9 @@
// Key for the result intent to send back to the controller
const val RESULT_DATA_TAG = "RESULT_DATA"
+ // Key for the actual parcelable type sent to the hidden activity
+ const val EXTRA_GET_CREDENTIAL_INTENT = "EXTRA_GET_CREDENTIAL_INTENT"
+
// Key for the failure boolean sent back from hidden activity to controller
const val FAILURE_RESPONSE_TAG = "FAILURE_RESPONSE"
@@ -115,6 +119,22 @@
}
}
+ internal fun ResultReceiver.reportError(errName: String, errMsg: String) {
+ val bundle = Bundle()
+ bundle.putBoolean(FAILURE_RESPONSE_TAG, true)
+ bundle.putString(EXCEPTION_TYPE_TAG, errName)
+ bundle.putString(EXCEPTION_MESSAGE_TAG, errMsg)
+ this.send(Integer.MAX_VALUE, bundle)
+ }
+
+ internal fun ResultReceiver.reportResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ val bundle = Bundle()
+ bundle.putBoolean(FAILURE_RESPONSE_TAG, false)
+ bundle.putInt(ACTIVITY_REQUEST_CODE_TAG, requestCode)
+ bundle.putParcelable(RESULT_DATA_TAG, data)
+ this.send(resultCode, bundle)
+ }
+
internal fun createCredentialExceptionTypeToException(
typeName: String?,
msg: String?
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/GetDigitalCredential/CredentialProviderGetDigitalCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/GetDigitalCredential/CredentialProviderGetDigitalCredentialController.kt
new file mode 100644
index 0000000..b606ece
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/GetDigitalCredential/CredentialProviderGetDigitalCredentialController.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.GetRestoreCredential
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.os.Handler
+import android.os.Looper
+import android.os.ResultReceiver
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.Credential
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.DigitalCredential
+import androidx.credentials.ExperimentalDigitalCredentialApi
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetDigitalCredentialOption
+import androidx.credentials.exceptions.GetCredentialCancellationException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.internal.toJetpackGetException
+import androidx.credentials.playservices.CredentialProviderPlayServicesImpl
+import androidx.credentials.playservices.IdentityCredentialApiHiddenActivity
+import androidx.credentials.playservices.controllers.CredentialProviderBaseController
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes
+import com.google.android.gms.identitycredentials.IdentityCredentialManager
+import com.google.android.gms.identitycredentials.IntentHelper
+import java.util.concurrent.Executor
+
+/** A controller to handle the GetRestoreCredential flow with play services. */
+@OptIn(ExperimentalDigitalCredentialApi::class)
+internal class CredentialProviderGetDigitalCredentialController(private val context: Context) :
+ CredentialProviderController<
+ GetCredentialRequest,
+ com.google.android.gms.identitycredentials.GetCredentialRequest,
+ com.google.android.gms.identitycredentials.GetCredentialResponse,
+ GetCredentialResponse,
+ GetCredentialException
+ >(context) {
+
+ /** The callback object state, used in the protected handleResponse method. */
+ @VisibleForTesting
+ lateinit var callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
+
+ /** The callback requires an executor to invoke it. */
+ @VisibleForTesting lateinit var executor: Executor
+
+ /**
+ * The cancellation signal, which is shuttled around to stop the flow at any moment prior to
+ * returning data.
+ */
+ @VisibleForTesting private var cancellationSignal: CancellationSignal? = null
+
+ @Suppress("deprecation")
+ private val resultReceiver =
+ object : ResultReceiver(Handler(Looper.getMainLooper())) {
+ public override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
+ if (
+ maybeReportErrorFromResultReceiver(
+ resultData,
+ CredentialProviderBaseController.Companion::
+ getCredentialExceptionTypeToException,
+ executor,
+ callback,
+ cancellationSignal
+ )
+ ) {
+ return
+ } else {
+ handleResponse(
+ resultData.getInt(ACTIVITY_REQUEST_CODE_TAG),
+ resultCode,
+ resultData.getParcelable(RESULT_DATA_TAG)
+ )
+ }
+ }
+ }
+
+ internal fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+ if (uniqueRequestCode != CONTROLLER_REQUEST_CODE) {
+ Log.w(
+ TAG,
+ "Returned request code $CONTROLLER_REQUEST_CODE which " +
+ " does not match what was given $uniqueRequestCode"
+ )
+ return
+ }
+
+ if (
+ maybeReportErrorResultCodeGet(
+ resultCode,
+ { s, f -> cancelOrCallbackExceptionOrResult(s, f) },
+ { e -> this.executor.execute { this.callback.onError(e) } },
+ cancellationSignal
+ )
+ ) {
+ return
+ }
+
+ try {
+ val response = IntentHelper.extractGetCredentialResponse(resultCode, data?.extras!!)
+ cancelOrCallbackExceptionOrResult(cancellationSignal) {
+ this.executor.execute {
+ this.callback.onResult(convertResponseToCredentialManager(response))
+ }
+ }
+ } catch (e: Exception) {
+ val getException = fromGmsException(e)
+ cancelOrCallbackExceptionOrResult(cancellationSignal) {
+ executor.execute { callback.onError(getException) }
+ }
+ }
+ }
+
+ override fun invokePlayServices(
+ request: GetCredentialRequest,
+ callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
+ executor: Executor,
+ cancellationSignal: CancellationSignal?
+ ) {
+ this.cancellationSignal = cancellationSignal
+ this.callback = callback
+ this.executor = executor
+
+ if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
+ return
+ }
+
+ val convertedRequest = this.convertRequestToPlayServices(request)
+ IdentityCredentialManager.getClient(context)
+ .getCredential(convertedRequest)
+ .addOnSuccessListener { result ->
+ if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
+ return@addOnSuccessListener
+ }
+ val hiddenIntent = Intent(context, IdentityCredentialApiHiddenActivity::class.java)
+ hiddenIntent.flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
+ hiddenIntent.putExtra(
+ RESULT_RECEIVER_TAG,
+ toIpcFriendlyResultReceiver(resultReceiver)
+ )
+ hiddenIntent.putExtra(EXTRA_GET_CREDENTIAL_INTENT, result.pendingIntent)
+ context.startActivity(hiddenIntent)
+ }
+ .addOnFailureListener { e ->
+ val getException = fromGmsException(e)
+ cancelOrCallbackExceptionOrResult(cancellationSignal) {
+ executor.execute { callback.onError(getException) }
+ }
+ }
+ }
+
+ private fun fromGmsException(e: Throwable): GetCredentialException {
+ return when (e) {
+ is com.google.android.gms.identitycredentials.GetCredentialException ->
+ toJetpackGetException(e.type, e.message)
+ is ApiException ->
+ when (e.statusCode) {
+ CommonStatusCodes.CANCELED -> {
+ GetCredentialCancellationException(e.message)
+ }
+ in retryables -> {
+ GetCredentialInterruptedException(e.message)
+ }
+ else -> {
+ GetCredentialUnknownException("Get digital credential failed, failure: $e")
+ }
+ }
+ else -> GetCredentialUnknownException("Get digital credential failed, failure: $e")
+ }
+ }
+
+ public override fun convertRequestToPlayServices(
+ request: GetCredentialRequest
+ ): com.google.android.gms.identitycredentials.GetCredentialRequest {
+ val credOptions =
+ mutableListOf<com.google.android.gms.identitycredentials.CredentialOption>()
+ for (option in request.credentialOptions) {
+ if (option is GetDigitalCredentialOption) {
+ credOptions.add(
+ com.google.android.gms.identitycredentials.CredentialOption(
+ option.type,
+ option.requestData,
+ option.candidateQueryData,
+ option.requestJson,
+ requestType = "",
+ protocolType = "",
+ )
+ )
+ }
+ }
+ return com.google.android.gms.identitycredentials.GetCredentialRequest(
+ credOptions,
+ GetCredentialRequest.getRequestMetadataBundle(request),
+ request.origin,
+ ResultReceiver(null) // No-op
+ )
+ }
+
+ public override fun convertResponseToCredentialManager(
+ response: com.google.android.gms.identitycredentials.GetCredentialResponse
+ ): GetCredentialResponse {
+ return GetCredentialResponse(
+ Credential.createFrom(
+ DigitalCredential.TYPE_DIGITAL_CREDENTIAL, // TODO: b/361100869 - use the real type
+ // returned as the response
+ response.credential.data,
+ )
+ )
+ }
+
+ private companion object {
+ private const val TAG = "DigitalCredentialClient"
+ }
+}
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 0692467..db192ff 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -11,8 +11,9 @@
}
public abstract class CreateCredentialRequest {
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public static final androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getCredentialData();
method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
@@ -33,15 +34,16 @@
}
public static final class CreateCredentialRequest.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -50,7 +52,7 @@
}
public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
}
public abstract class CreateCredentialResponse {
@@ -120,8 +122,7 @@
}
public final class CreateRestoreCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateRestoreCredentialResponse(String responseJson, android.os.Bundle data);
- method public static androidx.credentials.CreateRestoreCredentialResponse createFrom(android.os.Bundle data);
+ ctor public CreateRestoreCredentialResponse(String responseJson);
method public String getResponseJson();
property public final String responseJson;
field public static final String BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE = "androidx.credentials.BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE";
@@ -129,10 +130,11 @@
}
public static final class CreateRestoreCredentialResponse.Companion {
- method public androidx.credentials.CreateRestoreCredentialResponse createFrom(android.os.Bundle data);
}
public abstract class Credential {
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public static final androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public static final androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
method public final android.os.Bundle getData();
method public final String getType();
property public final android.os.Bundle data;
@@ -141,6 +143,8 @@
}
public static final class Credential.Companion {
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
}
public interface CredentialManager {
@@ -174,6 +178,8 @@
}
public abstract class CredentialOption {
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public static final androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public static final androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getRequestData();
@@ -196,6 +202,8 @@
}
public static final class CredentialOption.Companion {
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
}
public interface CredentialProvider {
@@ -211,22 +219,40 @@
ctor public CustomCredential(String type, android.os.Bundle data);
}
+ @SuppressCompatibility @androidx.credentials.ExperimentalDigitalCredentialApi public final class DigitalCredential extends androidx.credentials.Credential {
+ ctor public DigitalCredential(String credentialJson);
+ method public String getCredentialJson();
+ property public final String credentialJson;
+ field public static final androidx.credentials.DigitalCredential.Companion Companion;
+ field public static final String TYPE_DIGITAL_CREDENTIAL = "androidx.credentials.TYPE_DIGITAL_CREDENTIAL";
+ }
+
+ public static final class DigitalCredential.Companion {
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This CredentialManager API is experimental and is likely to change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalDigitalCredentialApi {
+ }
+
public final class GetCredentialRequest {
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public static androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public static androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
method public boolean getPreferIdentityDocUi();
method public android.content.ComponentName? getPreferUiBrandingComponentName();
+ method @Discouraged(message="It should only be used by OEM services and library groups") public static android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
method public boolean preferImmediatelyAvailableCredentials();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
property public final String? origin;
property public final boolean preferIdentityDocUi;
property public final boolean preferImmediatelyAvailableCredentials;
property public final android.content.ComponentName? preferUiBrandingComponentName;
+ field public static final androidx.credentials.GetCredentialRequest.Companion Companion;
}
public static final class GetCredentialRequest.Builder {
@@ -240,6 +266,12 @@
method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
}
+ public static final class GetCredentialRequest.Companion {
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
+ method @Discouraged(message="It should only be used by OEM services and library groups") public android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
+ }
+
public final class GetCredentialResponse {
ctor public GetCredentialResponse(androidx.credentials.Credential credential);
method public androidx.credentials.Credential getCredential();
@@ -253,6 +285,12 @@
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
}
+ @SuppressCompatibility @androidx.credentials.ExperimentalDigitalCredentialApi public final class GetDigitalCredentialOption extends androidx.credentials.CredentialOption {
+ ctor public GetDigitalCredentialOption(String requestJson);
+ method public String getRequestJson();
+ property public final String requestJson;
+ }
+
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
ctor public GetPasswordOption();
ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
@@ -360,6 +398,14 @@
}
public abstract class ClearCredentialException extends java.lang.Exception {
+ method public static final android.os.Bundle asBundle(androidx.credentials.exceptions.ClearCredentialException ex);
+ method public static final androidx.credentials.exceptions.ClearCredentialException fromBundle(android.os.Bundle bundle);
+ field public static final androidx.credentials.exceptions.ClearCredentialException.Companion Companion;
+ }
+
+ public static final class ClearCredentialException.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.exceptions.ClearCredentialException ex);
+ method public androidx.credentials.exceptions.ClearCredentialException fromBundle(android.os.Bundle bundle);
}
public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
@@ -395,6 +441,14 @@
}
public abstract class CreateCredentialException extends java.lang.Exception {
+ method public static final android.os.Bundle asBundle(androidx.credentials.exceptions.CreateCredentialException ex);
+ method public static final androidx.credentials.exceptions.CreateCredentialException fromBundle(android.os.Bundle bundle);
+ field public static final androidx.credentials.exceptions.CreateCredentialException.Companion Companion;
+ }
+
+ public static final class CreateCredentialException.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.exceptions.CreateCredentialException ex);
+ method public androidx.credentials.exceptions.CreateCredentialException fromBundle(android.os.Bundle bundle);
}
public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
@@ -435,6 +489,14 @@
}
public abstract class GetCredentialException extends java.lang.Exception {
+ method public static final android.os.Bundle asBundle(androidx.credentials.exceptions.GetCredentialException ex);
+ method public static final androidx.credentials.exceptions.GetCredentialException fromBundle(android.os.Bundle bundle);
+ field public static final androidx.credentials.exceptions.GetCredentialException.Companion Companion;
+ }
+
+ public static final class GetCredentialException.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.exceptions.GetCredentialException ex);
+ method public androidx.credentials.exceptions.GetCredentialException fromBundle(android.os.Bundle bundle);
}
public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
@@ -826,7 +888,6 @@
@RequiresApi(35) public final class BiometricPromptData {
ctor public BiometricPromptData();
- ctor public BiometricPromptData();
ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject);
ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject, optional int allowedAuthenticators);
method public int getAllowedAuthenticators();
@@ -1059,30 +1120,54 @@
public final class ProviderClearCredentialStateRequest {
ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
+ method public static android.os.Bundle asBundle(androidx.credentials.provider.ProviderClearCredentialStateRequest request);
+ method public static androidx.credentials.provider.ProviderClearCredentialStateRequest fromBundle(android.os.Bundle bundle);
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
+ field public static final androidx.credentials.provider.ProviderClearCredentialStateRequest.Companion Companion;
+ }
+
+ public static final class ProviderClearCredentialStateRequest.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.provider.ProviderClearCredentialStateRequest request);
+ method public androidx.credentials.provider.ProviderClearCredentialStateRequest fromBundle(android.os.Bundle bundle);
}
public final class ProviderCreateCredentialRequest {
ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method @RequiresApi(23) public static android.os.Bundle asBundle(androidx.credentials.provider.ProviderCreateCredentialRequest request);
+ method @RequiresApi(23) public static androidx.credentials.provider.ProviderCreateCredentialRequest fromBundle(android.os.Bundle bundle);
method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public androidx.credentials.CreateCredentialRequest getCallingRequest();
property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final androidx.credentials.CreateCredentialRequest callingRequest;
+ field public static final androidx.credentials.provider.ProviderCreateCredentialRequest.Companion Companion;
+ }
+
+ public static final class ProviderCreateCredentialRequest.Companion {
+ method @RequiresApi(23) public android.os.Bundle asBundle(androidx.credentials.provider.ProviderCreateCredentialRequest request);
+ method @RequiresApi(23) public androidx.credentials.provider.ProviderCreateCredentialRequest fromBundle(android.os.Bundle bundle);
}
public final class ProviderGetCredentialRequest {
ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method public static android.os.Bundle asBundle(androidx.credentials.provider.ProviderGetCredentialRequest request);
+ method public static androidx.credentials.provider.ProviderGetCredentialRequest fromBundle(android.os.Bundle bundle);
method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ field public static final androidx.credentials.provider.ProviderGetCredentialRequest.Companion Companion;
+ }
+
+ public static final class ProviderGetCredentialRequest.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.provider.ProviderGetCredentialRequest request);
+ method public androidx.credentials.provider.ProviderGetCredentialRequest fromBundle(android.os.Bundle bundle);
}
@RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 0692467..db192ff 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -11,8 +11,9 @@
}
public abstract class CreateCredentialRequest {
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public static final androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getCredentialData();
method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
@@ -33,15 +34,16 @@
}
public static final class CreateCredentialRequest.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -50,7 +52,7 @@
}
public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
}
public abstract class CreateCredentialResponse {
@@ -120,8 +122,7 @@
}
public final class CreateRestoreCredentialResponse extends androidx.credentials.CreateCredentialResponse {
- ctor public CreateRestoreCredentialResponse(String responseJson, android.os.Bundle data);
- method public static androidx.credentials.CreateRestoreCredentialResponse createFrom(android.os.Bundle data);
+ ctor public CreateRestoreCredentialResponse(String responseJson);
method public String getResponseJson();
property public final String responseJson;
field public static final String BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE = "androidx.credentials.BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE";
@@ -129,10 +130,11 @@
}
public static final class CreateRestoreCredentialResponse.Companion {
- method public androidx.credentials.CreateRestoreCredentialResponse createFrom(android.os.Bundle data);
}
public abstract class Credential {
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public static final androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public static final androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
method public final android.os.Bundle getData();
method public final String getType();
property public final android.os.Bundle data;
@@ -141,6 +143,8 @@
}
public static final class Credential.Companion {
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
}
public interface CredentialManager {
@@ -174,6 +178,8 @@
}
public abstract class CredentialOption {
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public static final androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public static final androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getRequestData();
@@ -196,6 +202,8 @@
}
public static final class CredentialOption.Companion {
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
}
public interface CredentialProvider {
@@ -211,22 +219,40 @@
ctor public CustomCredential(String type, android.os.Bundle data);
}
+ @SuppressCompatibility @androidx.credentials.ExperimentalDigitalCredentialApi public final class DigitalCredential extends androidx.credentials.Credential {
+ ctor public DigitalCredential(String credentialJson);
+ method public String getCredentialJson();
+ property public final String credentialJson;
+ field public static final androidx.credentials.DigitalCredential.Companion Companion;
+ field public static final String TYPE_DIGITAL_CREDENTIAL = "androidx.credentials.TYPE_DIGITAL_CREDENTIAL";
+ }
+
+ public static final class DigitalCredential.Companion {
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This CredentialManager API is experimental and is likely to change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalDigitalCredentialApi {
+ }
+
public final class GetCredentialRequest {
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public static androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public static androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
method public boolean getPreferIdentityDocUi();
method public android.content.ComponentName? getPreferUiBrandingComponentName();
+ method @Discouraged(message="It should only be used by OEM services and library groups") public static android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
method public boolean preferImmediatelyAvailableCredentials();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
property public final String? origin;
property public final boolean preferIdentityDocUi;
property public final boolean preferImmediatelyAvailableCredentials;
property public final android.content.ComponentName? preferUiBrandingComponentName;
+ field public static final androidx.credentials.GetCredentialRequest.Companion Companion;
}
public static final class GetCredentialRequest.Builder {
@@ -240,6 +266,12 @@
method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
}
+ public static final class GetCredentialRequest.Companion {
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
+ method @Discouraged(message="It should only be used by OEM services and library groups") public android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
+ }
+
public final class GetCredentialResponse {
ctor public GetCredentialResponse(androidx.credentials.Credential credential);
method public androidx.credentials.Credential getCredential();
@@ -253,6 +285,12 @@
ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
}
+ @SuppressCompatibility @androidx.credentials.ExperimentalDigitalCredentialApi public final class GetDigitalCredentialOption extends androidx.credentials.CredentialOption {
+ ctor public GetDigitalCredentialOption(String requestJson);
+ method public String getRequestJson();
+ property public final String requestJson;
+ }
+
public final class GetPasswordOption extends androidx.credentials.CredentialOption {
ctor public GetPasswordOption();
ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
@@ -360,6 +398,14 @@
}
public abstract class ClearCredentialException extends java.lang.Exception {
+ method public static final android.os.Bundle asBundle(androidx.credentials.exceptions.ClearCredentialException ex);
+ method public static final androidx.credentials.exceptions.ClearCredentialException fromBundle(android.os.Bundle bundle);
+ field public static final androidx.credentials.exceptions.ClearCredentialException.Companion Companion;
+ }
+
+ public static final class ClearCredentialException.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.exceptions.ClearCredentialException ex);
+ method public androidx.credentials.exceptions.ClearCredentialException fromBundle(android.os.Bundle bundle);
}
public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
@@ -395,6 +441,14 @@
}
public abstract class CreateCredentialException extends java.lang.Exception {
+ method public static final android.os.Bundle asBundle(androidx.credentials.exceptions.CreateCredentialException ex);
+ method public static final androidx.credentials.exceptions.CreateCredentialException fromBundle(android.os.Bundle bundle);
+ field public static final androidx.credentials.exceptions.CreateCredentialException.Companion Companion;
+ }
+
+ public static final class CreateCredentialException.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.exceptions.CreateCredentialException ex);
+ method public androidx.credentials.exceptions.CreateCredentialException fromBundle(android.os.Bundle bundle);
}
public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
@@ -435,6 +489,14 @@
}
public abstract class GetCredentialException extends java.lang.Exception {
+ method public static final android.os.Bundle asBundle(androidx.credentials.exceptions.GetCredentialException ex);
+ method public static final androidx.credentials.exceptions.GetCredentialException fromBundle(android.os.Bundle bundle);
+ field public static final androidx.credentials.exceptions.GetCredentialException.Companion Companion;
+ }
+
+ public static final class GetCredentialException.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.exceptions.GetCredentialException ex);
+ method public androidx.credentials.exceptions.GetCredentialException fromBundle(android.os.Bundle bundle);
}
public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
@@ -826,7 +888,6 @@
@RequiresApi(35) public final class BiometricPromptData {
ctor public BiometricPromptData();
- ctor public BiometricPromptData();
ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject);
ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject, optional int allowedAuthenticators);
method public int getAllowedAuthenticators();
@@ -1059,30 +1120,54 @@
public final class ProviderClearCredentialStateRequest {
ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
+ method public static android.os.Bundle asBundle(androidx.credentials.provider.ProviderClearCredentialStateRequest request);
+ method public static androidx.credentials.provider.ProviderClearCredentialStateRequest fromBundle(android.os.Bundle bundle);
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
+ field public static final androidx.credentials.provider.ProviderClearCredentialStateRequest.Companion Companion;
+ }
+
+ public static final class ProviderClearCredentialStateRequest.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.provider.ProviderClearCredentialStateRequest request);
+ method public androidx.credentials.provider.ProviderClearCredentialStateRequest fromBundle(android.os.Bundle bundle);
}
public final class ProviderCreateCredentialRequest {
ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method @RequiresApi(23) public static android.os.Bundle asBundle(androidx.credentials.provider.ProviderCreateCredentialRequest request);
+ method @RequiresApi(23) public static androidx.credentials.provider.ProviderCreateCredentialRequest fromBundle(android.os.Bundle bundle);
method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public androidx.credentials.CreateCredentialRequest getCallingRequest();
property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final androidx.credentials.CreateCredentialRequest callingRequest;
+ field public static final androidx.credentials.provider.ProviderCreateCredentialRequest.Companion Companion;
+ }
+
+ public static final class ProviderCreateCredentialRequest.Companion {
+ method @RequiresApi(23) public android.os.Bundle asBundle(androidx.credentials.provider.ProviderCreateCredentialRequest request);
+ method @RequiresApi(23) public androidx.credentials.provider.ProviderCreateCredentialRequest fromBundle(android.os.Bundle bundle);
}
public final class ProviderGetCredentialRequest {
ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+ method public static android.os.Bundle asBundle(androidx.credentials.provider.ProviderGetCredentialRequest request);
+ method public static androidx.credentials.provider.ProviderGetCredentialRequest fromBundle(android.os.Bundle bundle);
method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ field public static final androidx.credentials.provider.ProviderGetCredentialRequest.Companion Companion;
+ }
+
+ public static final class ProviderGetCredentialRequest.Companion {
+ method public android.os.Bundle asBundle(androidx.credentials.provider.ProviderGetCredentialRequest request);
+ method public androidx.credentials.provider.ProviderGetCredentialRequest fromBundle(android.os.Bundle bundle);
}
@RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
index 5ecdf5e..3d75ca8 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCustomCredentialRequestTest.kt
@@ -90,7 +90,7 @@
assertThat(request.origin).isEqualTo(expectedOrigin)
}
- @SdkSuppress(minSdkVersion = 23)
+ @SdkSuppress(minSdkVersion = 28)
@Test
fun frameworkConversion_success() {
val expectedType = "TYPE"
@@ -145,4 +145,63 @@
assertThat(actualRequest.preferImmediatelyAvailableCredentials)
.isEqualTo(expectedPreferImmediatelyAvailableCredentials)
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val expectedType = "TYPE"
+ val expectedCredentialDataBundle = Bundle()
+ expectedCredentialDataBundle.putString("Test", "Test")
+ val expectedCandidateQueryDataBundle = Bundle()
+ expectedCandidateQueryDataBundle.putBoolean("key", true)
+ val expectedDisplayInfo = DisplayInfo("userId")
+ val expectedSystemProvider = true
+ val expectedAutoSelectAllowed = true
+ val expectedPreferImmediatelyAvailableCredentials = true
+ val expectedOrigin = "Origin"
+ val request =
+ CreateCustomCredentialRequest(
+ expectedType,
+ expectedCredentialDataBundle,
+ expectedCandidateQueryDataBundle,
+ expectedSystemProvider,
+ expectedDisplayInfo,
+ expectedAutoSelectAllowed,
+ expectedOrigin,
+ expectedPreferImmediatelyAvailableCredentials,
+ )
+ val finalCredentialData = request.credentialData
+ finalCredentialData.putBundle(
+ DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO,
+ expectedDisplayInfo.toBundle()
+ )
+
+ val convertedRequest =
+ createFrom(
+ android.credentials.CreateCredentialRequest.Builder(
+ request.type,
+ request.credentialData,
+ request.candidateQueryData
+ )
+ .setOrigin(expectedOrigin)
+ .setIsSystemProviderRequired(request.isSystemProviderRequired)
+ .build()
+ )
+
+ assertThat(convertedRequest).isInstanceOf(CreateCustomCredentialRequest::class.java)
+ val actualRequest = convertedRequest as CreateCustomCredentialRequest
+ assertThat(actualRequest.type).isEqualTo(expectedType)
+ assertThat(equals(actualRequest.credentialData, expectedCredentialDataBundle)).isTrue()
+ assertThat(equals(actualRequest.candidateQueryData, expectedCandidateQueryDataBundle))
+ .isTrue()
+ assertThat(actualRequest.isSystemProviderRequired).isEqualTo(expectedSystemProvider)
+ assertThat(actualRequest.isAutoSelectAllowed).isEqualTo(expectedAutoSelectAllowed)
+ assertThat(actualRequest.displayInfo.userId).isEqualTo(expectedDisplayInfo.userId)
+ assertThat(actualRequest.displayInfo.userDisplayName)
+ .isEqualTo(expectedDisplayInfo.userDisplayName)
+ assertThat(actualRequest.origin).isEqualTo(expectedOrigin)
+ assertThat(actualRequest.origin).isEqualTo(expectedOrigin)
+ assertThat(actualRequest.preferImmediatelyAvailableCredentials)
+ .isEqualTo(expectedPreferImmediatelyAvailableCredentials)
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
index 2654035..2491f72 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -189,7 +189,7 @@
.isEqualTo(R.drawable.ic_password)
}
- @SdkSuppress(minSdkVersion = 34)
+ @SdkSuppress(minSdkVersion = 28)
@Test
fun frameworkConversion_success() {
val idExpected = "id"
@@ -248,4 +248,68 @@
assertThat(convertedRequest.candidateQueryData.getBoolean(customCandidateQueryDataKey))
.isEqualTo(customCandidateQueryDataValue)
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val idExpected = "id"
+ val passwordExpected = "pwd"
+ val preferImmediatelyAvailableCredentialsExpected = true
+ val isAutoSelectAllowedExpected = true
+ val originExpected = "origin"
+ val defaultProviderExpected = "com.test/com.test.TestProviderComponent"
+ val request =
+ CreatePasswordRequest(
+ idExpected,
+ passwordExpected,
+ originExpected,
+ defaultProviderExpected,
+ preferImmediatelyAvailableCredentialsExpected,
+ isAutoSelectAllowedExpected
+ )
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val credentialData = getFinalCreateCredentialData(request, mContext)
+ val customRequestDataKey = "customRequestDataKey"
+ val customRequestDataValue = "customRequestDataValue"
+ credentialData.putString(customRequestDataKey, customRequestDataValue)
+ request.credentialData.putAll(credentialData)
+ val candidateQueryData = request.candidateQueryData
+ val customCandidateQueryDataKey = "customRequestDataKey"
+ val customCandidateQueryDataValue = true
+ candidateQueryData.putBoolean(customCandidateQueryDataKey, customCandidateQueryDataValue)
+
+ val convertedRequest =
+ createFrom(
+ android.credentials.CreateCredentialRequest.Builder(
+ request.type,
+ credentialData,
+ candidateQueryData
+ )
+ .setOrigin(originExpected)
+ .setIsSystemProviderRequired(request.isSystemProviderRequired)
+ .build()
+ )
+
+ assertThat(convertedRequest).isInstanceOf(CreatePasswordRequest::class.java)
+ val convertedCreatePasswordRequest = convertedRequest as CreatePasswordRequest
+ assertThat(convertedCreatePasswordRequest.password).isEqualTo(passwordExpected)
+ assertThat(convertedCreatePasswordRequest.id).isEqualTo(idExpected)
+ assertThat(convertedCreatePasswordRequest.preferImmediatelyAvailableCredentials)
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ assertThat(convertedCreatePasswordRequest.origin).isEqualTo(originExpected)
+ assertThat(convertedCreatePasswordRequest.isAutoSelectAllowed)
+ .isEqualTo(isAutoSelectAllowedExpected)
+ val displayInfo = convertedCreatePasswordRequest.displayInfo
+ assertThat(displayInfo.userDisplayName).isNull()
+ assertThat(displayInfo.userId).isEqualTo(idExpected)
+ assertThat(displayInfo.credentialTypeIcon!!.resId).isEqualTo(R.drawable.ic_password)
+ assertThat(displayInfo.preferDefaultProvider).isEqualTo(defaultProviderExpected)
+ assertThat(convertedRequest.credentialData.getString(customRequestDataKey))
+ .isEqualTo(customRequestDataValue)
+ assertThat(convertedRequest.candidateQueryData.getBoolean(customCandidateQueryDataKey))
+ .isEqualTo(customCandidateQueryDataValue)
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index b6bb51f..71072855 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -211,7 +211,7 @@
.isEqualTo(R.drawable.ic_passkey)
}
- @SdkSuppress(minSdkVersion = 34)
+ @SdkSuppress(minSdkVersion = 28)
@Test
fun frameworkConversion_success() {
val clientDataHashExpected = "hash".toByteArray()
@@ -266,4 +266,63 @@
assertThat(convertedRequest.candidateQueryData.getBoolean(customCandidateQueryDataKey))
.isEqualTo(customCandidateQueryDataValue)
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val clientDataHashExpected = "hash".toByteArray()
+ val originExpected = "origin"
+ val preferImmediatelyAvailableCredentialsExpected = true
+ val isAutoSelectAllowedExpected = true
+ val request =
+ CreatePublicKeyCredentialRequest(
+ TEST_REQUEST_JSON,
+ clientDataHashExpected,
+ preferImmediatelyAvailableCredentialsExpected,
+ originExpected,
+ isAutoSelectAllowedExpected
+ )
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val credentialData = getFinalCreateCredentialData(request, mContext)
+ val customRequestDataKey = "customRequestDataKey"
+ val customRequestDataValue = "customRequestDataValue"
+ credentialData.putString(customRequestDataKey, customRequestDataValue)
+ val candidateQueryData = request.candidateQueryData
+ val customCandidateQueryDataKey = "customRequestDataKey"
+ val customCandidateQueryDataValue = true
+ candidateQueryData.putBoolean(customCandidateQueryDataKey, customCandidateQueryDataValue)
+
+ val convertedRequest =
+ createFrom(
+ android.credentials.CreateCredentialRequest.Builder(
+ request.type,
+ credentialData,
+ candidateQueryData
+ )
+ .setOrigin(originExpected)
+ .setIsSystemProviderRequired(request.isSystemProviderRequired)
+ .build()
+ )
+
+ assertThat(convertedRequest).isInstanceOf(CreatePublicKeyCredentialRequest::class.java)
+ val convertedSubclassRequest = convertedRequest as CreatePublicKeyCredentialRequest
+ assertThat(convertedSubclassRequest.requestJson).isEqualTo(request.requestJson)
+ assertThat(convertedSubclassRequest.origin).isEqualTo(originExpected)
+ assertThat(convertedSubclassRequest.clientDataHash).isEqualTo(clientDataHashExpected)
+ assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
+ .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ assertThat(convertedSubclassRequest.isAutoSelectAllowed)
+ .isEqualTo(isAutoSelectAllowedExpected)
+ val displayInfo = convertedSubclassRequest.displayInfo
+ assertThat(displayInfo.userDisplayName).isEqualTo(TEST_USER_DISPLAYNAME)
+ assertThat(displayInfo.userId).isEqualTo(TEST_USERNAME)
+ assertThat(displayInfo.credentialTypeIcon!!.resId).isEqualTo(R.drawable.ic_passkey)
+ assertThat(convertedRequest.credentialData.getString(customRequestDataKey))
+ .isEqualTo(customRequestDataValue)
+ assertThat(convertedRequest.candidateQueryData.getBoolean(customCandidateQueryDataKey))
+ .isEqualTo(customCandidateQueryDataValue)
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CustomCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CustomCredentialTest.kt
index edf0d9a..87e38e1 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CustomCredentialTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CustomCredentialTest.kt
@@ -17,7 +17,9 @@
package androidx.credentials
import android.os.Bundle
+import androidx.credentials.Credential.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
@@ -51,4 +53,18 @@
assertThat(option.type).isEqualTo(expectedType)
assertThat(equals(option.data, expectedBundle)).isTrue()
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val expectedType = "TYPE"
+ val expectedBundle = Bundle()
+ expectedBundle.putString("Test", "Test")
+ val credential = CustomCredential(expectedType, expectedBundle)
+
+ val convertedCredential =
+ createFrom(android.credentials.Credential(credential.type, credential.data))
+
+ equals(convertedCredential, credential)
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/DigitalCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/DigitalCredentialJavaTest.java
new file mode 100644
index 0000000..2168792
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/DigitalCredentialJavaTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+import androidx.annotation.OptIn;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@OptIn(markerClass = ExperimentalDigitalCredentialApi.class)
+public class DigitalCredentialJavaTest {
+ private static final String TEST_CREDENTIAL_JSON =
+ "{\"protocol\":{\"preview\":{\"test\":\"val\"}}}";
+ @Test
+ public void typeConstant() {
+ assertThat(DigitalCredential.TYPE_DIGITAL_CREDENTIAL)
+ .isEqualTo("androidx.credentials.TYPE_DIGITAL_CREDENTIAL");
+ }
+
+ @Test
+ public void constructor_emptyCredentialJson_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new DigitalCredential("")
+ );
+ }
+
+
+ @Test
+ public void constructor_invalidCredentialJsonFormat_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new DigitalCredential("hello")
+ );
+ }
+
+ @Test
+ public void constructorAndGetter() {
+ DigitalCredential credential = new DigitalCredential(TEST_CREDENTIAL_JSON);
+ assertThat(credential.getCredentialJson()).isEqualTo(TEST_CREDENTIAL_JSON);
+ }
+
+ @Test
+ public void frameworkConversion_success() {
+ DigitalCredential credential = new DigitalCredential(TEST_CREDENTIAL_JSON);
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ Bundle data = credential.getData();
+ String customDataKey = "customRequestDataKey";
+ CharSequence customDataValue = "customRequestDataValue";
+ data.putCharSequence(customDataKey, customDataValue);
+
+ Credential convertedCredential = Credential.createFrom(
+ credential.getType(), data);
+
+ assertThat(convertedCredential).isInstanceOf(DigitalCredential.class);
+ DigitalCredential convertedSubclassCredential = (DigitalCredential) convertedCredential;
+ assertThat(convertedSubclassCredential.getCredentialJson())
+ .isEqualTo(credential.getCredentialJson());
+ assertThat(convertedCredential.getData().getCharSequence(customDataKey))
+ .isEqualTo(customDataValue);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/DigitalCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/DigitalCredentialTest.kt
new file mode 100644
index 0000000..7221999
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/DigitalCredentialTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.credentials.Credential.Companion.createFrom
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@OptIn(ExperimentalDigitalCredentialApi::class)
+class DigitalCredentialTest {
+ @Test
+ fun typeConstant() {
+ assertThat(DigitalCredential.TYPE_DIGITAL_CREDENTIAL)
+ .isEqualTo("androidx.credentials.TYPE_DIGITAL_CREDENTIAL")
+ }
+
+ @Test
+ fun constructor_emptyCredentialJson_throws() {
+ assertThrows(IllegalArgumentException::class.java) { DigitalCredential("") }
+ }
+
+ @Test
+ fun constructor_invalidCredentialJsonFormat_throws() {
+ assertThrows(IllegalArgumentException::class.java) { DigitalCredential("hello") }
+ }
+
+ @Test
+ fun constructorAndGetter() {
+ val credential = DigitalCredential(TEST_CREDENTIAL_JSON)
+ assertThat(credential.credentialJson).isEqualTo(TEST_CREDENTIAL_JSON)
+ }
+
+ @Test
+ fun frameworkConversion_success() {
+ val credential = DigitalCredential(TEST_CREDENTIAL_JSON)
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val data = credential.data
+ val customDataKey = "customRequestDataKey"
+ val customDataValue: CharSequence = "customRequestDataValue"
+ data.putCharSequence(customDataKey, customDataValue)
+
+ val convertedCredential = createFrom(credential.type, data)
+
+ assertThat(convertedCredential).isInstanceOf(DigitalCredential::class.java)
+ val convertedSubclassCredential = convertedCredential as DigitalCredential
+ assertThat(convertedSubclassCredential.credentialJson).isEqualTo(credential.credentialJson)
+ assertThat(convertedCredential.data.getCharSequence(customDataKey))
+ .isEqualTo(customDataValue)
+ }
+
+ companion object {
+ private const val TEST_CREDENTIAL_JSON = "{\"protocol\":{\"preview\":{\"test\":\"val\"}}}"
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
index 4232a1b..73c8083 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestJavaTest.java
@@ -220,7 +220,7 @@
GetCredentialRequest convertedRequest = GetCredentialRequest.createFrom(
- options, request.getOrigin(), GetCredentialRequest.toRequestDataBundle(request)
+ options, request.getOrigin(), GetCredentialRequest.getRequestMetadataBundle(request)
);
assertThat(convertedRequest.getOrigin()).isEqualTo(expectedOrigin);
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
index bf7c7c5..65961ef 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
@@ -18,8 +18,9 @@
import android.content.ComponentName
import androidx.credentials.GetCredentialRequest.Companion.createFrom
-import androidx.credentials.GetCredentialRequest.Companion.toRequestDataBundle
+import androidx.credentials.GetCredentialRequest.Companion.getRequestMetadataBundle
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
@@ -231,7 +232,52 @@
expectedPreferImmediatelyAvailableCredentials
)
- val convertedRequest = createFrom(options, request.origin, toRequestDataBundle(request))
+ val convertedRequest =
+ createFrom(options, request.origin, getRequestMetadataBundle(request))
+
+ assertThat(convertedRequest.origin).isEqualTo(expectedOrigin)
+ assertThat(convertedRequest.preferIdentityDocUi).isEqualTo(expectedPreferIdentityDocUi)
+ assertThat(convertedRequest.preferUiBrandingComponentName).isEqualTo(expectedComponentName)
+ assertThat(convertedRequest.preferImmediatelyAvailableCredentials)
+ .isEqualTo(expectedPreferImmediatelyAvailableCredentials)
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val options = java.util.ArrayList<CredentialOption>()
+ options.add(GetPasswordOption())
+ val expectedPreferImmediatelyAvailableCredentials = true
+ val expectedComponentName = ComponentName("test pkg", "test cls")
+ val expectedPreferIdentityDocUi = true
+ val expectedOrigin = "origin"
+ val request =
+ GetCredentialRequest(
+ options,
+ expectedOrigin,
+ expectedPreferIdentityDocUi,
+ expectedComponentName,
+ expectedPreferImmediatelyAvailableCredentials
+ )
+
+ val convertedRequest =
+ createFrom(
+ android.credentials.GetCredentialRequest.Builder(getRequestMetadataBundle(request))
+ .setOrigin(expectedOrigin)
+ .setCredentialOptions(
+ options.map {
+ android.credentials.CredentialOption.Builder(
+ it.type,
+ it.requestData,
+ it.candidateQueryData
+ )
+ .setAllowedProviders(it.allowedProviders)
+ .setIsSystemProviderRequired(it.isSystemProviderRequired)
+ .build()
+ }
+ )
+ .build()
+ )
assertThat(convertedRequest.origin).isEqualTo(expectedOrigin)
assertThat(convertedRequest.preferIdentityDocUi).isEqualTo(expectedPreferIdentityDocUi)
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
index ab3291b..c9c5e8a 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCustomCredentialOptionTest.kt
@@ -20,6 +20,7 @@
import android.os.Bundle
import androidx.credentials.CredentialOption.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
@@ -153,6 +154,45 @@
assertThat(actualOption.typePriorityHint).isEqualTo(expectedPriorityHint)
}
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val expectedType = "TYPE"
+ val expectedBundle = Bundle()
+ expectedBundle.putString("Test", "Test")
+ val expectedCandidateQueryDataBundle = Bundle()
+ expectedCandidateQueryDataBundle.putBoolean("key", true)
+ val expectedSystemProvider = true
+ val expectedAutoSelectAllowed = false
+ val expectedAllowedProviders: Set<ComponentName> =
+ setOf(ComponentName("pkg", "cls"), ComponentName("pkg2", "cls2"))
+ val expectedPriorityHint = CredentialOption.PRIORITY_OIDC_OR_SIMILAR
+ val option =
+ GetCustomCredentialOption(
+ expectedType,
+ expectedBundle,
+ expectedCandidateQueryDataBundle,
+ expectedSystemProvider,
+ expectedAutoSelectAllowed,
+ expectedAllowedProviders,
+ expectedPriorityHint
+ )
+
+ val convertedOption =
+ createFrom(
+ android.credentials.CredentialOption.Builder(
+ option.type,
+ option.requestData,
+ option.candidateQueryData
+ )
+ .setAllowedProviders(option.allowedProviders)
+ .setIsSystemProviderRequired(option.isSystemProviderRequired)
+ .build()
+ )
+
+ assertEquals(convertedOption, option)
+ }
+
private companion object {
private const val EXPECTED_CUSTOM_DEFAULT_PRIORITY = CredentialOption.PRIORITY_DEFAULT
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetDigitalCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetDigitalCredentialOptionJavaTest.java
new file mode 100644
index 0000000..ec4ecd1
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetDigitalCredentialOptionJavaTest.java
@@ -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.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+import androidx.annotation.OptIn;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@OptIn(markerClass = ExperimentalDigitalCredentialApi.class)
+public class GetDigitalCredentialOptionJavaTest {
+ private static final String TEST_REQUEST_JSON =
+ "{\"protocol\":{\"preview\":{\"test\":\"val\"}}}";
+
+ private static final int EXPECTED_PRIORITY =
+ CredentialOption.PRIORITY_PASSKEY_OR_SIMILAR;
+
+ @Test
+ public void constructorAndGetter() {
+ GetDigitalCredentialOption option = new GetDigitalCredentialOption(TEST_REQUEST_JSON);
+
+ assertThat(option.getRequestJson()).isEqualTo(TEST_REQUEST_JSON);
+ assertThat(option.getAllowedProviders()).isEmpty();
+ assertThat(option.isSystemProviderRequired()).isFalse();
+ assertThat(option.isAutoSelectAllowed()).isFalse();
+ assertThat(option.getType()).isEqualTo(DigitalCredential.TYPE_DIGITAL_CREDENTIAL);
+ assertThat(option.getTypePriorityHint()).isEqualTo(EXPECTED_PRIORITY);
+ }
+
+ @Test
+ public void frameworkConversion_success() {
+ GetDigitalCredentialOption option = new GetDigitalCredentialOption(TEST_REQUEST_JSON);
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ Bundle requestData = option.getRequestData();
+ String customRequestDataKey = "customRequestDataKey";
+ String customRequestDataValue = "customRequestDataValue";
+ requestData.putString(customRequestDataKey, customRequestDataValue);
+ Bundle candidateQueryData = option.getCandidateQueryData();
+ String customCandidateQueryDataKey = "customRequestDataKey";
+ boolean customCandidateQueryDataValue = true;
+ candidateQueryData.putBoolean(customCandidateQueryDataKey, customCandidateQueryDataValue);
+
+ CredentialOption convertedOption = CredentialOption.createFrom(
+ option.getType(), requestData, candidateQueryData,
+ option.isSystemProviderRequired(), option.getAllowedProviders());
+
+ assertThat(convertedOption).isInstanceOf(GetDigitalCredentialOption.class);
+ GetDigitalCredentialOption actualOption = (GetDigitalCredentialOption) convertedOption;
+ assertThat(actualOption.isAutoSelectAllowed()).isFalse();
+ assertThat(actualOption.getAllowedProviders()).isEmpty();
+ assertThat(actualOption.getRequestJson()).isEqualTo(TEST_REQUEST_JSON);
+ assertThat(convertedOption.getRequestData().getString(customRequestDataKey))
+ .isEqualTo(customRequestDataValue);
+ assertThat(convertedOption.getCandidateQueryData().getBoolean(customCandidateQueryDataKey))
+ .isEqualTo(customCandidateQueryDataValue);
+ assertThat(convertedOption.getTypePriorityHint()).isEqualTo(EXPECTED_PRIORITY);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetDigitalCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetDigitalCredentialOptionTest.kt
new file mode 100644
index 0000000..b0e1240f
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetDigitalCredentialOptionTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.credentials.CredentialOption.Companion.createFrom
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@OptIn(ExperimentalDigitalCredentialApi::class)
+class GetDigitalCredentialOptionTest {
+ @Test
+ fun constructorAndGetter() {
+ val option = GetDigitalCredentialOption(TEST_REQUEST_JSON)
+
+ assertThat(option.requestJson).isEqualTo(TEST_REQUEST_JSON)
+ assertThat(option.allowedProviders).isEmpty()
+ assertThat(option.isSystemProviderRequired).isFalse()
+ assertThat(option.isAutoSelectAllowed).isFalse()
+ assertThat(option.type).isEqualTo(DigitalCredential.TYPE_DIGITAL_CREDENTIAL)
+ assertThat(option.typePriorityHint).isEqualTo(EXPECTED_PRIORITY)
+ }
+
+ @Test
+ fun frameworkConversion_success() {
+ val option = GetDigitalCredentialOption(TEST_REQUEST_JSON)
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val requestData = option.requestData
+ val customRequestDataKey = "customRequestDataKey"
+ val customRequestDataValue = "customRequestDataValue"
+ requestData.putString(customRequestDataKey, customRequestDataValue)
+ val candidateQueryData = option.candidateQueryData
+ val customCandidateQueryDataKey = "customRequestDataKey"
+ val customCandidateQueryDataValue = true
+ candidateQueryData.putBoolean(customCandidateQueryDataKey, customCandidateQueryDataValue)
+
+ val convertedOption =
+ createFrom(
+ option.type,
+ requestData,
+ candidateQueryData,
+ option.isSystemProviderRequired,
+ option.allowedProviders
+ )
+
+ assertThat(convertedOption).isInstanceOf(GetDigitalCredentialOption::class.java)
+ val actualOption = convertedOption as GetDigitalCredentialOption
+ assertThat(actualOption.isAutoSelectAllowed).isFalse()
+ assertThat(actualOption.allowedProviders).isEmpty()
+ assertThat(actualOption.requestJson).isEqualTo(TEST_REQUEST_JSON)
+ assertThat(convertedOption.requestData.getString(customRequestDataKey))
+ .isEqualTo(customRequestDataValue)
+ assertThat(convertedOption.candidateQueryData.getBoolean(customCandidateQueryDataKey))
+ .isEqualTo(customCandidateQueryDataValue)
+ assertThat(convertedOption.typePriorityHint).isEqualTo(EXPECTED_PRIORITY)
+ }
+
+ companion object {
+ private const val TEST_REQUEST_JSON = "{\"protocol\":{\"preview\":{\"test\":\"val\"}}}"
+
+ private const val EXPECTED_PRIORITY = CredentialOption.PRIORITY_PASSKEY_OR_SIMILAR
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
index b32e9ef..a6a47e7 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
@@ -20,6 +20,7 @@
import androidx.credentials.CredentialOption.Companion.createFrom
import androidx.credentials.GetPasswordOption.Companion.BUNDLE_KEY_ALLOWED_USER_IDS
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.collect.ImmutableSet
import com.google.common.truth.Truth.assertThat
@@ -154,6 +155,47 @@
assertThat(option.typePriorityHint).isEqualTo(EXPECTED_PASSWORD_PRIORITY)
}
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val expectedIsAutoSelectAllowed = true
+ val expectedAllowedProviders: Set<ComponentName> =
+ ImmutableSet.of(ComponentName("pkg", "cls"), ComponentName("pkg2", "cls2"))
+ val expectedAllowedUserIds: Set<String> = ImmutableSet.of("id1", "id2", "id3")
+ val option =
+ GetPasswordOption(
+ expectedAllowedUserIds,
+ expectedIsAutoSelectAllowed,
+ expectedAllowedProviders
+ )
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val requestData = option.requestData
+ val customRequestDataKey = "customRequestDataKey"
+ val customRequestDataValue = "customRequestDataValue"
+ requestData.putString(customRequestDataKey, customRequestDataValue)
+ val candidateQueryData = option.candidateQueryData
+ val customCandidateQueryDataKey = "customRequestDataKey"
+ val customCandidateQueryDataValue = true
+ candidateQueryData.putBoolean(customCandidateQueryDataKey, customCandidateQueryDataValue)
+
+ val convertedOption =
+ createFrom(
+ android.credentials.CredentialOption.Builder(
+ option.type,
+ requestData,
+ candidateQueryData
+ )
+ .setAllowedProviders(option.allowedProviders)
+ .setIsSystemProviderRequired(option.isSystemProviderRequired)
+ .build()
+ )
+
+ assertEquals(convertedOption, option)
+ }
+
private companion object {
const val EXPECTED_PASSWORD_PRIORITY = CredentialOption.PRIORITY_PASSWORD_OR_SIMILAR
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
index 5cb1d34..b91e0bb 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
@@ -21,6 +21,7 @@
import androidx.credentials.CredentialOption.Companion.BUNDLE_KEY_TYPE_PRIORITY_VALUE
import androidx.credentials.CredentialOption.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.collect.ImmutableSet
import com.google.common.truth.Truth.assertThat
@@ -150,6 +151,46 @@
.isEqualTo(customCandidateQueryDataValue)
}
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val clientDataHash = "hash".toByteArray()
+ val expectedAllowedProviders: Set<ComponentName> =
+ ImmutableSet.of(ComponentName("pkg", "cls"), ComponentName("pkg2", "cls2"))
+ val option =
+ GetPublicKeyCredentialOption(
+ TEST_REQUEST_JSON,
+ clientDataHash,
+ expectedAllowedProviders
+ )
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val requestData = option.requestData
+ val customRequestDataKey = "customRequestDataKey"
+ val customRequestDataValue = "customRequestDataValue"
+ requestData.putString(customRequestDataKey, customRequestDataValue)
+ val candidateQueryData = option.candidateQueryData
+ val customCandidateQueryDataKey = "customRequestDataKey"
+ val customCandidateQueryDataValue = true
+ candidateQueryData.putBoolean(customCandidateQueryDataKey, customCandidateQueryDataValue)
+
+ val convertedOption =
+ createFrom(
+ android.credentials.CredentialOption.Builder(
+ option.type,
+ requestData,
+ candidateQueryData
+ )
+ .setAllowedProviders(option.allowedProviders)
+ .setIsSystemProviderRequired(option.isSystemProviderRequired)
+ .build()
+ )
+
+ assertEquals(convertedOption, option)
+ }
+
companion object Constant {
private const val TEST_REQUEST_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
const val EXPECTED_PASSKEY_PRIORITY = CredentialOption.PRIORITY_PASSKEY_OR_SIMILAR
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
index 81cfcb9b..656f68c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
@@ -19,6 +19,7 @@
import android.os.Bundle
import androidx.credentials.Credential.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
@@ -89,4 +90,22 @@
assertThat(convertedCredential.data.getCharSequence(customDataKey))
.isEqualTo(customDataValue)
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val credential = PasswordCredential("id", "password")
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val data = credential.data
+ val customDataKey = "customRequestDataKey"
+ val customDataValue: CharSequence = "customRequestDataValue"
+ data.putCharSequence(customDataKey, customDataValue)
+
+ val convertedCredential = createFrom(android.credentials.Credential(credential.type, data))
+
+ equals(convertedCredential, credential)
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
index 47c095f..b44ec90 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
@@ -19,6 +19,7 @@
import android.os.Bundle
import androidx.credentials.Credential.Companion.createFrom
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
@@ -90,7 +91,7 @@
}
@Test
- fun frameworkConversion_success() {
+ fun frameworkConversion_allApis_success() {
val credential = PublicKeyCredential(TEST_JSON)
// Add additional data to the request data and candidate query data to make sure
// they persist after the conversion
@@ -111,6 +112,24 @@
.isEqualTo(customDataValue)
}
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkConversion_frameworkClass_success() {
+ val credential = PublicKeyCredential(TEST_JSON)
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ // Add additional data to the request data and candidate query data to make sure
+ // they persist after the conversion
+ val data = credential.data
+ val customDataKey = "customRequestDataKey"
+ val customDataValue: CharSequence = "customRequestDataValue"
+ data.putCharSequence(customDataKey, customDataValue)
+
+ val convertedCredential = createFrom(android.credentials.Credential(credential.type, data))
+
+ equals(convertedCredential, credential)
+ }
+
@Test
fun staticProperty_hasCorrectTypeConstantValue() {
val typeExpected = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
index f8ccfc1..1c6aa3f 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
@@ -34,9 +34,11 @@
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.CustomCredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.ProviderClearCredentialStateRequest
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
import androidx.credentials.provider.PublicKeyCredentialEntry
+import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
@@ -341,6 +343,14 @@
assertEquals(context, actual.callingRequest, expected.callingRequest)
}
+fun assertEquals(
+ actual: ProviderClearCredentialStateRequest,
+ expected: ProviderClearCredentialStateRequest
+) {
+ if (actual === expected) return
+ assertThat(actual.callingAppInfo).isEqualTo(expected.callingAppInfo)
+}
+
@RequiresApi(23)
fun assertEquals(
context: Context,
@@ -567,3 +577,22 @@
assertThat(actual.isAutoSelectAllowed).isEqualTo(expected.isAutoSelectAllowed)
assertThat(actual.biometricPromptData).isEqualTo(expected.biometricPromptData)
}
+
+@Suppress("DEPRECATION")
+fun getTestCallingAppInfo(origin: String?): CallingAppInfo {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val packageName = context.packageName
+ if (Build.VERSION.SDK_INT >= 28) {
+ val packageInfo =
+ context.packageManager.getPackageInfo(
+ packageName,
+ PackageManager.GET_SIGNING_CERTIFICATES
+ )
+ Assert.assertNotNull(packageInfo.signingInfo)
+ return CallingAppInfo(packageName, packageInfo.signingInfo!!, origin)
+ } else {
+ val packageInfo =
+ context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
+ return CallingAppInfo(packageName, packageInfo.signatures!!.filterNotNull(), origin)
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialCustomExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialCustomExceptionTest.kt
index 7f35fe3..9595553 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialCustomExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialCustomExceptionTest.kt
@@ -19,6 +19,7 @@
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -59,7 +60,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialCustomException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialCustomException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -73,15 +74,15 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialCustomException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialCustomException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@Test
- fun bundleConversion_emptyBundle_returnsNull() {
- val actual = ClearCredentialException.fromBundle(Bundle())
-
- assertThat(actual).isNull()
+ fun bundleConversion_emptyBundle_throws() {
+ assertThrows(IllegalArgumentException::class.java) {
+ ClearCredentialException.fromBundle(Bundle())
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialInterruptedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialInterruptedExceptionTest.kt
index bd59df4..c82c438 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialInterruptedExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialInterruptedExceptionTest.kt
@@ -57,7 +57,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialInterruptedException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialInterruptedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -74,7 +74,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialInterruptedException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialInterruptedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialProviderConfigurationExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialProviderConfigurationExceptionTest.kt
index 1c21f20..ec1d44d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialProviderConfigurationExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialProviderConfigurationExceptionTest.kt
@@ -57,7 +57,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialProviderConfigurationException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialProviderConfigurationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -73,7 +73,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialProviderConfigurationException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialProviderConfigurationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnknownExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnknownExceptionTest.kt
index a676658..0ea513a 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnknownExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnknownExceptionTest.kt
@@ -53,7 +53,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialUnknownException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialUnknownException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -67,7 +67,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialUnknownException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialUnknownException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt
index 2186e92..4ea7392 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt
@@ -55,7 +55,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialUnsupportedException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialUnsupportedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -70,7 +70,7 @@
val actual =
ClearCredentialException.fromBundle(ClearCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(ClearCredentialUnsupportedException::class.java)
+ assertThat(actual).isInstanceOf(ClearCredentialUnsupportedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
index e165f7c..e54b51b 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
@@ -55,7 +55,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialCancellationException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialCancellationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -70,7 +70,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialCancellationException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialCancellationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCustomExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCustomExceptionTest.kt
index 06f797a..3a87aed 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCustomExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCustomExceptionTest.kt
@@ -19,6 +19,7 @@
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -59,7 +60,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialCustomException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialCustomException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -73,15 +74,15 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialCustomException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialCustomException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@Test
- fun bundleConversion_emptyBundle_returnsNull() {
- val actual = CreateCredentialException.fromBundle(Bundle())
-
- assertThat(actual).isNull()
+ fun bundleConversion_emptyBundle_throws() {
+ assertThrows(IllegalArgumentException::class.java) {
+ CreateCredentialException.fromBundle(Bundle())
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialInterruptedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialInterruptedExceptionTest.kt
index b7014594..ca45e78 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialInterruptedExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialInterruptedExceptionTest.kt
@@ -55,7 +55,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialInterruptedException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialInterruptedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -70,7 +70,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialInterruptedException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialInterruptedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialNoCreateOptionExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialNoCreateOptionExceptionTest.kt
index 48e74a3..4bf96bc 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialNoCreateOptionExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialNoCreateOptionExceptionTest.kt
@@ -59,7 +59,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(expectedClass)
+ assertThat(actual).isInstanceOf(expectedClass)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -72,7 +72,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(expectedClass)
+ assertThat(actual).isInstanceOf(expectedClass)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialProviderConfigurationExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialProviderConfigurationExceptionTest.kt
index 11058d4..f7670c1 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialProviderConfigurationExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialProviderConfigurationExceptionTest.kt
@@ -57,8 +57,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!)
- .isInstanceOf(CreateCredentialProviderConfigurationException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialProviderConfigurationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -74,8 +73,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!)
- .isInstanceOf(CreateCredentialProviderConfigurationException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialProviderConfigurationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnknownExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnknownExceptionTest.kt
index a78d1a1..566d8d4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnknownExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnknownExceptionTest.kt
@@ -53,7 +53,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialUnknownException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialUnknownException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -67,7 +67,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialUnknownException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialUnknownException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt
index 39f45f5..6d73526 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt
@@ -55,7 +55,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialUnsupportedException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialUnsupportedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -70,7 +70,7 @@
val actual =
CreateCredentialException.fromBundle(CreateCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(CreateCredentialUnsupportedException::class.java)
+ assertThat(actual).isInstanceOf(CreateCredentialUnsupportedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
index b204cf3..7ecb0b2 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
@@ -54,7 +54,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialCancellationException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialCancellationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -68,7 +68,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialCancellationException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialCancellationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCustomExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCustomExceptionTest.kt
index 01ceb05..21d40cd 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCustomExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCustomExceptionTest.kt
@@ -19,6 +19,7 @@
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -58,7 +59,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialCustomException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialCustomException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -71,15 +72,15 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialCustomException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialCustomException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@Test
- fun bundleConversion_emptyBundle_returnsNull() {
- val actual = GetCredentialException.fromBundle(Bundle())
-
- assertThat(actual).isNull()
+ fun bundleConversion_emptyBundle_throws() {
+ assertThrows(IllegalArgumentException::class.java) {
+ GetCredentialException.fromBundle(Bundle())
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialInterruptedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialInterruptedExceptionTest.kt
index 449753a..9821b7e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialInterruptedExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialInterruptedExceptionTest.kt
@@ -54,7 +54,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialInterruptedException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialInterruptedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -68,7 +68,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialInterruptedException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialInterruptedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialProviderConfigurationExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialProviderConfigurationExceptionTest.kt
index e8c2712..2fd5e31 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialProviderConfigurationExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialProviderConfigurationExceptionTest.kt
@@ -56,7 +56,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialProviderConfigurationException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialProviderConfigurationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -71,7 +71,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialProviderConfigurationException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialProviderConfigurationException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnknownExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnknownExceptionTest.kt
index 7a6727b..a1f4d1e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnknownExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnknownExceptionTest.kt
@@ -52,7 +52,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialUnknownException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialUnknownException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -65,7 +65,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialUnknownException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialUnknownException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt
index d99f7c7..b9f9206 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt
@@ -54,7 +54,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialUnsupportedException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialUnsupportedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -68,7 +68,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(GetCredentialUnsupportedException::class.java)
+ assertThat(actual).isInstanceOf(GetCredentialUnsupportedException::class.java)
assertThat(actual.type).isEqualTo(expectedType)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/NoCredentialExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/NoCredentialExceptionTest.kt
index 783a01a..19fa0dc 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/NoCredentialExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/NoCredentialExceptionTest.kt
@@ -52,7 +52,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(expectedClass)
+ assertThat(actual).isInstanceOf(expectedClass)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
@@ -64,7 +64,7 @@
val actual = GetCredentialException.fromBundle(GetCredentialException.asBundle(exception))
- assertThat(actual!!).isInstanceOf(expectedClass)
+ assertThat(actual).isInstanceOf(expectedClass)
assertThat(actual.errorMessage).isEqualTo(expectedMessage)
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
index 1e5593c..0a2d095 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
@@ -27,6 +27,7 @@
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
+import androidx.credentials.provider.utils.BiometricTestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
@@ -34,15 +35,13 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import javax.crypto.NullCipher;
-
@RunWith(AndroidJUnit4.class)
@SmallTest
@SdkSuppress(minSdkVersion = 35)
public class BiometricPromptDataJavaTest {
- private static final BiometricPrompt.CryptoObject TEST_CRYPTO_OBJECT = new
- BiometricPrompt.CryptoObject(new NullCipher());
+ private static final BiometricPrompt.CryptoObject TEST_CRYPTO_OBJECT = BiometricTestUtils
+ .INSTANCE.createCryptoObject$credentials_releaseAndroidTest();
private static final long DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID = 0L;
@@ -136,7 +135,6 @@
.isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
}
- @SdkSuppress(maxSdkVersion = 34)
@Test
public void fromBundle_validAllowedAuthenticator_success() {
Bundle inputBundle = new Bundle();
@@ -150,13 +148,12 @@
assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
}
- @SdkSuppress(minSdkVersion = 35)
@Test
public void fromBundle_validAllowedAuthenticatorAboveApi35_success() {
- int expectedOpId = Integer.MIN_VALUE;
+ long expectedOpId = TEST_CRYPTO_OBJECT.getOperationHandle();
Bundle inputBundle = new Bundle();
inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
- inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+ inputBundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
@@ -164,24 +161,21 @@
assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
TEST_ALLOWED_AUTHENTICATOR);
assertThat(actualBiometricPromptData.getCryptoObject()).isNotNull();
- assertThat(actualBiometricPromptData.getCryptoObject().hashCode())
- .isEqualTo(expectedOpId);
+ assertThat(actualBiometricPromptData.getCryptoObject().getOperationHandle())
+ .isEqualTo(TEST_CRYPTO_OBJECT.getOperationHandle());
}
@Test
public void fromBundle_unrecognizedAllowedAuthenticator_success() {
- int expectedOpId = Integer.MIN_VALUE;
Bundle inputBundle = new Bundle();
int unrecognizedAuthenticator = Integer.MAX_VALUE;
inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator);
- inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
assertThat(actualBiometricPromptData).isNotNull();
assertThat(actualBiometricPromptData.getAllowedAuthenticators())
.isEqualTo(unrecognizedAuthenticator);
-
}
@Test
@@ -197,7 +191,6 @@
assertThat(actualBiometricPromptData).isNull();
}
- @SdkSuppress(maxSdkVersion = 34)
@Test
public void toBundle_success() {
BiometricPromptData testBiometricPromptData = new BiometricPromptData(/*cryptoObject=*/null,
@@ -214,12 +207,11 @@
DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID);
}
- @SdkSuppress(minSdkVersion = 35)
@Test
public void toBundle_api35AndAboveWithOpId_success() {
BiometricPromptData testBiometricPromptData = new BiometricPromptData(TEST_CRYPTO_OBJECT,
TEST_ALLOWED_AUTHENTICATOR);
- long expectedOpId = TEST_CRYPTO_OBJECT.hashCode();
+ long expectedOpId = TEST_CRYPTO_OBJECT.getOperationHandle();
Bundle actualBundle = BiometricPromptData.toBundle(
testBiometricPromptData);
@@ -228,7 +220,7 @@
assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
TEST_ALLOWED_AUTHENTICATOR
);
- assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId);
+ assertThat(actualBundle.getLong(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId);
}
@Test
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
index 399a783..bdc1ea0 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
@@ -18,14 +18,13 @@
import android.os.Bundle
import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_CRYPTO_OP_ID
+import androidx.credentials.provider.utils.BiometricTestUtils
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
-import javax.crypto.NullCipher
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
@@ -129,7 +128,6 @@
}
}
- @SdkSuppress(maxSdkVersion = 34)
@Test
fun fromBundle_validAllowedAuthenticator_success() {
val inputBundle = Bundle()
@@ -143,13 +141,12 @@
assertThat(actualBiometricPromptData.cryptoObject).isNull()
}
- @SdkSuppress(minSdkVersion = 35)
@Test
fun fromBundle_validAllowedAuthenticatorAboveApi35_success() {
- val expectedOpId = Integer.MIN_VALUE
+ val expectedOpId = TEST_CRYPTO_OBJECT.operationHandle
val inputBundle = Bundle()
inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
- inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
+ inputBundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
@@ -157,7 +154,8 @@
assertThat(actualBiometricPromptData!!.allowedAuthenticators)
.isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
assertThat(actualBiometricPromptData.cryptoObject).isNotNull()
- assertThat(actualBiometricPromptData.cryptoObject!!.hashCode()).isEqualTo(expectedOpId)
+ assertThat(actualBiometricPromptData.cryptoObject!!.operationHandle)
+ .isEqualTo(TEST_CRYPTO_OBJECT.operationHandle)
}
@Test
@@ -186,7 +184,6 @@
assertThat(actualBiometricPromptData).isNull()
}
- @SdkSuppress(maxSdkVersion = 34)
@Test
fun toBundle_success() {
val testBiometricPromptData =
@@ -201,23 +198,22 @@
.isEqualTo(DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID)
}
- @SdkSuppress(minSdkVersion = 35)
@Test
fun toBundle_api35AndAboveWithOpId_success() {
val testBiometricPromptData =
BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
- val expectedOpId = TEST_CRYPTO_OBJECT.hashCode()
+ val expectedOpId = TEST_CRYPTO_OBJECT.operationHandle
val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
assertThat(actualBundle).isNotNull()
assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS))
.isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
- assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId)
+ assertThat(actualBundle.getLong(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId)
}
private companion object {
- private val TEST_CRYPTO_OBJECT = BiometricPrompt.CryptoObject(NullCipher())
+ private val TEST_CRYPTO_OBJECT = BiometricTestUtils.createCryptoObject()
private const val DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID = 0L
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi23Test.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi23Test.kt
index 63f1935..eb403dbc 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi23Test.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi23Test.kt
@@ -20,7 +20,6 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.os.Binder
@@ -47,6 +46,7 @@
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.domerrors.NotAllowedError
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
+import androidx.credentials.getTestCallingAppInfo
import androidx.credentials.internal.getFinalCreateCredentialData
import androidx.credentials.provider.PendingIntentHandler.Api23Impl.Companion.extractBeginGetCredentialResponse
import androidx.credentials.provider.PendingIntentHandler.Api23Impl.Companion.extractCreateCredentialException
@@ -74,7 +74,6 @@
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
-import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@@ -436,24 +435,6 @@
assertThat(actual.errorMessage).isEqualTo(expected.errorMessage)
}
- @Throws(Exception::class)
- private fun getTestCallingAppInfo(origin: String?): CallingAppInfo {
- val packageName = mContext.packageName
- if (Build.VERSION.SDK_INT >= 28) {
- val packageInfo =
- mContext.packageManager.getPackageInfo(
- packageName,
- PackageManager.GET_SIGNING_CERTIFICATES
- )
- Assert.assertNotNull(packageInfo.signingInfo)
- return CallingAppInfo(packageName, packageInfo.signingInfo!!, origin)
- } else {
- val packageInfo =
- mContext.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
- return CallingAppInfo(packageName, packageInfo.signatures!!.filterNotNull(), origin)
- }
- }
-
companion object {
private val ICON =
Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34JavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34JavaTest.java
index f6cf56d..031750f 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34JavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerApi34JavaTest.java
@@ -73,7 +73,7 @@
public void test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
for (int jetpackResult :
AuthenticationResult.Companion
- .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
+ .getBiometricFrameworkToJetpackResultMap$credentials_release().values()) {
BiometricPromptResult biometricPromptResult =
new BiometricPromptResult(new AuthenticationResult(jetpackResult));
android.service.credentials.CreateCredentialRequest request =
@@ -97,7 +97,7 @@
public void test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
for (int jetpackResult :
AuthenticationResult.Companion
- .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
+ .getBiometricFrameworkToJetpackResultMap$credentials_release().values()) {
BiometricPromptResult biometricPromptResult =
new BiometricPromptResult(new AuthenticationResult(jetpackResult));
Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
@@ -118,10 +118,10 @@
public void test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_resultConverted() {
for (int frameworkResult :
AuthenticationResult.Companion
- .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
+ .getBiometricFrameworkToJetpackResultMap$credentials_release().keySet()) {
BiometricPromptResult biometricPromptResult =
new BiometricPromptResult(
- AuthenticationResult.Companion.createFrom$credentials_debug(
+ AuthenticationResult.Companion.createFrom$credentials_release(
frameworkResult,
/*isFrameworkBiometricPrompt=*/true
));
@@ -129,7 +129,7 @@
TestUtilsKt.setUpCreatePasswordRequest();
int expectedResult =
AuthenticationResult.Companion
- .getBiometricFrameworkToJetpackResultMap$credentials_debug()
+ .getBiometricFrameworkToJetpackResultMap$credentials_release()
.get(frameworkResult);
Intent intent = prepareIntentWithCreateRequest(
request,
@@ -150,16 +150,16 @@
public void test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_resultConverted() {
for (int frameworkResult :
AuthenticationResult.Companion
- .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
+ .getBiometricFrameworkToJetpackResultMap$credentials_release().keySet()) {
BiometricPromptResult biometricPromptResult =
new BiometricPromptResult(
- AuthenticationResult.Companion.createFrom$credentials_debug(
+ AuthenticationResult.Companion.createFrom$credentials_release(
frameworkResult,
/*isFrameworkBiometricPrompt=*/true
));
int expectedResult =
AuthenticationResult.Companion
- .getBiometricFrameworkToJetpackResultMap$credentials_debug()
+ .getBiometricFrameworkToJetpackResultMap$credentials_release()
.get(frameworkResult);
Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
biometricPromptResult);
@@ -180,7 +180,7 @@
public void test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
for (int jetpackError :
AuthenticationError.Companion
- .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
+ .getBiometricFrameworkToJetpackErrorMap$credentials_release().values()) {
BiometricPromptResult biometricPromptResult =
new BiometricPromptResult(
new AuthenticationError(
@@ -207,7 +207,7 @@
public void test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
for (int jetpackError :
AuthenticationError.Companion
- .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
+ .getBiometricFrameworkToJetpackErrorMap$credentials_release().values()) {
BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
new AuthenticationError(
jetpackError,
@@ -232,10 +232,10 @@
public void test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_errorConverted() {
for (int frameworkError :
AuthenticationError.Companion
- .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
+ .getBiometricFrameworkToJetpackErrorMap$credentials_release().keySet()) {
BiometricPromptResult biometricPromptResult =
new BiometricPromptResult(
- AuthenticationError.Companion.createFrom$credentials_debug(
+ AuthenticationError.Companion.createFrom$credentials_release(
frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
/*isFrameworkBiometricPrompt=*/true
));
@@ -243,7 +243,7 @@
TestUtilsKt.setUpCreatePasswordRequest();
int expectedErrorCode =
AuthenticationError.Companion
- .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
+ .getBiometricFrameworkToJetpackErrorMap$credentials_release()
.get(frameworkError);
Intent intent = prepareIntentWithCreateRequest(
request, biometricPromptResult);
@@ -264,9 +264,9 @@
public void test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedErr() {
for (int frameworkError :
AuthenticationError.Companion
- .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
+ .getBiometricFrameworkToJetpackErrorMap$credentials_release().keySet()) {
BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
- AuthenticationError.Companion.createFrom$credentials_debug(
+ AuthenticationError.Companion.createFrom$credentials_release(
frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
/*isFrameworkBiometricPrompt=*/true
));
@@ -274,7 +274,7 @@
biometricPromptResult);
int expectedErrorCode =
AuthenticationError.Companion
- .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
+ .getBiometricFrameworkToJetpackErrorMap$credentials_release()
.get(frameworkError);
ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt
index 15f7ee2..eae43d7 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderClearCredentialStateRequestTest.kt
@@ -16,26 +16,47 @@
package androidx.credentials.provider
-import android.content.pm.SigningInfo
+import android.os.Bundle
+import androidx.credentials.assertEquals
import androidx.credentials.equals
+import androidx.credentials.getTestCallingAppInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-@SdkSuppress(minSdkVersion = 28)
@RunWith(AndroidJUnit4::class)
@SmallTest
class ProviderClearCredentialStateRequestTest {
@Test
fun testConstructor_success() {
- val callingAppInfo = CallingAppInfo("sample_package_name", SigningInfo())
+ val callingAppInfo = getTestCallingAppInfo("origin")
val request = ProviderClearCredentialStateRequest(callingAppInfo)
assertThat(equals(callingAppInfo, request.callingAppInfo)).isTrue()
}
+
+ @Test
+ fun bundleConversion_success() {
+ val callingAppInfo = getTestCallingAppInfo("origin")
+ val request = ProviderClearCredentialStateRequest(callingAppInfo)
+
+ val actualRequest =
+ ProviderClearCredentialStateRequest.fromBundle(
+ ProviderClearCredentialStateRequest.asBundle(request)
+ )
+
+ assertEquals(request, actualRequest)
+ }
+
+ @Test
+ fun bundleConversion_emptyBundle_throws() {
+ assertThrows(IllegalArgumentException::class.java) {
+ ProviderClearCredentialStateRequest.fromBundle(Bundle())
+ }
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderCreateCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderCreateCredentialRequestTest.kt
index 2abf7ae..25501104 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderCreateCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderCreateCredentialRequestTest.kt
@@ -16,15 +16,19 @@
package androidx.credentials.provider
-import android.content.pm.SigningInfo
+import android.os.Bundle
import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.assertEquals
+import androidx.credentials.getTestCallingAppInfo
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
-@SdkSuppress(minSdkVersion = 28)
+@SdkSuppress(minSdkVersion = 23)
@RunWith(AndroidJUnit4::class)
@SmallTest
class ProviderCreateCredentialRequestTest {
@@ -33,6 +37,29 @@
fun constructor_success() {
val request = CreatePasswordRequest("id", "password")
- ProviderCreateCredentialRequest(request, CallingAppInfo("name", SigningInfo()))
+ ProviderCreateCredentialRequest(request, getTestCallingAppInfo("origin"))
+ }
+
+ @Test
+ fun bundleConversion_success() {
+ val request =
+ ProviderCreateCredentialRequest(
+ CreatePasswordRequest("id", "password", "origin"),
+ getTestCallingAppInfo("origin")
+ )
+
+ val actualRequest =
+ ProviderCreateCredentialRequest.fromBundle(
+ ProviderCreateCredentialRequest.asBundle(request)
+ )
+
+ assertEquals(ApplicationProvider.getApplicationContext(), request, actualRequest)
+ }
+
+ @Test
+ fun bundleConversion_emptyBundle_throws() {
+ assertThrows(IllegalArgumentException::class.java) {
+ ProviderCreateCredentialRequest.fromBundle(Bundle())
+ }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt
index 6b22cdc..d5ed39e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ProviderGetCredentialRequestTest.kt
@@ -20,14 +20,16 @@
import android.content.pm.SigningInfo
import android.os.Bundle
import androidx.credentials.CredentialOption.Companion.createFrom
+import androidx.credentials.assertEquals
+import androidx.credentials.getTestCallingAppInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-@SdkSuppress(minSdkVersion = 28)
@RunWith(AndroidJUnit4::class)
@SmallTest
class ProviderGetCredentialRequestTest {
@@ -36,7 +38,7 @@
fun constructor_success() {
ProviderGetCredentialRequest(
listOf(createFrom("type", Bundle(), Bundle(), true, emptySet())),
- CallingAppInfo("name", SigningInfo())
+ getTestCallingAppInfo(null)
)
}
@@ -44,7 +46,7 @@
fun constructor_createFrom_success() {
ProviderGetCredentialRequest.createFrom(
listOf(createFrom("type", Bundle(), Bundle(), true, emptySet())),
- CallingAppInfo("name", SigningInfo())
+ getTestCallingAppInfo("origin")
)
}
@@ -74,7 +76,7 @@
expectedAllowedProviders
)
),
- CallingAppInfo("name", SigningInfo())
+ getTestCallingAppInfo(null)
)
val actualCredentialOptionsList = providerGetCredentialRequest.credentialOptions
assertThat(actualCredentialOptionsList.size).isEqualTo(1)
@@ -93,6 +95,7 @@
.containsAtLeastElementsIn(expectedAllowedProviders)
}
+ @SdkSuppress(minSdkVersion = 28)
@Test
fun getter_signingInfo() {
val expectedPackageName = "cool.security.package"
@@ -106,4 +109,25 @@
assertThat(actualPackageName).isEqualTo(expectedPackageName)
}
+
+ @Test
+ fun bundleConversion_success() {
+ val request =
+ ProviderGetCredentialRequest(
+ listOf(createFrom("type", Bundle(), Bundle(), true, emptySet())),
+ getTestCallingAppInfo("test-origin")
+ )
+
+ val actualRequest =
+ ProviderGetCredentialRequest.fromBundle(ProviderGetCredentialRequest.asBundle(request))
+
+ assertEquals(request, actualRequest)
+ }
+
+ @Test
+ fun bundleConversion_emptyBundle_throws() {
+ assertThrows(IllegalArgumentException::class.java) {
+ ProviderGetCredentialRequest.fromBundle(Bundle())
+ }
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
index 83a2d1d..6816e8d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
@@ -16,6 +16,8 @@
package androidx.credentials.provider.ui;
+import static androidx.credentials.provider.ui.UiUtils.testBiometricPromptData;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNotNull;
@@ -29,11 +31,7 @@
import android.graphics.drawable.Icon;
import android.os.Build;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricManager;
-import androidx.biometric.BiometricPrompt;
import androidx.core.os.BuildCompat;
-import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.CreateEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -45,8 +43,6 @@
import java.time.Instant;
-import javax.crypto.NullCipher;
-
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
@@ -204,12 +200,4 @@
testBiometricPromptData().getAllowedAuthenticators());
}
}
-
- @RequiresApi(35)
- private static BiometricPromptData testBiometricPromptData() {
- return new BiometricPromptData.Builder()
- .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build();
- }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
index 4c6810e..2f8838b 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
@@ -21,22 +21,18 @@
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
import androidx.core.os.BuildCompat
-import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CreateEntry.Companion.fromCreateEntry
import androidx.credentials.provider.CreateEntry.Companion.fromSlice
import androidx.credentials.provider.CreateEntry.Companion.toSlice
+import androidx.credentials.provider.ui.UiUtils.Companion.testBiometricPromptData
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
-import javax.crypto.NullCipher
import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -208,13 +204,5 @@
private const val LAST_USED_TIME = 10L
private val ICON =
Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
-
- @RequiresApi(35)
- private fun testBiometricPromptData(): BiometricPromptData {
- return BiometricPromptData.Builder()
- .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build()
- }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
index d93cd88..00f5651 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
@@ -16,6 +16,7 @@
package androidx.credentials.provider.ui;
import static androidx.credentials.CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
+import static androidx.credentials.provider.ui.UiUtils.testBiometricPromptData;
import static com.google.common.truth.Truth.assertThat;
@@ -32,15 +33,11 @@
import android.os.Bundle;
import android.service.credentials.CredentialEntry;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricManager;
-import androidx.biometric.BiometricPrompt;
import androidx.core.os.BuildCompat;
import androidx.credentials.R;
import androidx.credentials.TestUtilsKt;
import androidx.credentials.provider.BeginGetCredentialOption;
import androidx.credentials.provider.BeginGetCustomCredentialOption;
-import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.CustomCredentialEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -52,8 +49,6 @@
import java.time.Instant;
-import javax.crypto.NullCipher;
-
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
@@ -387,12 +382,4 @@
assertThat(entry.getBiometricPromptData()).isNull();
}
}
-
- @RequiresApi(35)
- private static BiometricPromptData testBiometricPromptData() {
- return new BiometricPromptData.Builder()
- .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build();
- }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
index 70aa96d..235ccf4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
@@ -22,27 +22,23 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.credentials.CredentialEntry
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
import androidx.core.os.BuildCompat
import androidx.credentials.CredentialOption
import androidx.credentials.R
import androidx.credentials.equals
import androidx.credentials.provider.BeginGetCredentialOption
import androidx.credentials.provider.BeginGetCustomCredentialOption
-import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CustomCredentialEntry
import androidx.credentials.provider.CustomCredentialEntry.Companion.fromCredentialEntry
import androidx.credentials.provider.CustomCredentialEntry.Companion.fromSlice
import androidx.credentials.provider.CustomCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.ui.UiUtils.Companion.testBiometricPromptData
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
-import javax.crypto.NullCipher
import org.junit.Assert
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertThrows
@@ -431,13 +427,5 @@
private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
private const val SINGLE_PROVIDER_ICON_BIT = true
private const val ENTRY_GROUP_ID = "entryGroupId"
-
- @RequiresApi(35)
- private fun testBiometricPromptData(): BiometricPromptData {
- return BiometricPromptData.Builder()
- .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build()
- }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
index 48ab2a9..da1e8b2 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
@@ -16,6 +16,7 @@
package androidx.credentials.provider.ui;
import static androidx.credentials.CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
+import static androidx.credentials.provider.ui.UiUtils.testBiometricPromptData;
import static com.google.common.truth.Truth.assertThat;
@@ -32,15 +33,11 @@
import android.os.Bundle;
import android.service.credentials.CredentialEntry;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricManager;
-import androidx.biometric.BiometricPrompt;
import androidx.core.os.BuildCompat;
import androidx.credentials.PasswordCredential;
import androidx.credentials.R;
import androidx.credentials.TestUtilsKt;
import androidx.credentials.provider.BeginGetPasswordOption;
-import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.PasswordCredentialEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -53,8 +50,6 @@
import java.time.Instant;
import java.util.HashSet;
-import javax.crypto.NullCipher;
-
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
@@ -401,12 +396,4 @@
assertThat(entry.getBiometricPromptData()).isNull();
}
}
-
- @RequiresApi(35)
- private static BiometricPromptData testBiometricPromptData() {
- return new BiometricPromptData.Builder()
- .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build();
- }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
index bd72745..a924a21 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
@@ -22,25 +22,21 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.credentials.CredentialEntry
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
import androidx.core.os.BuildCompat
import androidx.credentials.CredentialOption
import androidx.credentials.PasswordCredential
import androidx.credentials.R
import androidx.credentials.equals
import androidx.credentials.provider.BeginGetPasswordOption
-import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry.Companion.fromSlice
+import androidx.credentials.provider.ui.UiUtils.Companion.testBiometricPromptData
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
-import javax.crypto.NullCipher
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import org.junit.Assert
@@ -401,13 +397,5 @@
private val AFFILIATED_DOMAIN = "affiliation-name"
private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
private const val SINGLE_PROVIDER_ICON_BIT = true
-
- @RequiresApi(35)
- private fun testBiometricPromptData(): BiometricPromptData {
- return BiometricPromptData.Builder()
- .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build()
- }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
index fff3b7e..ba76140 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
@@ -16,6 +16,7 @@
package androidx.credentials.provider.ui;
import static androidx.credentials.CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
+import static androidx.credentials.provider.ui.UiUtils.testBiometricPromptData;
import static com.google.common.truth.Truth.assertThat;
@@ -31,15 +32,11 @@
import android.graphics.drawable.Icon;
import android.os.Bundle;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricManager;
-import androidx.biometric.BiometricPrompt;
import androidx.core.os.BuildCompat;
import androidx.credentials.PublicKeyCredential;
import androidx.credentials.R;
import androidx.credentials.TestUtilsKt;
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption;
-import androidx.credentials.provider.BiometricPromptData;
import androidx.credentials.provider.PublicKeyCredentialEntry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -51,8 +48,6 @@
import java.time.Instant;
-import javax.crypto.NullCipher;
-
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 26) // Instant usage
@SmallTest
@@ -272,12 +267,4 @@
assertThat(entry.getBiometricPromptData()).isNull();
}
}
-
- @RequiresApi(35)
- private static BiometricPromptData testBiometricPromptData() {
- return new BiometricPromptData.Builder()
- .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build();
- }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
index d4e521e..c328433 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
@@ -22,27 +22,23 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.credentials.CredentialEntry
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
import androidx.core.os.BuildCompat
import androidx.credentials.CredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.R
import androidx.credentials.equals
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
-import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromSlice
import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.ui.UiUtils.Companion.testBiometricPromptData
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.time.Instant
-import javax.crypto.NullCipher
import junit.framework.TestCase.assertNotNull
import org.junit.Assert
import org.junit.Assert.assertThrows
@@ -327,13 +323,5 @@
private const val IS_AUTO_SELECT_ALLOWED = true
private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
private const val SINGLE_PROVIDER_ICON_BIT = true
-
- @RequiresApi(35)
- private fun testBiometricPromptData(): BiometricPromptData {
- return BiometricPromptData.Builder()
- .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .build()
- }
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt
index 96dd0b5..6df5116 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/UiUtils.kt
@@ -22,13 +22,17 @@
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
import androidx.credentials.provider.Action
import androidx.credentials.provider.AuthenticationAction
import androidx.credentials.provider.BeginGetPasswordOption
+import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.RemoteEntry
+import androidx.credentials.provider.utils.BiometricTestUtils
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
@@ -105,5 +109,13 @@
fun constructRemoteEntry(): RemoteEntry {
return RemoteEntry(sPendingIntent)
}
+
+ @JvmStatic
+ @RequiresApi(35)
+ fun testBiometricPromptData(): BiometricPromptData =
+ BiometricPromptData.Builder()
+ .setCryptoObject(BiometricTestUtils.createCryptoObject())
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .build()
}
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt
new file mode 100644
index 0000000..b9ed941
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/utils/BiometricTestUtils.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.provider.utils
+
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricPrompt
+import java.security.KeyStore
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+
+@RequiresApi(35)
+object BiometricTestUtils {
+
+ /** A name used to refer to an app-specified secret key. */
+ private const val KEY_NAME = "mySecretKey"
+
+ /** The name of the Android keystore provider instance. */
+ private const val KEYSTORE_INSTANCE = "AndroidKeyStore"
+
+ /**
+ * Returns a [BiometricPrompt.CryptoObject] for crypto-based authentication. Adapted from:
+ * [package androidx.biometric.samples.auth].
+ */
+ @RequiresApi(35)
+ internal fun createCryptoObject(): BiometricPrompt.CryptoObject {
+ val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
+ val keySpec =
+ KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose)
+ .apply {
+ setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ // Disable user authentication requirement for test purposes.
+ // In reality, given this is primarily for biometric flows, Authenticator
+ // Types are expected, but emulators used in testing lack a
+ // lockscreen. This allows us to generate a more official
+ // CryptoObject instead of relying on mocks with this compromise.
+ setUserAuthenticationRequired(false)
+ }
+ .build()
+
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE).run {
+ init(keySpec)
+ generateKey()
+ }
+
+ val cipher =
+ Cipher.getInstance(
+ "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/" +
+ KeyProperties.ENCRYPTION_PADDING_PKCS7
+ )
+ .apply {
+ val keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE).apply { load(null) }
+ init(Cipher.ENCRYPT_MODE, keyStore.getKey(KEY_NAME, null) as SecretKey)
+ }
+
+ return BiometricPrompt.CryptoObject(cipher)
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
index 1d219bb..fa9b7ee 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
@@ -19,6 +19,7 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.text.TextUtils
+import androidx.annotation.Discouraged
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
@@ -180,7 +181,10 @@
* [CreateCredentialRequest.credentialData]
*/
@JvmStatic
- @RequiresApi(23)
+ @RequiresApi(23) // Icon dependency
+ @Discouraged(
+ "It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor"
+ )
fun createFrom(from: Bundle): DisplayInfo {
return try {
val displayInfoBundle = from.getBundle(BUNDLE_KEY_REQUEST_DISPLAY_INFO)!!
@@ -209,12 +213,39 @@
"androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
/**
+ * Parses the [request] into an instance of [CreateCredentialRequest].
+ *
+ * @param request the framework CreateCredentialRequest object
+ */
+ @JvmStatic
+ @RequiresApi(34)
+ @Discouraged(
+ "It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass"
+ )
+ fun createFrom(
+ request: android.credentials.CreateCredentialRequest
+ ): CreateCredentialRequest {
+ return createFrom(
+ request.type,
+ request.credentialData,
+ request.candidateQueryData,
+ request.isSystemProviderRequired,
+ request.origin
+ )
+ }
+
+ /**
* Attempts to parse the raw data into one of [CreatePasswordRequest],
* [CreatePublicKeyCredentialRequest], and [CreateCustomCredentialRequest].
*
* @param type matches [CreateCredentialRequest.type]
- * @param credentialData matches [CreateCredentialRequest.credentialData]
- * @param candidateQueryData matches [CreateCredentialRequest.candidateQueryData]
+ * @param credentialData matches [CreateCredentialRequest.credentialData], the request data
+ * in the [Bundle] format; this should be constructed and retrieved from the a given
+ * [CreateCredentialRequest] itself and never be created from scratch
+ * @param candidateQueryData matches [CreateCredentialRequest.candidateQueryData], the
+ * partial request data in the [Bundle] format that will be sent to the provider during
+ * the initial candidate query stage; this should be constructed and retrieved from the a
+ * given [CreateCredentialRequest] itself and never be created from scratch
* @param requireSystemProvider whether the request must only be fulfilled by a system
* provider
* @param origin the origin of a different application if the request is being made on
@@ -223,6 +254,9 @@
@JvmStatic
@JvmOverloads
@RequiresApi(23)
+ @Discouraged(
+ "It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass"
+ )
fun createFrom(
type: String,
credentialData: Bundle,
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialRequest.kt
index d650000..8b4929d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialRequest.kt
@@ -33,7 +33,8 @@
* 2.2 and above. If the cloud backup is not enabled, catch the [E2eeUnavailableException] and retry
* without cloud backup.
*
- * @param requestJson the request in JSON format in the standard webauthn web json
+ * @param requestJson the request in JSON format in the
+ * [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
* @param isCloudBackupEnabled whether the credential should be backed up to cloud.
* @throws E2eeUnavailableException if [isCloudBackupEnabled] was requested but the user device did
* not enable backup or e2ee (screen lock).
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt
index dcf9f2f..a2ed952 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt
@@ -17,21 +17,29 @@
package androidx.credentials
import android.os.Bundle
+import androidx.annotation.RestrictTo
import androidx.credentials.exceptions.CreateCredentialUnknownException
/**
* A response of the [RestoreCredential] flow.
*
- * @property responseJson the public key credential registration response in JSON format.
+ * @property responseJson the public key credential registration response in
+ * [JSON format](https://w3c.github.io/webauthn/#authenticatorattestationresponse).
*/
-class CreateRestoreCredentialResponse(
+class CreateRestoreCredentialResponse
+private constructor(
val responseJson: String,
data: Bundle,
) : CreateCredentialResponse(RestoreCredential.TYPE_RESTORE_CREDENTIAL, data) {
+
+ /** Constructs a [CreateRestoreCredentialResponse]. */
+ constructor(responseJson: String) : this(responseJson, toBundle(responseJson))
+
companion object {
const val BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE =
"androidx.credentials.BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE"
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun createFrom(data: Bundle): CreateRestoreCredentialResponse {
val responseJson =
@@ -41,5 +49,12 @@
)
return CreateRestoreCredentialResponse(responseJson, data)
}
+
+ @JvmStatic
+ internal fun toBundle(responseJson: String): Bundle {
+ val bundle = Bundle()
+ bundle.putString(BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE, responseJson)
+ return bundle
+ }
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
index 77bfdc7..6f20564 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
@@ -17,7 +17,8 @@
package androidx.credentials
import android.os.Bundle
-import androidx.annotation.RestrictTo
+import androidx.annotation.Discouraged
+import androidx.annotation.RequiresApi
import androidx.credentials.internal.FrameworkClassParsingException
/**
@@ -29,14 +30,25 @@
* [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL] for `PublicKeyCredential`)
* @property data the credential data in the [Bundle] format
*/
+@OptIn(ExperimentalDigitalCredentialApi::class)
abstract class Credential
internal constructor(
val type: String,
val data: Bundle,
) {
companion object {
+ /**
+ * Parses the raw data into an instance of [Credential].
+ *
+ * @param type matches [Credential.type], the credential type
+ * @param data matches [Credential.data], the credential data in the [Bundle] format; this
+ * should be constructed and retrieved from the a given [Credential] itself and never be
+ * created from scratch
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // used from java tests
+ @Discouraged(
+ "It is recommended to construct a Credential by directly instantiating a Credential subclass"
+ )
fun createFrom(type: String, data: Bundle): Credential {
return try {
when (type) {
@@ -45,6 +57,7 @@
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
PublicKeyCredential.createFrom(data)
RestoreCredential.TYPE_RESTORE_CREDENTIAL -> RestoreCredential.createFrom(data)
+ DigitalCredential.TYPE_DIGITAL_CREDENTIAL -> DigitalCredential.createFrom(data)
else -> throw FrameworkClassParsingException()
}
} catch (e: FrameworkClassParsingException) {
@@ -53,5 +66,19 @@
CustomCredential(type, data)
}
}
+
+ /**
+ * Parses the [credential] into an instance of [Credential].
+ *
+ * @param credential the framework Credential object
+ */
+ @JvmStatic
+ @RequiresApi(34)
+ @Discouraged(
+ "It is recommended to construct a Credential by directly instantiating a Credential subclass"
+ )
+ fun createFrom(credential: android.credentials.Credential): Credential {
+ return createFrom(credential.type, credential.data)
+ }
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
index 8d58380..89d293c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
@@ -18,7 +18,9 @@
import android.content.ComponentName
import android.os.Bundle
+import androidx.annotation.Discouraged
import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.credentials.internal.FrameworkClassParsingException
@@ -53,13 +55,15 @@
* the only one available option
* @property allowedProviders a set of provider service [ComponentName] allowed to receive this
* option (Note: a [SecurityException] will be thrown if it is set as non-empty but your app does
- * not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; for API level < 34, this
- * property will not take effect and you should control the allowed provider via
+ * not have android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; empty means every
+ * provider is eligible; for API level < 34, this property will not take effect and you should
+ * control the allowed provider via
* [library dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies))
* @property typePriorityHint sets the priority of this entry, which defines how it appears in the
* credential selector, with less precedence than account ordering but more precedence than last
* used time; see [PriorityHints] for more information
*/
+@OptIn(ExperimentalDigitalCredentialApi::class)
abstract class CredentialOption
internal constructor(
val type: String,
@@ -113,8 +117,44 @@
return data.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)
}
+ /**
+ * Parses the [option] into an instance of [CredentialOption].
+ *
+ * @param option the framework CredentialOption object
+ */
+ @RequiresApi(34)
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
+ @Discouraged(
+ "It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass"
+ )
+ fun createFrom(option: android.credentials.CredentialOption): CredentialOption {
+ return createFrom(
+ option.type,
+ option.credentialRetrievalData,
+ option.candidateQueryData,
+ option.isSystemProviderRequired,
+ option.allowedProviders
+ )
+ }
+
+ /**
+ * Parses the raw data into an instance of [CredentialOption].
+ *
+ * @param type matches [CredentialOption.type]
+ * @param requestData matches [CredentialOption.requestData], the request data in the
+ * [Bundle] format; this should be constructed and retrieved from the a given
+ * [CredentialOption] itself and never be created from scratch
+ * @param candidateQueryData matches [CredentialOption.candidateQueryData]; this should be
+ * constructed and retrieved from the a given [CredentialOption] itself and never be
+ * created from scratch
+ * @param requireSystemProvider matches [CredentialOption.isSystemProviderRequired]
+ * @param allowedProviders matches [CredentialOption.allowedProviders], empty means every
+ * provider is eligible
+ */
+ @JvmStatic
+ @Discouraged(
+ "It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass"
+ )
fun createFrom(
type: String,
requestData: Bundle,
@@ -141,6 +181,13 @@
)
else -> throw FrameworkClassParsingException()
}
+ DigitalCredential.TYPE_DIGITAL_CREDENTIAL ->
+ GetDigitalCredentialOption.createFrom(
+ requestData = requestData,
+ candidateQueryData = candidateQueryData,
+ requireSystemProvider = requireSystemProvider,
+ allowedProviders = allowedProviders,
+ )
else -> throw FrameworkClassParsingException()
}
} catch (e: FrameworkClassParsingException) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index ad97445..302316c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -25,6 +25,7 @@
import androidx.annotation.VisibleForTesting
/** Factory that returns the credential provider to be used by Credential Manager. */
+@OptIn(ExperimentalDigitalCredentialApi::class)
internal class CredentialProviderFactory(val context: Context) {
@set:VisibleForTesting
@@ -77,7 +78,13 @@
return tryCreatePreUOemProvider()
} else if (request is GetCredentialRequest) {
for (option in request.credentialOptions) {
- if (option is GetRestoreCredentialOption) {
+ if (option is GetRestoreCredentialOption || option is GetDigitalCredentialOption) {
+ if (request.credentialOptions.any { it !is GetDigitalCredentialOption }) {
+ throw IllegalArgumentException(
+ "`GetDigitalCredentialOption` cannot be" +
+ " combined with other option types in a single request"
+ )
+ }
return tryCreatePreUOemProvider()
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
index 397cde3..9eb1c0d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
@@ -256,7 +256,7 @@
): android.credentials.GetCredentialRequest {
val builder =
android.credentials.GetCredentialRequest.Builder(
- GetCredentialRequest.toRequestDataBundle(request)
+ GetCredentialRequest.getRequestMetadataBundle(request)
)
request.credentialOptions.forEach {
builder.addCredentialOption(
diff --git a/credentials/credentials/src/main/java/androidx/credentials/DigitalCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/DigitalCredential.kt
new file mode 100644
index 0000000..d4d9bd9
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/DigitalCredential.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import android.os.Bundle
+import androidx.credentials.internal.FrameworkClassParsingException
+import androidx.credentials.internal.RequestValidationHelper
+
+/**
+ * Represents the user's digital credential, generally used for verification or sign-in purposes.
+ *
+ * @property credentialJson the digital credential in the JSON format; the latest format is defined
+ * at https://wicg.github.io/digital-credentials/#the-digitalcredential-interface
+ */
+@ExperimentalDigitalCredentialApi
+class DigitalCredential
+private constructor(
+ val credentialJson: String,
+ data: Bundle,
+) : Credential(TYPE_DIGITAL_CREDENTIAL, data) {
+
+ init {
+ require(RequestValidationHelper.isValidJSON(credentialJson)) {
+ "credentialJson must not be empty, and must be a valid JSON"
+ }
+ }
+
+ /**
+ * Constructs a `DigitalCredential`.
+ *
+ * @param credentialJson the digital credential in the JSON format; the latest format is defined
+ * at https://wicg.github.io/digital-credentials/#the-digitalcredential-interface
+ * @throws IllegalArgumentException if the `credentialJson` is not a valid json
+ */
+ constructor(
+ credentialJson: String,
+ ) : this(credentialJson, toBundle(credentialJson))
+
+ /** Companion constants / helpers for [DigitalCredential]. */
+ companion object {
+ /** The type value for public key credential related operations. */
+ const val TYPE_DIGITAL_CREDENTIAL: String = "androidx.credentials.TYPE_DIGITAL_CREDENTIAL"
+
+ internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+
+ @JvmStatic
+ internal fun createFrom(data: Bundle): DigitalCredential {
+ try {
+ val credentialJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+ return DigitalCredential(credentialJson!!, data)
+ } catch (e: Exception) {
+ throw FrameworkClassParsingException()
+ }
+ }
+
+ @JvmStatic
+ internal fun toBundle(responseJson: String): Bundle {
+ val bundle = Bundle()
+ bundle.putString(BUNDLE_KEY_REQUEST_JSON, responseJson)
+ return bundle
+ }
+ }
+}
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/Placeholder.kt b/credentials/credentials/src/main/java/androidx/credentials/ExperimentalDigitalCredentialApi.kt
similarity index 65%
copy from room/room-paging/src/commonMain/kotlin/androidx/room/paging/Placeholder.kt
copy to credentials/credentials/src/main/java/androidx/credentials/ExperimentalDigitalCredentialApi.kt
index fb1781e..c1fb83f 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/Placeholder.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/ExperimentalDigitalCredentialApi.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,6 +14,10 @@
* limitations under the License.
*/
-package androidx.room.paging
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.credentials
+
+@RequiresOptIn(
+ "This CredentialManager API is experimental and is likely to change or to be removed in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalDigitalCredentialApi
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
index 786acbb..5f49e5e 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
@@ -18,7 +18,8 @@
import android.content.ComponentName
import android.os.Bundle
-import androidx.annotation.RestrictTo
+import androidx.annotation.Discouraged
+import androidx.annotation.RequiresApi
import androidx.credentials.internal.FrameworkClassParsingException
/**
@@ -168,7 +169,7 @@
}
}
- internal companion object {
+ companion object {
internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
private const val BUNDLE_KEY_PREFER_IDENTITY_DOC_UI =
@@ -176,9 +177,15 @@
private const val BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME =
"androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME"
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ /**
+ * Returns the request metadata as a `Bundle`.
+ *
+ * Note: this is not the equivalent of the complete request itself. For example, it does not
+ * include the request's `credentialOptions` or `origin`.
+ */
@JvmStatic
- fun toRequestDataBundle(request: GetCredentialRequest): Bundle {
+ @Discouraged("It should only be used by OEM services and library groups")
+ fun getRequestMetadataBundle(request: GetCredentialRequest): Bundle {
val bundle = Bundle()
bundle.putBoolean(BUNDLE_KEY_PREFER_IDENTITY_DOC_UI, request.preferIdentityDocUi)
bundle.putBoolean(
@@ -192,20 +199,49 @@
return bundle
}
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ /**
+ * Parses the [request] into an instance of [GetCredentialRequest].
+ *
+ * @param request the framework GetCredentialRequest object
+ */
+ @RequiresApi(34)
@JvmStatic
+ @Discouraged(
+ "It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest"
+ )
+ fun createFrom(request: android.credentials.GetCredentialRequest): GetCredentialRequest {
+ return createFrom(
+ request.credentialOptions.map { CredentialOption.createFrom(it) },
+ request.origin,
+ request.data
+ )
+ }
+
+ /**
+ * Parses the raw data into an instance of [GetCredentialRequest].
+ *
+ * @param credentialOptions matches [GetCredentialRequest.credentialOptions]
+ * @param origin matches [GetCredentialRequest.origin]
+ * @param metadata request metadata serialized as a Bundle using [getRequestMetadataBundle]
+ */
+ @JvmStatic
+ @Discouraged(
+ "It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest"
+ )
fun createFrom(
credentialOptions: List<CredentialOption>,
origin: String?,
- data: Bundle
+ metadata: Bundle
): GetCredentialRequest {
try {
- val preferIdentityDocUi = data.getBoolean(BUNDLE_KEY_PREFER_IDENTITY_DOC_UI)
+ val preferIdentityDocUi = metadata.getBoolean(BUNDLE_KEY_PREFER_IDENTITY_DOC_UI)
val preferImmediatelyAvailableCredentials =
- data.getBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+ metadata.getBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
@Suppress("DEPRECATION")
val preferUiBrandingComponentName =
- data.getParcelable<ComponentName>(BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME)
+ metadata.getParcelable<ComponentName>(
+ BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME
+ )
val getCredentialBuilder =
Builder()
.setCredentialOptions(credentialOptions)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetDigitalCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetDigitalCredentialOption.kt
new file mode 100644
index 0000000..b7b6c62
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetDigitalCredentialOption.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import android.content.ComponentName
+import android.os.Bundle
+import androidx.credentials.internal.FrameworkClassParsingException
+import androidx.credentials.internal.RequestValidationHelper
+
+/**
+ * A request to retrieve the user's digital credential, normally used for verification or sign-in
+ * purpose.
+ *
+ * Note that this option cannot be combined with other types of options in a single
+ * [GetCredentialRequest].
+ *
+ * @property requestJson the request in the JSON format; the latest format is defined at
+ * https://wicg.github.io/digital-credentials/#the-digitalcredentialrequestoptions-dictionary
+ */
+@ExperimentalDigitalCredentialApi
+class GetDigitalCredentialOption
+internal constructor(
+ val requestJson: String,
+ requestData: Bundle,
+ candidateQueryData: Bundle,
+ isSystemProviderRequired: Boolean,
+ isAutoSelectAllowed: Boolean,
+ allowedProviders: Set<ComponentName>,
+ typePriorityHint: @PriorityHints Int,
+) :
+ CredentialOption(
+ type = DigitalCredential.TYPE_DIGITAL_CREDENTIAL,
+ requestData = requestData,
+ candidateQueryData = candidateQueryData,
+ isSystemProviderRequired = isSystemProviderRequired,
+ isAutoSelectAllowed = isAutoSelectAllowed,
+ allowedProviders = allowedProviders,
+ typePriorityHint = typePriorityHint,
+ ) {
+
+ init {
+ require(RequestValidationHelper.isValidJSON(requestJson)) {
+ "credentialJson must not be empty, and must be a valid JSON"
+ }
+ }
+
+ /**
+ * Constructs a `GetDigitalCredentialOption`.
+ *
+ * Note that this option cannot be combined with other types of options in a single
+ * [GetCredentialRequest].
+ *
+ * @param requestJson the request in the JSON format; the latest format is defined at
+ * https://wicg.github.io/digital-credentials/#the-digitalcredentialrequestoptions-dictionary
+ * @throws IllegalArgumentException if the `credentialJson` is not a valid json
+ */
+ constructor(
+ requestJson: String
+ ) : this(
+ requestJson = requestJson,
+ requestData = toBundle(requestJson),
+ candidateQueryData = toBundle(requestJson),
+ isSystemProviderRequired = false,
+ isAutoSelectAllowed = false,
+ allowedProviders = emptySet(),
+ typePriorityHint = PRIORITY_PASSKEY_OR_SIMILAR,
+ )
+
+ /** Companion constants / helpers for [GetDigitalCredentialOption]. */
+ internal companion object {
+ internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+
+ @JvmStatic
+ internal fun toBundle(requestJson: String): Bundle {
+ val bundle = Bundle()
+ bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+ return bundle
+ }
+
+ @JvmStatic
+ internal fun createFrom(
+ requestData: Bundle,
+ candidateQueryData: Bundle,
+ requireSystemProvider: Boolean,
+ allowedProviders: Set<ComponentName>,
+ ): GetDigitalCredentialOption {
+ try {
+ val requestJson = requestData.getString(BUNDLE_KEY_REQUEST_JSON)!!
+ return GetDigitalCredentialOption(
+ requestJson = requestJson,
+ requestData = requestData,
+ candidateQueryData = candidateQueryData,
+ isSystemProviderRequired = requireSystemProvider,
+ isAutoSelectAllowed =
+ requestData.getBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false),
+ allowedProviders = allowedProviders,
+ typePriorityHint =
+ requestData.getInt(
+ BUNDLE_KEY_TYPE_PRIORITY_VALUE,
+ PRIORITY_PASSKEY_OR_SIMILAR
+ ),
+ )
+ } catch (e: Exception) {
+ throw FrameworkClassParsingException()
+ }
+ }
+ }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialException.kt
index bee84b5..73e1742 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialException.kt
@@ -34,14 +34,18 @@
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) open val type: String,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) open val errorMessage: CharSequence? = null
) : Exception(errorMessage?.toString()) {
- internal companion object {
+ companion object {
private const val EXTRA_CLEAR_CREDENTIAL_EXCEPTION_TYPE =
"androidx.credentials.provider.extra.CLEAR_CREDENTIAL_EXCEPTION_TYPE"
private const val EXTRA_CLEAR_CREDENTIAL_EXCEPTION_MESSAGE =
"androidx.credentials.provider.extra.CLEAR_CREDENTIAL_EXCEPTION_MESSAGE"
+ /**
+ * Helper method to convert the given [ex] to a parcelable [Bundle], in case the instance
+ * needs to be sent across a process. Consumers of this method should use [fromBundle] to
+ * reconstruct the class instance back from the bundle returned here.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun asBundle(ex: ClearCredentialException): Bundle {
val bundle = Bundle()
bundle.putString(EXTRA_CLEAR_CREDENTIAL_EXCEPTION_TYPE, ex.type)
@@ -51,10 +55,20 @@
return bundle
}
+ /**
+ * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
+ * [ClearCredentialException].
+ *
+ * Throws [IllegalArgumentException] if the conversion fails. This means that the given
+ * [bundle] does not contain a `ClearCredentialException`. The bundle should be constructed
+ * and retrieved from [asBundle] itself and never be created from scratch to avoid the
+ * failure.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun fromBundle(bundle: Bundle): ClearCredentialException? {
- val type = bundle.getString(EXTRA_CLEAR_CREDENTIAL_EXCEPTION_TYPE) ?: return null
+ fun fromBundle(bundle: Bundle): ClearCredentialException {
+ val type =
+ bundle.getString(EXTRA_CLEAR_CREDENTIAL_EXCEPTION_TYPE)
+ ?: throw IllegalArgumentException("Bundle was missing exception type.")
val msg = bundle.getCharSequence(EXTRA_CLEAR_CREDENTIAL_EXCEPTION_MESSAGE)
return when (type) {
ClearCredentialUnknownException.TYPE_CLEAR_CREDENTIAL_UNKNOWN_EXCEPTION ->
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
index 6e2dd88..ef37f09 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
@@ -36,14 +36,18 @@
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) open val type: String,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) open val errorMessage: CharSequence? = null
) : Exception(errorMessage?.toString()) {
- internal companion object {
+ companion object {
private const val EXTRA_CREATE_CREDENTIAL_EXCEPTION_TYPE =
"androidx.credentials.provider.extra.CREATE_CREDENTIAL_EXCEPTION_TYPE"
private const val EXTRA_CREATE_CREDENTIAL_EXCEPTION_MESSAGE =
"androidx.credentials.provider.extra.CREATE_CREDENTIAL_EXCEPTION_MESSAGE"
+ /**
+ * Helper method to convert the given [ex] to a parcelable [Bundle], in case the instance
+ * needs to be sent across a process. Consumers of this method should use [fromBundle] to
+ * reconstruct the class instance back from the bundle returned here.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun asBundle(ex: CreateCredentialException): Bundle {
val bundle = Bundle()
bundle.putString(EXTRA_CREATE_CREDENTIAL_EXCEPTION_TYPE, ex.type)
@@ -53,10 +57,20 @@
return bundle
}
+ /**
+ * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
+ * [CreateCredentialException].
+ *
+ * Throws [IllegalArgumentException] if the conversion fails. This means that the given
+ * [bundle] does not contain a `CreateCredentialException`. The bundle should be constructed
+ * and retrieved from [asBundle] itself and never be created from scratch to avoid the
+ * failure.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun fromBundle(bundle: Bundle): CreateCredentialException? {
- val type = bundle.getString(EXTRA_CREATE_CREDENTIAL_EXCEPTION_TYPE) ?: return null
+ fun fromBundle(bundle: Bundle): CreateCredentialException {
+ val type =
+ bundle.getString(EXTRA_CREATE_CREDENTIAL_EXCEPTION_TYPE)
+ ?: throw IllegalArgumentException("Bundle was missing exception type.")
val msg = bundle.getCharSequence(EXTRA_CREATE_CREDENTIAL_EXCEPTION_MESSAGE)
return toJetpackCreateException(type, msg)
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
index fe44c7f..9a861c5 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
@@ -36,14 +36,18 @@
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) open val type: String,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) open val errorMessage: CharSequence? = null
) : Exception(errorMessage?.toString()) {
- internal companion object {
+ companion object {
private const val EXTRA_GET_CREDENTIAL_EXCEPTION_TYPE =
"androidx.credentials.provider.extra.CREATE_CREDENTIAL_EXCEPTION_TYPE"
private const val EXTRA_GET_CREDENTIAL_EXCEPTION_MESSAGE =
"androidx.credentials.provider.extra.CREATE_CREDENTIAL_EXCEPTION_MESSAGE"
+ /**
+ * Helper method to convert the given [ex] to a parcelable [Bundle], in case the instance
+ * needs to be sent across a process. Consumers of this method should use [fromBundle] to
+ * reconstruct the class instance back from the bundle returned here.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun asBundle(ex: GetCredentialException): Bundle {
val bundle = Bundle()
bundle.putString(EXTRA_GET_CREDENTIAL_EXCEPTION_TYPE, ex.type)
@@ -53,10 +57,20 @@
return bundle
}
+ /**
+ * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
+ * [GetCredentialException].
+ *
+ * Throws [IllegalArgumentException] if the conversion fails. This means that the given
+ * [bundle] does not contain a `GetCredentialException`. The bundle should be constructed
+ * and retrieved from [asBundle] itself and never be created from scratch to avoid the
+ * failure.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun fromBundle(bundle: Bundle): GetCredentialException? {
- val type = bundle.getString(EXTRA_GET_CREDENTIAL_EXCEPTION_TYPE) ?: return null
+ fun fromBundle(bundle: Bundle): GetCredentialException {
+ val type =
+ bundle.getString(EXTRA_GET_CREDENTIAL_EXCEPTION_TYPE)
+ ?: throw IllegalArgumentException("Bundle was missing exception type.")
val msg = bundle.getCharSequence(EXTRA_GET_CREDENTIAL_EXCEPTION_MESSAGE)
return toJetpackGetException(type, msg)
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/ConversionUtils.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/ConversionUtils.kt
index f0bee73..d120681 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/internal/ConversionUtils.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/ConversionUtils.kt
@@ -74,10 +74,8 @@
return createCredentialData
}
-internal fun toJetpackGetException(
- errorType: String,
- errorMsg: CharSequence?
-): GetCredentialException {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+fun toJetpackGetException(errorType: String, errorMsg: CharSequence?): GetCredentialException {
return when (errorType) {
android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL ->
diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
index bea36df..a109d8a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
@@ -45,7 +45,7 @@
): android.credentials.GetCredentialRequest {
val builder =
android.credentials.GetCredentialRequest.Builder(
- GetCredentialRequest.toRequestDataBundle(request)
+ GetCredentialRequest.getRequestMetadataBundle(request)
)
request.credentialOptions.forEach {
builder.addCredentialOption(
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
index 81161df..bdfbcfa 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -286,9 +286,13 @@
fun retrieveProviderCreateCredentialRequest(
intent: Intent
): ProviderCreateCredentialRequest? {
- return ProviderCreateCredentialRequest.fromBundle(
- intent.getBundleExtra(EXTRA_CREATE_CREDENTIAL_REQUEST) ?: return null
- )
+ return try {
+ ProviderCreateCredentialRequest.fromBundle(
+ intent.getBundleExtra(EXTRA_CREATE_CREDENTIAL_REQUEST) ?: return null
+ )
+ } catch (e: Exception) {
+ null
+ }
}
private const val EXTRA_BEGIN_GET_CREDENTIAL_REQUEST =
@@ -342,9 +346,13 @@
fun retrieveProviderGetCredentialRequest(
intent: Intent
): ProviderGetCredentialRequest? {
- return ProviderGetCredentialRequest.fromBundle(
- intent.getBundleExtra(EXTRA_GET_CREDENTIAL_REQUEST) ?: return null
- )
+ return try {
+ ProviderGetCredentialRequest.fromBundle(
+ intent.getBundleExtra(EXTRA_GET_CREDENTIAL_REQUEST) ?: return null
+ )
+ } catch (e: Exception) {
+ null
+ }
}
private const val EXTRA_GET_CREDENTIAL_RESPONSE =
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt
index f1f0c63..14e812a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderClearCredentialStateRequest.kt
@@ -17,7 +17,6 @@
package androidx.credentials.provider
import android.os.Bundle
-import androidx.annotation.RestrictTo
import androidx.credentials.provider.CallingAppInfo.Companion.extractCallingAppInfo
import androidx.credentials.provider.CallingAppInfo.Companion.setCallingAppInfo
@@ -32,19 +31,33 @@
* production flow. This constructor must only be used for testing purposes.
*/
class ProviderClearCredentialStateRequest constructor(val callingAppInfo: CallingAppInfo) {
- internal companion object {
+ companion object {
+ /**
+ * Helper method to convert the given [request] to a parcelable [Bundle], in case the
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [fromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun asBundle(request: ProviderClearCredentialStateRequest): Bundle {
val bundle = Bundle()
bundle.setCallingAppInfo(request.callingAppInfo)
return bundle
}
+ /**
+ * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
+ * [ProviderClearCredentialStateRequest].
+ *
+ * Throws [IllegalArgumentException] if the conversion fails. This means that the given
+ * [bundle] does not contain a `ProviderCreateCredentialRequest`. The bundle should be
+ * constructed and retrieved from [asBundle] itself and never be created from scratch to
+ * avoid the failure.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun fromBundle(bundle: Bundle): ProviderClearCredentialStateRequest? {
- val callingAppInfo = extractCallingAppInfo(bundle) ?: return null
+ fun fromBundle(bundle: Bundle): ProviderClearCredentialStateRequest {
+ val callingAppInfo =
+ extractCallingAppInfo(bundle)
+ ?: throw IllegalArgumentException("Bundle was missing CallingAppInfo.")
return ProviderClearCredentialStateRequest(callingAppInfo)
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
index d6b19cd..577f02b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
@@ -17,7 +17,6 @@
import android.os.Bundle
import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.provider.CallingAppInfo.Companion.EXTRA_CREDENTIAL_REQUEST_ORIGIN
import androidx.credentials.provider.CallingAppInfo.Companion.extractCallingAppInfo
@@ -49,7 +48,7 @@
val callingAppInfo: CallingAppInfo,
val biometricPromptResult: BiometricPromptResult? = null
) {
- internal companion object {
+ companion object {
private const val EXTRA_CREATE_CREDENTIAL_REQUEST_TYPE =
"androidx.credentials.provider.extra.CREATE_CREDENTIAL_REQUEST_TYPE"
private const val EXTRA_CREATE_REQUEST_CANDIDATE_QUERY_DATA =
@@ -58,9 +57,13 @@
private const val EXTRA_CREATE_REQUEST_CREDENTIAL_DATA =
"androidx.credentials.provider.extra.CREATE_REQUEST_CREDENTIAL_DATA"
+ /**
+ * Helper method to convert the given [request] to a parcelable [Bundle], in case the
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [fromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
@JvmStatic
- @RequiresApi(23)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RequiresApi(23) // Icon dependency
fun asBundle(request: ProviderCreateCredentialRequest): Bundle {
val bundle = Bundle()
bundle.putString(EXTRA_CREATE_CREDENTIAL_REQUEST_TYPE, request.callingRequest.type)
@@ -76,18 +79,29 @@
return bundle
}
+ /**
+ * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
+ * [ProviderCreateCredentialRequest].
+ *
+ * Throws [IllegalArgumentException] if the conversion fails. This means that the given
+ * [bundle] does not contain a `ProviderCreateCredentialRequest`. The bundle should be
+ * constructed and retrieved from [asBundle] itself and never be created from scratch to
+ * avoid the failure.
+ */
+ @RequiresApi(23) // Icon dependency
@JvmStatic
- @RequiresApi(23)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun fromBundle(bundle: Bundle): ProviderCreateCredentialRequest? {
+ fun fromBundle(bundle: Bundle): ProviderCreateCredentialRequest {
val requestType: String =
- bundle.getString(EXTRA_CREATE_CREDENTIAL_REQUEST_TYPE) ?: return null
+ bundle.getString(EXTRA_CREATE_CREDENTIAL_REQUEST_TYPE)
+ ?: throw IllegalArgumentException("Bundle was missing request type.")
val requestData: Bundle =
bundle.getBundle(EXTRA_CREATE_REQUEST_CREDENTIAL_DATA) ?: Bundle()
val candidateQueryData: Bundle =
bundle.getBundle(EXTRA_CREATE_REQUEST_CANDIDATE_QUERY_DATA) ?: Bundle()
val origin = bundle.getString(EXTRA_CREDENTIAL_REQUEST_ORIGIN)
- val callingAppInfo = extractCallingAppInfo(bundle) ?: return null
+ val callingAppInfo =
+ extractCallingAppInfo(bundle)
+ ?: throw IllegalArgumentException("Bundle was missing CallingAppInfo.")
return try {
ProviderCreateCredentialRequest(
@@ -102,7 +116,7 @@
callingAppInfo = callingAppInfo,
)
} catch (e: Exception) {
- return null
+ throw IllegalArgumentException("Conversion failed with $e")
}
}
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
index 65cd2921..2cdcdaa 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
@@ -18,7 +18,6 @@
import android.app.PendingIntent
import android.content.ComponentName
import android.os.Bundle
-import androidx.annotation.RestrictTo
import androidx.credentials.CredentialOption
import androidx.credentials.provider.CallingAppInfo.Companion.extractCallingAppInfo
import androidx.credentials.provider.CallingAppInfo.Companion.setCallingAppInfo
@@ -54,7 +53,7 @@
val callingAppInfo: CallingAppInfo,
val biometricPromptResult: BiometricPromptResult? = null,
) {
- internal companion object {
+ companion object {
@JvmStatic
internal fun createFrom(
options: List<CredentialOption>,
@@ -77,8 +76,12 @@
private const val EXTRA_CREDENTIAL_OPTION_ALLOWED_PROVIDERS_PREFIX =
"androidx.credentials.provider.extra.CREDENTIAL_OPTION_ALLOWED_PROVIDERS_"
+ /**
+ * Helper method to convert the given [request] to a parcelable [Bundle], in case the
+ * instance needs to be sent across a process. Consumers of this method should use
+ * [fromBundle] to reconstruct the class instance back from the bundle returned here.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
fun asBundle(request: ProviderGetCredentialRequest): Bundle {
val bundle = Bundle()
val optionSize = request.credentialOptions.size
@@ -107,23 +110,41 @@
return bundle
}
+ /**
+ * Helper method to convert a [Bundle] retrieved through [asBundle], back to an instance of
+ * [ProviderGetCredentialRequest].
+ *
+ * Throws [IllegalArgumentException] if the conversion fails. This means that the given
+ * [bundle] does not contain a `ProviderGetCredentialRequest`. The bundle should be
+ * constructed and retrieved from [asBundle] itself and never be created from scratch to
+ * avoid the failure.
+ */
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun fromBundle(bundle: Bundle): ProviderGetCredentialRequest? {
- val callingAppInfo = extractCallingAppInfo(bundle) ?: return null
+ fun fromBundle(bundle: Bundle): ProviderGetCredentialRequest {
+ val callingAppInfo =
+ extractCallingAppInfo(bundle)
+ ?: throw IllegalArgumentException("Bundle was missing CallingAppInfo.")
val optionSize = bundle.getInt(EXTRA_CREDENTIAL_OPTION_SIZE, -1)
if (optionSize < 0) {
- return null
+ throw IllegalArgumentException("Bundle had invalid option size as $optionSize.")
}
val options = mutableListOf<CredentialOption>()
for (i in 0 until optionSize) {
- val type = bundle.getString("$EXTRA_CREDENTIAL_OPTION_TYPE_PREFIX$i") ?: return null
+ val type =
+ bundle.getString("$EXTRA_CREDENTIAL_OPTION_TYPE_PREFIX$i")
+ ?: throw IllegalArgumentException(
+ "Bundle was missing option type at index $optionSize."
+ )
val candidateQueryData =
bundle.getBundle("$EXTRA_CREDENTIAL_OPTION_CANDIDATE_QUERY_DATA_PREFIX$i")
- ?: return null
+ ?: throw IllegalArgumentException(
+ "Bundle was missing candidate query data at index $optionSize."
+ )
val requestData =
bundle.getBundle("$EXTRA_CREDENTIAL_OPTION_CREDENTIAL_RETRIEVAL_DATA_PREFIX$i")
- ?: return null
+ ?: throw IllegalArgumentException(
+ "Bundle was missing request data at index $optionSize."
+ )
val isSystemProviderRequired =
bundle.getBoolean(
"$EXTRA_CREDENTIAL_OPTION_IS_SYSTEM_PROVIDER_REQUIRED_PREFIX$i",
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index f184764..d509e6a 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -28,8 +28,6 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("com.google.protobuf")
- id ("kotlin-parcelize")
}
android {
@@ -42,26 +40,6 @@
namespace "androidx.datastore.core"
}
-protobuf {
- protoc {
- artifact = libs.protobufCompiler.get()
- }
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- java {
- option "lite"
- }
- }
- }
- }
-}
-
-def protoDir = project.layout.projectDirectory.dir("src/androidInstrumentedTest/proto")
-tasks.named("extractAndroidTestProto").configure {
- it.inputFiles.from(project.files(protoDir))
-}
-
androidXMultiplatform {
jvm()
mac()
@@ -137,7 +115,6 @@
androidInstrumentedTest {
dependsOn(commonJvmTest)
dependencies {
- implementation(libs.protobufLite)
implementation(libs.truth)
implementation(project(":internal-testutils-truth"))
implementation(libs.testRunner)
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/AndroidManifest.xml b/datastore/datastore-core/src/androidInstrumentedTest/AndroidManifest.xml
index c25da00..4ec52f2f 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/AndroidManifest.xml
+++ b/datastore/datastore-core/src/androidInstrumentedTest/AndroidManifest.xml
@@ -15,16 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <application>
- <service android:name="androidx.datastore.core.twoWayIpc.TwoWayIpcService"
- android:enabled="true"
- android:exported="false"
- android:process=":TwoWayIpcService" />
- <service android:name="androidx.datastore.core.twoWayIpc.TwoWayIpcService2"
- android:enabled="true"
- android:exported="false"
- android:process=":TwoWayIpcService2" />
- </application>
-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
diff --git a/datastore/integration-tests/testapp/build.gradle b/datastore/integration-tests/testapp/build.gradle
new file mode 100644
index 0000000..8266634
--- /dev/null
+++ b/datastore/integration-tests/testapp/build.gradle
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("com.google.protobuf")
+ id ("kotlin-parcelize")
+}
+
+android {
+ namespace "androidx.datastore.testapp"
+}
+
+protobuf {
+ protoc {
+ artifact = libs.protobufCompiler.get()
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {
+ option "lite"
+ }
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.okio)
+ implementation(libs.protobufLite)
+ implementation("androidx.lifecycle:lifecycle-service:2.6.1")
+ implementation(project(":datastore:datastore-core"))
+ implementation(project(":datastore:datastore-core-okio"))
+
+ androidTestImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation(libs.kotlinTest)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(project(":internal-testutils-datastore"))
+ androidTestImplementation(project(":kruth:kruth"))
+}
diff --git a/datastore/integration-tests/testapp/src/androidTest/AndroidManifest.xml b/datastore/integration-tests/testapp/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..867fda8
--- /dev/null
+++ b/datastore/integration-tests/testapp/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <service android:name="androidx.datastore.testapp.twoWayIpc.TwoWayIpcService"
+ android:enabled="true"
+ android:exported="false"
+ android:process=":TwoWayIpcService" />
+ <service android:name="androidx.datastore.testapp.twoWayIpc.TwoWayIpcService2"
+ android:enabled="true"
+ android:exported="false"
+ android:process=":TwoWayIpcService2" />
+ </application>
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+</manifest>
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/InterProcessCompletableTest.kt
similarity index 89%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt
rename to datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/InterProcessCompletableTest.kt
index 9623f58..c6233a5 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/InterProcessCompletableTest.kt
+++ b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/InterProcessCompletableTest.kt
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess
+package androidx.datastore.testapp.multiprocess
import android.os.Parcelable
-import androidx.datastore.core.twoWayIpc.InterProcessCompletable
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.IpcUnit
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+import androidx.datastore.testapp.twoWayIpc.InterProcessCompletable
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.IpcUnit
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.async
import kotlinx.coroutines.yield
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultiProcessDataStoreIpcTest.kt
similarity index 96%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
rename to datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultiProcessDataStoreIpcTest.kt
index 86887fc..b13ee2d 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
+++ b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultiProcessDataStoreIpcTest.kt
@@ -14,21 +14,24 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess
+// Parcelize object is testing internal implementation of datastore-core library
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
+
+package androidx.datastore.testapp.multiprocess
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.CorruptionHandler
import androidx.datastore.core.IOException
import androidx.datastore.core.SharedCounter
-import androidx.datastore.core.multiprocess.ipcActions.ReadTextAction
-import androidx.datastore.core.multiprocess.ipcActions.SetTextAction
-import androidx.datastore.core.multiprocess.ipcActions.StorageVariant
-import androidx.datastore.core.multiprocess.ipcActions.createMultiProcessTestDatastore
-import androidx.datastore.core.multiprocess.ipcActions.datastore
-import androidx.datastore.core.twoWayIpc.InterProcessCompletable
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.IpcUnit
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+import androidx.datastore.testapp.multiprocess.ipcActions.ReadTextAction
+import androidx.datastore.testapp.multiprocess.ipcActions.SetTextAction
+import androidx.datastore.testapp.multiprocess.ipcActions.StorageVariant
+import androidx.datastore.testapp.multiprocess.ipcActions.createMultiProcessTestDatastore
+import androidx.datastore.testapp.multiprocess.ipcActions.datastore
+import androidx.datastore.testapp.twoWayIpc.InterProcessCompletable
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.IpcUnit
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import androidx.datastore.testing.TestMessageProto.FooProto
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CompletableDeferred
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultiProcessTestRule.kt
similarity index 92%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt
rename to datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultiProcessTestRule.kt
index c2cbb5b..82df64d 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessTestRule.kt
+++ b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultiProcessTestRule.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess
+package androidx.datastore.testapp.multiprocess
-import androidx.datastore.core.twoWayIpc.TwoWayIpcConnection
-import androidx.datastore.core.twoWayIpc.TwoWayIpcService
-import androidx.datastore.core.twoWayIpc.TwoWayIpcService2
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcConnection
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcService
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcService2
import androidx.test.platform.app.InstrumentationRegistry
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.seconds
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
similarity index 91%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
rename to datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
index 00442d7..9b07814 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
+++ b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/MultipleDataStoresInMultipleProcessesTest.kt
@@ -14,19 +14,22 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess
+// Parcelize object is testing internal implementation of datastore-core library
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-import androidx.datastore.core.multiprocess.ipcActions.ReadTextAction
-import androidx.datastore.core.multiprocess.ipcActions.SetTextAction
-import androidx.datastore.core.multiprocess.ipcActions.StorageVariant
-import androidx.datastore.core.multiprocess.ipcActions.createMultiProcessTestDatastore
-import androidx.datastore.core.multiprocess.ipcActions.datastore
-import androidx.datastore.core.twoWayIpc.CompositeServiceSubjectModel
-import androidx.datastore.core.twoWayIpc.InterProcessCompletable
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.IpcUnit
-import androidx.datastore.core.twoWayIpc.SubjectReadWriteProperty
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+package androidx.datastore.testapp.multiprocess
+
+import androidx.datastore.testapp.multiprocess.ipcActions.ReadTextAction
+import androidx.datastore.testapp.multiprocess.ipcActions.SetTextAction
+import androidx.datastore.testapp.multiprocess.ipcActions.StorageVariant
+import androidx.datastore.testapp.multiprocess.ipcActions.createMultiProcessTestDatastore
+import androidx.datastore.testapp.multiprocess.ipcActions.datastore
+import androidx.datastore.testapp.twoWayIpc.CompositeServiceSubjectModel
+import androidx.datastore.testapp.twoWayIpc.InterProcessCompletable
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.IpcUnit
+import androidx.datastore.testapp.twoWayIpc.SubjectReadWriteProperty
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import androidx.datastore.testing.TestMessageProto.FooProto
import androidx.kruth.assertThat
import kotlin.time.Duration.Companion.seconds
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/TwoWayIpcTest.kt
similarity index 95%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt
rename to datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/TwoWayIpcTest.kt
index 3cf76e5..a6d77b6 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/TwoWayIpcTest.kt
+++ b/datastore/integration-tests/testapp/src/androidTest/java/androidx/datastore/testapp/multiprocess/TwoWayIpcTest.kt
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess
+package androidx.datastore.testapp.multiprocess
import android.os.Parcelable
-import androidx.datastore.core.twoWayIpc.CompositeServiceSubjectModel
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+import androidx.datastore.testapp.twoWayIpc.CompositeServiceSubjectModel
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import com.google.common.truth.Truth.assertThat
import kotlinx.parcelize.Parcelize
import org.junit.Rule
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoOkioSerializer.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/ProtoOkioSerializer.kt
similarity index 95%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoOkioSerializer.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/ProtoOkioSerializer.kt
index cb319f1..6a291b8 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoOkioSerializer.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/ProtoOkioSerializer.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.datastore.core
+package androidx.datastore.testapp
+import androidx.datastore.core.CorruptionException
import androidx.datastore.core.okio.OkioSerializer
import com.google.protobuf.ExtensionRegistryLite
import com.google.protobuf.InvalidProtocolBufferException
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoSerializer.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/ProtoSerializer.kt
similarity index 92%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoSerializer.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/ProtoSerializer.kt
index 587f24f..f9eb2e3 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/ProtoSerializer.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/ProtoSerializer.kt
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-package androidx.datastore.core
+package androidx.datastore.testapp
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
import com.google.protobuf.ExtensionRegistryLite
import com.google.protobuf.InvalidProtocolBufferException
import com.google.protobuf.MessageLite
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/CreateDatastoreAction.kt
similarity index 87%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/CreateDatastoreAction.kt
index 4890be9..cb3ef18 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/CreateDatastoreAction.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/CreateDatastoreAction.kt
@@ -14,24 +14,28 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess.ipcActions
+// Parcelize object is testing internal implementation of datastore-core library
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+package androidx.datastore.testapp.multiprocess.ipcActions
+
+import android.annotation.SuppressLint
import android.os.Parcelable
import androidx.datastore.core.CorruptionHandler
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreImpl
import androidx.datastore.core.FileStorage
import androidx.datastore.core.MultiProcessCoordinator
-import androidx.datastore.core.ProtoOkioSerializer
-import androidx.datastore.core.ProtoSerializer
import androidx.datastore.core.Serializer
import androidx.datastore.core.handlers.NoOpCorruptionHandler
import androidx.datastore.core.okio.OkioSerializer
import androidx.datastore.core.okio.OkioStorage
-import androidx.datastore.core.twoWayIpc.CompositeServiceSubjectModel
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.SubjectReadWriteProperty
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+import androidx.datastore.testapp.ProtoOkioSerializer
+import androidx.datastore.testapp.ProtoSerializer
+import androidx.datastore.testapp.twoWayIpc.CompositeServiceSubjectModel
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.SubjectReadWriteProperty
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import androidx.datastore.testing.TestMessageProto.FooProto
import com.google.protobuf.ExtensionRegistryLite
import java.io.File
@@ -117,6 +121,7 @@
)
}
+@SuppressLint("BanParcelableUsage")
@Parcelize
private class CreateDatastoreAction(
private val filePath: String,
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/ReadTextAction.kt
similarity index 67%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/ReadTextAction.kt
index eafa6b5..0162238 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/ReadTextAction.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/ReadTextAction.kt
@@ -14,17 +14,23 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess.ipcActions
+// Parcelize object is testing internal implementation of datastore-core library
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+package androidx.datastore.testapp.multiprocess.ipcActions
+
+import android.annotation.SuppressLint
import android.os.Parcelable
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import kotlinx.coroutines.flow.first
import kotlinx.parcelize.Parcelize
@Parcelize
internal class ReadTextAction : IpcAction<ReadTextAction.TextValue>() {
- @Parcelize data class TextValue(val value: String) : Parcelable
+ @SuppressLint("BanParcelableUsage")
+ @Parcelize
+ data class TextValue(val value: String) : Parcelable
override suspend fun invokeInRemoteProcess(subject: TwoWayIpcSubject): TextValue {
return TextValue(subject.datastore.data.first().text)
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/SetTextAction.kt
similarity index 73%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/SetTextAction.kt
index ce98deb..929b23f 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/multiprocess/ipcActions/SetTextAction.kt
@@ -14,15 +14,20 @@
* limitations under the License.
*/
-package androidx.datastore.core.multiprocess.ipcActions
+// Parcelize object is testing internal implementation of datastore-core library
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+package androidx.datastore.testapp.multiprocess.ipcActions
+
+import android.annotation.SuppressLint
import android.os.Parcelable
-import androidx.datastore.core.twoWayIpc.InterProcessCompletable
-import androidx.datastore.core.twoWayIpc.IpcAction
-import androidx.datastore.core.twoWayIpc.IpcUnit
-import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
+import androidx.datastore.testapp.twoWayIpc.InterProcessCompletable
+import androidx.datastore.testapp.twoWayIpc.IpcAction
+import androidx.datastore.testapp.twoWayIpc.IpcUnit
+import androidx.datastore.testapp.twoWayIpc.TwoWayIpcSubject
import kotlinx.parcelize.Parcelize
+@SuppressLint("BanParcelableUsage")
@Parcelize
internal class SetTextAction(
private val value: String,
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/CompositeServiceSubjectModel.kt
similarity index 94%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/CompositeServiceSubjectModel.kt
index 3b8dd64..741bb88 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/CompositeServiceSubjectModel.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/CompositeServiceSubjectModel.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
+//noinspection BanConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
/**
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/InterProcessCompletable.kt
similarity index 96%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/InterProcessCompletable.kt
index 46eaffa..4e5d506 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/InterProcessCompletable.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/InterProcessCompletable.kt
@@ -14,14 +14,16 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
+import android.annotation.SuppressLint
import android.os.Parcelable
import java.util.UUID
import kotlinx.coroutines.CompletableDeferred
import kotlinx.parcelize.Parcelize
/** A [Parcelable] [CompletableDeferred] implementation that can be shared across processes. */
+@SuppressLint("BanParcelableUsage")
@Parcelize
internal class InterProcessCompletable<T : Parcelable>(
private val key: String = UUID.randomUUID().toString(),
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcAction.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/IpcAction.kt
similarity index 85%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcAction.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/IpcAction.kt
index fa2de84..086510f 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcAction.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/IpcAction.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
+import android.annotation.SuppressLint
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -25,4 +26,4 @@
}
/** Utility object for [IpcAction]s that do not return a value. */
-@Parcelize object IpcUnit : Parcelable
+@SuppressLint("BanParcelableUsage") @Parcelize object IpcUnit : Parcelable
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcLogger.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/IpcLogger.kt
similarity index 96%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcLogger.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/IpcLogger.kt
index 628ff8b..278f16d 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/IpcLogger.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/IpcLogger.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
import android.app.Application
import android.os.Build
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcBus.kt
similarity index 97%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcBus.kt
index 4768068..3e84a15 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcBus.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcBus.kt
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.os.Messenger
-import androidx.datastore.core.twoWayIpc.IpcLogger.log
+import androidx.datastore.testapp.twoWayIpc.IpcLogger.log
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CompletableDeferred
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcConnection.kt
similarity index 98%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcConnection.kt
index e153d5d..d37575e 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcConnection.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcConnection.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
import android.content.ComponentName
import android.content.Context
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcService.kt
similarity index 95%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcService.kt
index 9834950..2fb7ec2 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcService.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcService.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
import android.content.Intent
import android.os.Handler
@@ -44,7 +44,7 @@
* It properly scopes those subjects and destroys their scopes when the Service is destroyed,
* allowing tests to properly maintain resources.
*
- * @see androidx.datastore.core.multiprocess.MultiProcessTestRule
+ * @see androidx.datastore.testapp.multiprocess.MultiProcessTestRule
*/
open class TwoWayIpcService : LifecycleService() {
private val subjects = mutableListOf<TwoWayIpcSubject>()
@@ -89,6 +89,7 @@
)
override fun onBind(intent: Intent): IBinder? {
+ super.onBind(intent)
return messenger.binder
}
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcSubject.kt
similarity index 98%
rename from datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt
rename to datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcSubject.kt
index 8c6cd3f..c6689a3 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/twoWayIpc/TwoWayIpcSubject.kt
+++ b/datastore/integration-tests/testapp/src/main/java/androidx/datastore/testapp/twoWayIpc/TwoWayIpcSubject.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.datastore.core.twoWayIpc
+package androidx.datastore.testapp.twoWayIpc
import android.os.Bundle
import android.os.Parcelable
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/proto/test.proto b/datastore/integration-tests/testapp/src/main/proto/test.proto
similarity index 100%
rename from datastore/datastore-core/src/androidInstrumentedTest/proto/test.proto
rename to datastore/integration-tests/testapp/src/main/proto/test.proto
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 619c5f9..d44adf5 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -48,8 +48,8 @@
[0-9]+ problem.* found storing the configuration cache.*
See the complete report at file://.*/build/reports/configuration\-cache/[^ ]*/[^ ]*/configuration\-cache\-report\.html
# > Task :compose:ui:ui:processDebugAndroidTestManifest
-\$OUT_DIR/androidx/compose/runtime/runtime\-saveable/build/intermediates/tmp/manifest/androidTest/debug/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/compose/ui/ui\-tooling/build/intermediates/tmp/manifest/androidTest/debug/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+\$OUT_DIR/androidx/compose/runtime/runtime\-saveable/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
+\$OUT_DIR/androidx/compose/ui/ui\-tooling/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :buildSrc:build UP-TO-DATE
A fine\-grained performance profile is available\: use the \-\-scan option\.
# > Task :docs
@@ -333,3 +333,5 @@
WARN: Attempt to load key 'java\.correct\.class\.type\.by\.place\.resolve\.scope' for not yet loaded registry
warning: unable to find kotlin\-stdlib\.jar in the Kotlin home directory\. Pass either '\-no\-stdlib' to prevent adding it to the classpath, or the correct '\-kotlin\-home'
warning: unable to find kotlin\-script\-runtime\.jar in the Kotlin home directory\. Pass either '\-no\-stdlib' to prevent adding it to the classpath, or the correct '\-kotlin\-home'
+# develocity plugin begign warning, reported back to Gradle
+Failed sysctl call: hw\.nperflevels, Error code: 2
diff --git a/development/validateRefactor.sh b/development/validateRefactor.sh
index 7170a98..1fb6cd2 100755
--- a/development/validateRefactor.sh
+++ b/development/validateRefactor.sh
@@ -32,13 +32,21 @@
Validates that libraries built from the given versions are the same as
the build outputs built at HEAD. This can be used to validate that a refactor
- did not change the outputs. If a git treeish is given with no path, the path is considered to be frameworks/support
+ did not change the outputs.
+ If a git treeish is given with no path, the path is considered to be frameworks/support
Example: $0 HEAD^
Example: $0 prebuilts/androidx/external:HEAD^ frameworks/support:work^
- * A git treeish is what you type when you run 'git checkout <git treeish>'
+ * A git treeish is what you type when you run 'git checkout <git treeish>'
See also https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddeftree-ishatree-ishalsotreeish .
+
+ You can also supply additional arguments that will be passed through to validateRefactorHelper.py, using -P
+ For example, the baseline arguments that validateRefactorHelper.py accepts.
+ Example: $0 HEAD^ -p agpKmp
+
+ validateRefactor also accepts git treeishes as named arguments using -g
+ Example: $0 -g HEAD^ -p agpKmp
"
return 1
}
@@ -118,27 +126,43 @@
unzipInPlace "${tempOutPath}/dist/docs-public-0.zip"
}
-oldCommits="$(expandCommitArgs $@)"
-projectPaths="$(getParticipatingProjectPaths $oldCommits)"
-if echo $projectPaths | grep external/dokka >/dev/null; then
- if [ "$BUILD_DOKKA" == "" ]; then
- echo "It doesn't make sense to include the external/dokka project without also setting BUILD_DOKKA=true. Did you mean to set BUILD_DOKKA=true?"
- exit 1
+nonNamedArgs=()
+oldCommits=()
+passThruArgs=()
+while [ $OPTIND -le "$#" ]; do
+ if getopts ":p:g:" opt; then
+ case $opt in
+ \? ) usage;;
+ g ) oldCommits+="$(expandCommitArgs $OPTARG)";;
+ p ) passThruArgs+="$OPTARG";;
+ esac
+ case $OPTARG in
+ -*) usage;;
+ esac
+ else
+ nonNamedArgs+=("${!OPTIND}")
+ ((OPTIND++))
fi
-fi
-echo old commits: $oldCommits
+done
+
+oldCommits+="$(expandCommitArgs $nonNamedArgs)"
+
+projectPaths="$(getParticipatingProjectPaths $oldCommits)"
if [ "$oldCommits" == "" ]; then
usage
fi
+
newCommits="$(getCurrentCommits $projectPaths)"
cd "$supportRoot"
+if [[ $(git update-index --refresh) ]]; then echo "You have local changes; stash or commit them or this script won't work"; exit 1; fi
+if [[ $(git diff-index --quiet HEAD) ]]; then echo "You have local changes; stash or commit them or this script won't work"; exit 1; fi
+echo old commits: $oldCommits
echo new commits: $newCommits
-
+cd "$supportRoot"
oldOutPath="${checkoutRoot}/out-old"
newOutPath="${checkoutRoot}/out-new"
tempOutPath="${checkoutRoot}/out"
-
rm -rf "$oldOutPath" "$newOutPath" "$tempOutPath"
echo building new commit
@@ -158,10 +182,9 @@
uncheckout "$projectPaths"
mv "$tempOutPath" "$oldOutPath"
+
echo
echo diffing results
-# Don't care about maven-metadata files because they have timestamps in them
-# We might care to know whether .sha1 or .md5 files have changed, but changes in those files will always be accompanied by more meaningful changes in other files, so we don't need to show changes in .sha1 or .md5 files
-# We also don't care about several specific files, either
-echoAndDo diff -r -x "*.md5*" -x "*.sha*" -x "*maven-metadata.xml" -x buildSrc.jar -x jetpad-integration.jar -x "top-of-tree-m2repository-all-0.zip" -x noto-emoji-compat-java.jar -x versionedparcelable-annotation.jar -x dokkaTipOfTreeDocs-0.zip "$oldOutPath/dist" "$newOutPath/dist"
+# This script performs the diff, and filters out known issues and non-issues with baselines
+python development/validateRefactorHelper.py "$passThruArgs"
echo end of difference
diff --git a/development/validateRefactorHelper.py b/development/validateRefactorHelper.py
new file mode 100644
index 0000000..dcdf3ae
--- /dev/null
+++ b/development/validateRefactorHelper.py
@@ -0,0 +1,197 @@
+#
+# Copyright (C) 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.
+#
+"""A helper script for validateRefactor.sh. Should generally not be used directly.
+
+Can be used directly if validateRefactor.sh has already created the out-old & out-new dirs.
+In such a case, it can be run to compare those directories without regenerating them.
+This is generally only useful when updating baselines or iterating on this script itself.
+Takes baseline names as CLI arguments, which may be passed through from validateRefactor.sh.
+
+Typical usage example:
+
+ python validateRefactorHelper.py agpKmp
+"""
+import itertools
+import os
+import shutil
+import subprocess
+import sys
+
+# noto-emoji-compat `bundleinside`s an externally-built with-timestamps jar.
+# classes.jar is compared using `diffuse` instead of unzipping and diffing class files.
+bannedJars = ["-x", "noto-emoji-compat-java.jar", "-x", "classes.jar"]
+# java and json aren"t for unzipping, but the poor exclude-everything-but-jars regex doesn't
+# exclude them. Same for exclude-non-klib and .kt/.knm
+areNotZips = ["-x", r"**\.java", "-x", r"**\.json", "-x", r"**\.kt", "-x", r"**\.knm"]
+# keeps making my regexes fall over :(
+hasNoExtension = ["-x", "manifest", "-x", "module"]
+doNotUnzip = bannedJars + areNotZips + hasNoExtension
+
+def diff(excludes):
+ return popenAndReturn(["diff", "-r", "../../out-old/dist/", "../../out-new/dist/"] + excludes)
+
+def popenAndReturn(args):
+ return subprocess.Popen(args, stdout=subprocess.PIPE).stdout.read().decode("utf-8").split("\n")
+
+# Finds and unzips all files with old/new diff that _do not_ match the argument regex.
+def findFilesMatchingWithDiffAndUnzip(regexThatMatchesEverythingElse):
+ # Exclude all things that are *not* the desired zip type
+ # (because diff doesn"t have an --include, only --exclude).
+ zipsWithDiffs = diff(["-q", "-x", regexThatMatchesEverythingElse] + doNotUnzip)
+ # Take only changed files, not new/deleted ones (the diff there is obvious)
+ zipsWithDiffs = filter(lambda s: s.startswith("Files"), zipsWithDiffs)
+ zipsWithDiffs = map(lambda s: s.split()[1:4:2], zipsWithDiffs)
+ zipsWithDiffs = list(itertools.chain.from_iterable(zipsWithDiffs)) # flatten
+ # And unzip them
+ for filename in zipsWithDiffs:
+ print("unzipping " + filename)
+ # if os.path.exists(filename+".unzipped/"): os.rmdir(filename+".unzipped/")
+ shutil.rmtree(filename+".unzipped/")
+ subprocess.Popen(["unzip", "-qq", "-o", filename, "-d", filename+".unzipped/"])
+
+diffusePath = "../../prebuilts/build-tools/diffuse-0.3.0/bin/diffuse"
+
+def compareWithDiffuse(listOfJars):
+ for jarPath in list(filter(None, listOfJars)):
+ print("jarpath: " + jarPath)
+ newJarPath = jarPath.replace("out-old", "out-new")
+ print(popenAndReturn([diffusePath, "diff", "--jar", jarPath, newJarPath]))
+
+# We might care to know whether .sha1 or .md5 files have changed, but changes in those files will
+# always be accompanied by more meaningful changes in other files, so we don"t need to show changes
+# in .sha1 or .md5 files, or in .module files showing the hashes of other files, or config names.
+excludedHashes = ["-x", "*.md5*", "-x", "*.sha**", "-I", " \"md5\".*", \
+ "-I", " \"sha.*", "-I", " \"size\".*", "-I", " \"name\".*"]
+# Don"t care about maven-metadata files because they have timestamps in them.
+excludedFiles = ["-x", "*maven-metadata.xml**", "-x", r"**\.knm"] # temporarily ignore knms
+# Also, ignore files that we already unzipped
+excludedZips = ["-x", "*.zip", "-x", "*.jar", "-x", "*.aar", "-x", "*.apk", "-x", "*.klib"]
+
+# These are baselined changes that we understand and know are no-ops in refactors
+# "Unskippable" changes are multi-line and can't be skipped in `diff`, so post-process
+baselinedChangesForAgpKmp = [
+ # these are new attributes being added
+ """ "org.gradle.libraryelements": "aar",""",
+ """ "org.gradle.jvm.environment": "android",""",
+ """ "org.gradle.jvm.environment": "non-jvm",""",
+ """ "org.gradle.jvm.environment": "standard-jvm",""",
+ # this attribute swap occurs alongside the above new attributes added.
+ # https://chat.google.com/room/AAAAW8qmCIs/4phaNn_gsrc
+ """ "org.jetbrains.kotlin.platform.type": "androidJvm\"""",
+ """ "org.jetbrains.kotlin.platform.type": "jvm\"""",
+ # name-only change; nothing resolves based on names
+ """ "name": "releaseApiElements-published",""",
+ """ "name": "androidApiElements-published",""",
+ """ <pre>actual typealias""", # open bug in dackka b/339221337
+ # we are switching from our KMP sourcejars solution to the upstream one
+ """ "org.gradle.docstype": "fake-sources",""",
+ """ "org.gradle.docstype": "sources",""",
+]
+unskippableBaselinedChangesForAgpKmp = [
+ """
+< },
+< "excludes": [
+< {
+< "group": "org.jetbrains.kotlin",
+< "module": "kotlin-stdlib-common"
+< },
+< {
+< "group": "org.jetbrains.kotlin",
+< "module": "kotlin-test-common"
+< },
+< {
+< "group": "org.jetbrains.kotlin",
+< "module": "kotlin-test-annotations-common"
+< }
+< ]
+---
+> }
+""",
+"""
+< <exclusions>
+< <exclusion>
+< <groupId>org.jetbrains.kotlin</groupId>
+< <artifactId>kotlin-stdlib-common</artifactId>
+< </exclusion>
+< <exclusion>
+< <groupId>org.jetbrains.kotlin</groupId>
+< <artifactId>kotlin-test-common</artifactId>
+< </exclusion>
+< <exclusion>
+< <groupId>org.jetbrains.kotlin</groupId>
+< <artifactId>kotlin-test-annotations-common</artifactId>
+< </exclusion>
+< </exclusions>
+"""
+]
+
+baselinedChanges = []
+unskippableBaselinedChanges = []
+arguments = sys.argv[1:]
+if "agpKmp" in arguments:
+ arguments.remove("agpKmp")
+ print("IGNORING DIFF FOR agpKmp")
+ baselinedChanges += baselinedChangesForAgpKmp
+ unskippableBaselinedChanges += unskippableBaselinedChangesForAgpKmp
+if arguments:
+ print("invalid argument(s) for validateRefactorHelper: " + ", ".join(arguments))
+ print("currently recognized arguments: agpKmp")
+ exit()
+
+# interleave "-I" to tell diffutils to 'I'gnore the baselined lines
+baselinedChanges = list(itertools.chain.from_iterable(zip(["-I"]*99, baselinedChanges)))
+
+# post-process the diff output to remove multi-line changes that can't be excluded in `diff` itself
+def filterOutUnskippableBaselinedChanges(inputString):
+ result = inputString
+ for toRemove in unskippableBaselinedChanges:
+ i = result.find(toRemove)
+ while (i != -1):
+ j = result.rfind("\n", 0, i-2) # also find and remove previous line e.g. 82,96c70
+ result = result[:j+1] + result[i+len(toRemove):]
+ i = result.find(toRemove)
+ #remove all "diff -r ..." header lines that no longer have content due to baselining
+ result = result.split("\n")
+ nRemoved = 0
+ for i in range(len(result)): # check for consecutive `diff -r` lines: the first has no content
+ if not result[i-nRemoved].startswith("diff -r "): continue
+ if not result[i+1-nRemoved].startswith("diff -r "): continue
+ del result[i]
+ nRemoved+=1
+ if not result[-1]: del result[-1] # remove possible ending blank line
+ if result[-1].startswith("diff -r "): del result[-1] # terminal `diff -r` line: has no content
+ return "\n".join(result)
+
+# print(baselinedChanges)
+
+# Find all zip files with a diff, e.g. the tip-of-tree-repository file, and maybe the docs zip
+# findFilesMatchingWithDiffAndUnzip(r"**\.[^z][a-z]*")
+# Find all aar and apk files with a diff. The proper regex would be `.*\..*[^akpr]+.*`, but it
+# doesn"t work in difftools exclude's very limited regex syntax.
+findFilesMatchingWithDiffAndUnzip(r"**\.[^a][a-z]*")
+# Find all jars and klibs and unzip them (comes after because they could be inside aars/apks).
+findFilesMatchingWithDiffAndUnzip(r"**\.[^j][a-z]*")
+findFilesMatchingWithDiffAndUnzip(r"**\.[^k][a-z]*")
+# now find all diffs in classes.jars
+classesJarsWithDiffs = popenAndReturn(["find", "../../out-old/dist/", "-name", "classes.jar"])
+print("classes.jar s: " + str(classesJarsWithDiffs))
+compareWithDiffuse(classesJarsWithDiffs)
+# Now find all diffs in non-zipped files
+finalExcludes = excludedHashes + excludedFiles + excludedZips + baselinedChanges
+finalDiff = "\n".join(diff(finalExcludes))
+finalDiff = filterOutUnskippableBaselinedChanges(finalDiff)
+print(finalDiff)
+
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 3c13a83..d1966329 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -22,8 +22,8 @@
docsWithoutApiSince("androidx.ads:ads-identifier:1.0.0-alpha05")
docsWithoutApiSince("androidx.ads:ads-identifier-common:1.0.0-alpha05")
docsWithoutApiSince("androidx.ads:ads-identifier-provider:1.0.0-alpha05")
- kmpDocs("androidx.annotation:annotation:1.8.2")
- docs("androidx.annotation:annotation-experimental:1.4.1")
+ kmpDocs("androidx.annotation:annotation:1.9.0-alpha02")
+ docs("androidx.annotation:annotation-experimental:1.5.0-alpha01")
docs("androidx.appcompat:appcompat:1.7.0")
docs("androidx.appcompat:appcompat-resources:1.7.0")
docs("androidx.appsearch:appsearch:1.1.0-alpha04")
@@ -38,10 +38,10 @@
docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
docs("androidx.autofill:autofill:1.3.0-alpha01")
- docs("androidx.benchmark:benchmark-common:1.3.0-rc01")
- docs("androidx.benchmark:benchmark-junit4:1.3.0-rc01")
- docs("androidx.benchmark:benchmark-macro:1.3.0-rc01")
- docs("androidx.benchmark:benchmark-macro-junit4:1.3.0-rc01")
+ docs("androidx.benchmark:benchmark-common:1.3.0")
+ docs("androidx.benchmark:benchmark-junit4:1.3.0")
+ docs("androidx.benchmark:benchmark-macro:1.3.0")
+ docs("androidx.benchmark:benchmark-macro-junit4:1.3.0")
docs("androidx.biometric:biometric:1.4.0-alpha02")
docs("androidx.biometric:biometric-ktx:1.4.0-alpha02")
docs("androidx.bluetooth:bluetooth:1.0.0-alpha02")
@@ -66,42 +66,42 @@
docs("androidx.cardview:cardview:1.0.0")
kmpDocs("androidx.collection:collection:1.4.3")
docs("androidx.collection:collection-ktx:1.4.3")
- kmpDocs("androidx.compose.animation:animation:1.7.0-beta06")
- kmpDocs("androidx.compose.animation:animation-core:1.7.0-beta06")
- kmpDocs("androidx.compose.animation:animation-graphics:1.7.0-beta06")
- kmpDocs("androidx.compose.foundation:foundation:1.7.0-beta07")
- kmpDocs("androidx.compose.foundation:foundation-layout:1.7.0-beta06")
- kmpDocs("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04")
- kmpDocs("androidx.compose.material3.adaptive:adaptive-layout:1.0.0-beta04")
- kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-beta04")
- kmpDocs("androidx.compose.material3:material3:1.3.0-beta05")
- kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0-beta05")
+ kmpDocs("androidx.compose.animation:animation:1.7.0-rc01")
+ kmpDocs("androidx.compose.animation:animation-core:1.7.0-rc01")
+ kmpDocs("androidx.compose.animation:animation-graphics:1.7.0-rc01")
+ kmpDocs("androidx.compose.foundation:foundation:1.7.0-rc01")
+ kmpDocs("androidx.compose.foundation:foundation-layout:1.7.0-rc01")
+ kmpDocs("androidx.compose.material3.adaptive:adaptive:1.1.0-alpha01")
+ kmpDocs("androidx.compose.material3.adaptive:adaptive-layout:1.1.0-alpha01")
+ kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-alpha01")
+ kmpDocs("androidx.compose.material3:material3:1.3.0-rc01")
+ kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0-rc01")
kmpDocs("androidx.compose.material3:material3-common:1.0.0-alpha01")
- kmpDocs("androidx.compose.material3:material3-window-size-class:1.3.0-beta05")
- kmpDocs("androidx.compose.material:material:1.7.0-beta06")
- kmpDocs("androidx.compose.material:material-icons-core:1.7.0-beta06")
+ kmpDocs("androidx.compose.material3:material3-window-size-class:1.3.0-rc01")
+ kmpDocs("androidx.compose.material:material:1.7.0-rc01")
+ kmpDocs("androidx.compose.material:material-icons-core:1.7.0-rc01")
docs("androidx.compose.material:material-navigation:1.7.0-beta03")
- kmpDocs("androidx.compose.material:material-ripple:1.7.0-beta06")
- kmpDocs("androidx.compose.runtime:runtime:1.7.0-beta07")
- docs("androidx.compose.runtime:runtime-livedata:1.7.0-beta06")
- docs("androidx.compose.runtime:runtime-rxjava2:1.7.0-beta06")
- docs("androidx.compose.runtime:runtime-rxjava3:1.7.0-beta06")
- kmpDocs("androidx.compose.runtime:runtime-saveable:1.7.0-beta06")
+ kmpDocs("androidx.compose.material:material-ripple:1.7.0-rc01")
+ kmpDocs("androidx.compose.runtime:runtime:1.7.0-rc01")
+ docs("androidx.compose.runtime:runtime-livedata:1.7.0-rc01")
+ docs("androidx.compose.runtime:runtime-rxjava2:1.7.0-rc01")
+ docs("androidx.compose.runtime:runtime-rxjava3:1.7.0-rc01")
+ kmpDocs("androidx.compose.runtime:runtime-saveable:1.7.0-rc01")
docs("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")
- kmpDocs("androidx.compose.ui:ui:1.7.0-beta06")
+ kmpDocs("androidx.compose.ui:ui:1.7.0-rc01")
docs("androidx.compose.ui:ui-android-stubs:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-geometry:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-graphics:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-test:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-test-junit4:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-text:1.7.0-beta06")
- docs("androidx.compose.ui:ui-text-google-fonts:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-tooling:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-tooling-data:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-tooling-preview:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-unit:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-util:1.7.0-beta06")
- docs("androidx.compose.ui:ui-viewbinding:1.7.0-beta06")
+ kmpDocs("androidx.compose.ui:ui-geometry:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-graphics:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-test:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-test-junit4:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-text:1.7.0-rc01")
+ docs("androidx.compose.ui:ui-text-google-fonts:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-tooling:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-tooling-data:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-tooling-preview:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-unit:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-util:1.7.0-rc01")
+ docs("androidx.compose.ui:ui-viewbinding:1.7.0-rc01")
docs("androidx.concurrent:concurrent-futures:1.2.0")
docs("androidx.concurrent:concurrent-futures-ktx:1.2.0")
docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha14")
@@ -109,13 +109,13 @@
docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha14")
docs("androidx.contentpager:contentpager:1.0.0")
docs("androidx.coordinatorlayout:coordinatorlayout:1.3.0-alpha02")
- docs("androidx.core:core:1.15.0-alpha01")
+ docs("androidx.core:core:1.15.0-alpha02")
// TODO(b/294531403): Turn on apiSince for core-animation when it releases as alpha
docsWithoutApiSince("androidx.core:core-animation:1.0.0-rc01")
docs("androidx.core:core-animation-testing:1.0.0")
docs("androidx.core:core-google-shortcuts:1.2.0-alpha01")
docs("androidx.core:core-i18n:1.0.0-alpha01")
- docs("androidx.core:core-ktx:1.15.0-alpha01")
+ docs("androidx.core:core-ktx:1.15.0-alpha02")
docs("androidx.core:core-location-altitude:1.0.0-alpha02")
docs("androidx.core:core-performance:1.0.0")
docs("androidx.core:core-performance-play-services:1.0.0")
@@ -125,7 +125,7 @@
docs("androidx.core:core-role:1.2.0-alpha01")
docs("androidx.core:core-splashscreen:1.2.0-alpha01")
docs("androidx.core:core-telecom:1.0.0-alpha03")
- docs("androidx.core:core-testing:1.15.0-alpha01")
+ docs("androidx.core:core-testing:1.15.0-alpha02")
docs("androidx.core.uwb:uwb:1.0.0-alpha08")
docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha08")
docs("androidx.credentials:credentials:1.5.0-alpha04")
@@ -150,11 +150,11 @@
docs("androidx.drawerlayout:drawerlayout:1.2.0")
docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
docs("androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03")
- docs("androidx.emoji2:emoji2:1.5.0-beta01")
- docs("androidx.emoji2:emoji2-bundled:1.5.0-beta01")
- docs("androidx.emoji2:emoji2-emojipicker:1.5.0-beta01")
- docs("androidx.emoji2:emoji2-views:1.5.0-beta01")
- docs("androidx.emoji2:emoji2-views-helper:1.5.0-beta01")
+ docs("androidx.emoji2:emoji2:1.5.0-rc01")
+ docs("androidx.emoji2:emoji2-bundled:1.5.0-rc01")
+ docs("androidx.emoji2:emoji2-emojipicker:1.5.0-rc01")
+ docs("androidx.emoji2:emoji2-views:1.5.0-rc01")
+ docs("androidx.emoji2:emoji2-views-helper:1.5.0-rc01")
docs("androidx.emoji:emoji:1.2.0-alpha03")
docs("androidx.emoji:emoji-appcompat:1.2.0-alpha03")
docs("androidx.emoji:emoji-bundled:1.2.0-alpha03")
@@ -177,7 +177,7 @@
docs("androidx.glance:glance-wear-tiles:1.0.0-alpha06")
docs("androidx.graphics:graphics-core:1.0.0")
docs("androidx.graphics:graphics-path:1.0.0")
- kmpDocs("androidx.graphics:graphics-shapes:1.0.0-rc01")
+ kmpDocs("androidx.graphics:graphics-shapes:1.0.0")
docs("androidx.gridlayout:gridlayout:1.1.0-beta01")
docs("androidx.health.connect:connect-client:1.1.0-alpha07")
samples("androidx.health.connect:connect-client-samples:1.1.0-alpha07")
@@ -223,47 +223,47 @@
docs("androidx.media2:media2-widget:1.3.0")
docs("androidx.media:media:1.7.0")
// androidx.media3 is not hosted in androidx
- docsWithoutApiSince("androidx.media3:media3-cast:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-common:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-container:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-database:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-datasource:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-decoder:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-effect:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-extractor:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-muxer:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-session:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-test-utils:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-transformer:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-ui:1.4.0")
- docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.4.0")
+ docsWithoutApiSince("androidx.media3:media3-cast:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-common:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-container:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-database:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-datasource:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-decoder:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-effect:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-extractor:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-muxer:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-session:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-test-utils:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-transformer:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-ui:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.4.1")
docs("androidx.mediarouter:mediarouter:1.7.0")
docs("androidx.mediarouter:mediarouter-testing:1.7.0")
docs("androidx.metrics:metrics-performance:1.0.0-beta01")
- docs("androidx.navigation:navigation-common:2.8.0-beta07")
- docs("androidx.navigation:navigation-common-ktx:2.8.0-beta07")
- docs("androidx.navigation:navigation-compose:2.8.0-beta07")
- docs("androidx.navigation:navigation-dynamic-features-fragment:2.8.0-beta07")
- docs("androidx.navigation:navigation-dynamic-features-runtime:2.8.0-beta07")
- docs("androidx.navigation:navigation-fragment:2.8.0-beta07")
- docs("androidx.navigation:navigation-fragment-compose:2.8.0-beta07")
- docs("androidx.navigation:navigation-fragment-ktx:2.8.0-beta07")
- docs("androidx.navigation:navigation-runtime:2.8.0-beta07")
- docs("androidx.navigation:navigation-runtime-ktx:2.8.0-beta07")
- docs("androidx.navigation:navigation-testing:2.8.0-beta07")
- docs("androidx.navigation:navigation-ui:2.8.0-beta07")
- docs("androidx.navigation:navigation-ui-ktx:2.8.0-beta07")
+ docs("androidx.navigation:navigation-common:2.8.0-rc01")
+ docs("androidx.navigation:navigation-common-ktx:2.8.0-rc01")
+ docs("androidx.navigation:navigation-compose:2.8.0-rc01")
+ docs("androidx.navigation:navigation-dynamic-features-fragment:2.8.0-rc01")
+ docs("androidx.navigation:navigation-dynamic-features-runtime:2.8.0-rc01")
+ docs("androidx.navigation:navigation-fragment:2.8.0-rc01")
+ docs("androidx.navigation:navigation-fragment-compose:2.8.0-rc01")
+ docs("androidx.navigation:navigation-fragment-ktx:2.8.0-rc01")
+ docs("androidx.navigation:navigation-runtime:2.8.0-rc01")
+ docs("androidx.navigation:navigation-runtime-ktx:2.8.0-rc01")
+ docs("androidx.navigation:navigation-testing:2.8.0-rc01")
+ docs("androidx.navigation:navigation-ui:2.8.0-rc01")
+ docs("androidx.navigation:navigation-ui-ktx:2.8.0-rc01")
kmpDocs("androidx.paging:paging-common:3.3.1")
docs("androidx.paging:paging-common-ktx:3.3.1")
kmpDocs("androidx.paging:paging-compose:3.3.1")
@@ -285,8 +285,8 @@
docs("androidx.privacysandbox.activity:activity-client:1.0.0-alpha01")
docs("androidx.privacysandbox.activity:activity-core:1.0.0-alpha01")
docs("androidx.privacysandbox.activity:activity-provider:1.0.0-alpha01")
- docs("androidx.privacysandbox.ads:ads-adservices:1.1.0-beta09")
- docs("androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta09")
+ docs("androidx.privacysandbox.ads:ads-adservices:1.1.0-beta10")
+ docs("androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta10")
docs("androidx.privacysandbox.sdkruntime:sdkruntime-client:1.0.0-alpha14")
docs("androidx.privacysandbox.sdkruntime:sdkruntime-core:1.0.0-alpha14")
docs("androidx.privacysandbox.sdkruntime:sdkruntime-provider:1.0.0-alpha14")
@@ -294,25 +294,25 @@
docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha09")
docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha09")
docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha09")
- docs("androidx.profileinstaller:profileinstaller:1.4.0-alpha02")
+ docs("androidx.profileinstaller:profileinstaller:1.4.0-beta01")
docs("androidx.recommendation:recommendation:1.0.0")
- docs("androidx.recyclerview:recyclerview:1.4.0-alpha02")
+ docs("androidx.recyclerview:recyclerview:1.4.0-beta01")
docs("androidx.recyclerview:recyclerview-selection:2.0.0-alpha01")
docs("androidx.remotecallback:remotecallback:1.0.0-alpha02")
docs("androidx.resourceinspection:resourceinspection-annotation:1.0.1")
- kmpDocs("androidx.room:room-common:2.7.0-alpha06")
+ kmpDocs("androidx.room:room-common:2.7.0-alpha07")
docs("androidx.room:room-external-antlr:2.7.0-alpha06")
- docs("androidx.room:room-guava:2.7.0-alpha06")
- docs("androidx.room:room-ktx:2.7.0-alpha06")
- kmpDocs("androidx.room:room-migration:2.7.0-alpha06")
- kmpDocs("androidx.room:room-paging:2.7.0-alpha06")
- docs("androidx.room:room-paging-guava:2.7.0-alpha06")
- docs("androidx.room:room-paging-rxjava2:2.7.0-alpha06")
- docs("androidx.room:room-paging-rxjava3:2.7.0-alpha06")
- kmpDocs("androidx.room:room-runtime:2.7.0-alpha06")
- docs("androidx.room:room-rxjava2:2.7.0-alpha06")
- docs("androidx.room:room-rxjava3:2.7.0-alpha06")
- kmpDocs("androidx.room:room-testing:2.7.0-alpha06")
+ docs("androidx.room:room-guava:2.7.0-alpha07")
+ docs("androidx.room:room-ktx:2.7.0-alpha07")
+ kmpDocs("androidx.room:room-migration:2.7.0-alpha07")
+ kmpDocs("androidx.room:room-paging:2.7.0-alpha07")
+ docs("androidx.room:room-paging-guava:2.7.0-alpha07")
+ docs("androidx.room:room-paging-rxjava2:2.7.0-alpha07")
+ docs("androidx.room:room-paging-rxjava3:2.7.0-alpha07")
+ kmpDocs("androidx.room:room-runtime:2.7.0-alpha07")
+ docs("androidx.room:room-rxjava2:2.7.0-alpha07")
+ docs("androidx.room:room-rxjava3:2.7.0-alpha07")
+ kmpDocs("androidx.room:room-testing:2.7.0-alpha07")
docs("androidx.savedstate:savedstate:1.3.0-alpha01")
docs("androidx.savedstate:savedstate-ktx:1.3.0-alpha01")
docs("androidx.security:security-app-authenticator:1.0.0-beta01")
@@ -327,11 +327,11 @@
docs("androidx.slice:slice-core:1.1.0-alpha02")
docs("androidx.slice:slice-view:1.1.0-alpha02")
docs("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
- kmpDocs("androidx.sqlite:sqlite:2.5.0-alpha06")
- kmpDocs("androidx.sqlite:sqlite-bundled:2.5.0-alpha06")
- kmpDocs("androidx.sqlite:sqlite-framework:2.5.0-alpha06")
- docs("androidx.sqlite:sqlite-ktx:2.5.0-alpha06")
- docs("androidx.startup:startup-runtime:1.2.0-alpha02")
+ kmpDocs("androidx.sqlite:sqlite:2.5.0-alpha07")
+ kmpDocs("androidx.sqlite:sqlite-bundled:2.5.0-alpha07")
+ kmpDocs("androidx.sqlite:sqlite-framework:2.5.0-alpha07")
+ docs("androidx.sqlite:sqlite-ktx:2.5.0-alpha07")
+ docs("androidx.startup:startup-runtime:1.2.0-beta01")
docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
// androidx.test is not hosted in androidx
docsWithoutApiSince("androidx.test:core:1.6.1")
@@ -363,7 +363,7 @@
docs("androidx.transition:transition:1.5.1")
docs("androidx.transition:transition-ktx:1.5.1")
docs("androidx.tv:tv-foundation:1.0.0-alpha11")
- docs("androidx.tv:tv-material:1.0.0-rc02")
+ docs("androidx.tv:tv-material:1.0.0")
docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
docs("androidx.vectordrawable:vectordrawable:1.2.0")
docs("androidx.vectordrawable:vectordrawable-animated:1.2.0")
@@ -371,12 +371,12 @@
docs("androidx.versionedparcelable:versionedparcelable:1.2.0")
docs("androidx.viewpager2:viewpager2:1.1.0")
docs("androidx.viewpager:viewpager:1.1.0-alpha01")
- docs("androidx.wear.compose:compose-foundation:1.4.0-beta03")
- docs("androidx.wear.compose:compose-material:1.4.0-beta03")
- docs("androidx.wear.compose:compose-material-core:1.4.0-beta03")
+ docs("androidx.wear.compose:compose-foundation:1.4.0-rc01")
+ docs("androidx.wear.compose:compose-material:1.4.0-rc01")
+ docs("androidx.wear.compose:compose-material-core:1.4.0-rc01")
docs("androidx.wear.compose:compose-material3:1.0.0-alpha23")
- docs("androidx.wear.compose:compose-navigation:1.4.0-beta03")
- docs("androidx.wear.compose:compose-ui-tooling:1.4.0-beta03")
+ docs("androidx.wear.compose:compose-navigation:1.4.0-rc01")
+ docs("androidx.wear.compose:compose-ui-tooling:1.4.0-rc01")
docs("androidx.wear.protolayout:protolayout:1.2.0")
docs("androidx.wear.protolayout:protolayout-expression:1.2.0")
docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.2.0")
@@ -415,7 +415,7 @@
docs("androidx.wear:wear-remote-interactions:1.1.0-beta01")
samples("androidx.wear:wear-remote-interactions-samples:1.1.0-alpha02")
docs("androidx.wear:wear-tooling-preview:1.0.0")
- docs("androidx.webkit:webkit:1.12.0-alpha02")
+ docs("androidx.webkit:webkit:1.12.0-beta01")
docs("androidx.window.extensions.core:core:1.0.0")
docs("androidx.window:window:1.4.0-alpha01")
stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index e73a6ca..b4d4e4f 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -52,6 +52,8 @@
docs(project(":bluetooth:bluetooth-testing"))
docs(project(":browser:browser"))
docs(project(":camera:camera-camera2"))
+ docs(project(":camera:camera-compose"))
+ samples(project(":camera:camera-compose:camera-compose-samples"))
docs(project(":camera:camera-core"))
docs(project(":camera:camera-effects"))
docs(project(":camera:camera-effects-still-portrait"))
@@ -84,6 +86,7 @@
kmpDocs(project(":compose:material3:adaptive:adaptive"))
kmpDocs(project(":compose:material3:adaptive:adaptive-layout"))
kmpDocs(project(":compose:material3:adaptive:adaptive-navigation"))
+ kmpDocs(project(":compose:material3:adaptive:adaptive-render-strategy"))
kmpDocs(project(":compose:material3:material3"))
kmpDocs(project(":compose:material3:material3-adaptive-navigation-suite"))
kmpDocs(project(":compose:material3:material3-common"))
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
index 4d42ff9..a35d75c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_category_recent" msgid="7142376595414250279">"المستخدمة حديثًا"</string>
- <string name="emoji_category_emotions" msgid="1570830970240985537">"الوجوه المبتسمة والرموز التعبيرية"</string>
+ <string name="emoji_category_emotions" msgid="1570830970240985537">"الوجوه المبتسمة ورموز الإيموجي"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"الأشخاص"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"الحيوانات والطبيعة"</string>
<string name="emoji_category_food_drink" msgid="1189971856721244395">"المأكولات والمشروبات"</string>
@@ -27,11 +27,11 @@
<string name="emoji_category_objects" msgid="6106115586332708067">"عناصر متنوعة"</string>
<string name="emoji_category_symbols" msgid="5626171724310261787">"الرموز"</string>
<string name="emoji_category_flags" msgid="6185639503532784871">"الأعلام"</string>
- <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز تعبيرية."</string>
- <string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز تعبيرية حتى الآن."</string>
- <string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"مفتاح ثنائي الاتجاه للرموز التعبيرية"</string>
+ <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز إيموجي."</string>
+ <string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز إيموجي حتى الآن."</string>
+ <string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"مفتاح ثنائي الاتجاه لرموز الإيموجي"</string>
<string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"تم تغيير اتجاه الإيموجي"</string>
- <string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"أداة اختيار الرموز التعبيرية"</string>
+ <string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"أداة اختيار رموز الإيموجي"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s و%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"الظل"</string>
<string name="emoji_skin_tone_light_content_desc" msgid="1052239040923092881">"بشرة فاتحة"</string>
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index a625a7b..821aaee 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -391,7 +391,7 @@
// https://issuetracker.google.com/342697059
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = 22)
+ @SdkSuppress(minSdkVersion = 22) // Parsing the large image causes OOM on API 21 FTL emulators.
public void testWebpWithoutExifHeight8192px() throws Throwable {
File imageFile =
copyFromResourceToFile(
diff --git a/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt b/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt
index ac229eb..a1369c7 100644
--- a/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt
+++ b/fragment/fragment-compose/src/androidTest/java/androidx/fragment/compose/AndroidFragmentTest.kt
@@ -33,12 +33,14 @@
import androidx.fragment.app.Fragment
import androidx.fragment.compose.test.EmptyTestActivity
import androidx.fragment.compose.test.R
+import androidx.lifecycle.Lifecycle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -98,6 +100,68 @@
}
@Test
+ fun addAfterStateSaved() {
+ lateinit var number: MutableState<Int>
+ testRule.setContent {
+ number = remember { mutableStateOf(0) }
+ if (number.value > 0) {
+ AndroidFragment<FragmentForCompose>()
+ }
+ }
+
+ testRule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
+
+ testRule.runOnIdle { number.value = 1 }
+
+ testRule.waitForIdle()
+
+ testRule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
+
+ onView(withText("Show me on Screen")).check(matches(isDisplayed()))
+
+ testRule.runOnIdle { number.value = 0 }
+
+ testRule.waitForIdle()
+
+ // Validate that the fragment was removed
+ val fragment =
+ testRule.activity.supportFragmentManager.fragments.firstOrNull {
+ it is FragmentForCompose
+ }
+ assertThat(fragment).isNull()
+ }
+
+ @Test
+ fun addAndRemoveAfterStateSaved() {
+ lateinit var number: MutableState<Int>
+ testRule.setContent {
+ number = remember { mutableStateOf(0) }
+ if (number.value > 0) {
+ AndroidFragment<FragmentForCompose>()
+ }
+ }
+
+ testRule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
+
+ testRule.runOnIdle { number.value = 1 }
+
+ testRule.waitForIdle()
+
+ testRule.runOnIdle { number.value = 0 }
+
+ testRule.waitForIdle()
+
+ testRule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
+
+ // Validate that the fragment was removed
+ val fragment =
+ testRule.activity.supportFragmentManager.fragments.firstOrNull {
+ it is FragmentForCompose
+ }
+ assertThat(fragment).isNull()
+ }
+
+ @Test
fun recomposeInsideKey() {
lateinit var number: MutableState<Int>
diff --git a/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt b/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt
index 5d9884b..7d455e9 100644
--- a/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt
+++ b/fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt
@@ -30,6 +30,8 @@
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commitNow
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
/**
* Allows for adding a [Fragment] directly into Compose. It creates a fragment of the given class
@@ -93,6 +95,7 @@
)
DisposableEffect(fragmentManager, clazz, fragmentState) {
+ var removeEvenIfStateIsSaved = false
val fragment =
fragmentManager.findFragmentById(container.id)
?: fragmentManager.fragmentFactory
@@ -100,18 +103,42 @@
.apply {
setInitialSavedState(fragmentState.state.value)
setArguments(arguments)
- fragmentManager
- .beginTransaction()
- .setReorderingAllowed(true)
- .add(container, this, "$hashKey")
- .commitNow()
+ val transaction =
+ fragmentManager
+ .beginTransaction()
+ .setReorderingAllowed(true)
+ .add(container, this, "$hashKey")
+ if (fragmentManager.isStateSaved) {
+ // If the state is saved when we add the fragment,
+ // we want to remove the Fragment in onDispose
+ // if isStateSaved never becomes true for the lifetime
+ // of this AndroidFragment - we use a LifecycleObserver
+ // on the Fragment as a proxy for that signal
+ removeEvenIfStateIsSaved = true
+ lifecycle.addObserver(
+ object : DefaultLifecycleObserver {
+ override fun onStart(owner: LifecycleOwner) {
+ removeEvenIfStateIsSaved = false
+ lifecycle.removeObserver(this)
+ }
+ }
+ )
+ transaction.commitNowAllowingStateLoss()
+ } else {
+ transaction.commitNow()
+ }
}
fragmentManager.onContainerAvailable(container)
@Suppress("UNCHECKED_CAST") updateCallback.value(fragment as T)
onDispose {
val state = fragmentManager.saveFragmentInstanceState(fragment)
fragmentState.state.value = state
- if (!fragmentManager.isStateSaved) {
+ if (removeEvenIfStateIsSaved) {
+ // The Fragment was added when the state was saved and
+ // isStateSaved never became true for the lifetime of this
+ // AndroidFragment, so we unconditionally remove it here
+ fragmentManager.commitNow(allowStateLoss = true) { remove(fragment) }
+ } else if (!fragmentManager.isStateSaved) {
// If the state isn't saved, that means that some state change
// has removed this Composable from the hierarchy
fragmentManager.commitNow { remove(fragment) }
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PredictiveBackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PredictiveBackTest.kt
index f9ef07b..340f50b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PredictiveBackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PredictiveBackTest.kt
@@ -114,4 +114,30 @@
assertThat(fm.backStackEntryCount).isEqualTo(0)
}
}
+
+ @Test
+ fun backOnNoRecordTest() {
+ withUse(ActivityScenario.launch(SimpleContainerActivity::class.java)) {
+ val fm = withActivity { supportFragmentManager }
+
+ val fragment1 = StrictViewFragment()
+
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment1, "1")
+ .setReorderingAllowed(true)
+ .commit()
+ executePendingTransactions()
+
+ val dispatcher = withActivity { onBackPressedDispatcher }
+
+ // We need a pending commit that doesn't include a fragment to mimic calling
+ // system back while commit is pending.
+ fm.beginTransaction().commit()
+
+ dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+ withActivity { dispatcher.onBackPressed() }
+
+ assertThat(fm.backStackEntryCount).isEqualTo(0)
+ }
+ }
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index e3d35ae..15ce216 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -2595,6 +2595,16 @@
boolean prepareBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
+ if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+ Log.v(
+ TAG, "FragmentManager has the following pending actions inside of "
+ + "prepareBackStackState: " + mPendingActions
+ );
+ }
+ if (mBackStack.isEmpty()) {
+ Log.i(TAG, "Ignoring call to start back stack pop because the back stack is empty.");
+ return false;
+ }
// The transitioning record is the last one on the back stack.
mTransitioningOp = mBackStack.get(mBackStack.size() - 1);
// Mark all fragments in the record as transitioning
diff --git a/glance/glance-appwidget/src/main/res/values-fa/strings.xml b/glance/glance-appwidget/src/main/res/values-fa/strings.xml
index 5dd49dc..313a5b7 100644
--- a/glance/glance-appwidget/src/main/res/values-fa/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-fa/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="glance_error_layout_title" msgid="3631961919234443531">""<b>"خطا در ابزارک برنامه Glance"</b></string>
+ <string name="glance_error_layout_title" msgid="3631961919234443531">""<b>"خطا در ابزاره برنامه Glance"</b></string>
<string name="glance_error_layout_text" msgid="2863935784364843033">"بااستفاده از "<b><tt>"adb logcat"</tt></b>" و جستجوی "<b><tt>"GlanceAppWidget"</tt></b>"، خطا را بهطور دقیق بررسی کنید"</string>
<string name="glance_error_layout_text_v2" msgid="5191168365305634625">"محتوا نشان داده نشد"</string>
</resources>
diff --git a/gradle.properties b/gradle.properties
index 4cfe031..ebd54e7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -44,6 +44,7 @@
android.suppressUnsupportedCompileSdk=35
androidx.compileSdk=34
+androidx.latestStableCompileSdk=35
androidx.targetSdkVersion=34
androidx.allowCustomCompileSdk=true
androidx.includeOptionalProjects=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3571b46..5165817 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -57,7 +57,7 @@
ktfmt = "0.50"
leakcanary = "2.13"
media3 = "1.1.0"
-metalava = "1.0.0-alpha11"
+metalava = "1.0.0-alpha12"
mockito = "2.25.0"
moshi = "1.13.0"
node = "16.20.2"
@@ -143,7 +143,7 @@
espressoIntents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso" }
espressoRemote = { module = "androidx.test.espresso:espresso-remote", version.ref = "espresso" }
espressoWeb = { module = "androidx.test.espresso:espresso-web", version.ref = "espresso" }
-errorProne = { module = "com.google.errorprone:error_prone_core", version = "2.23.0" }
+errorProne = { module = "com.google.errorprone:error_prone_core", version = "2.30.0" }
findbugs = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" }
firebaseAppindexing = { module = "com.google.firebase:firebase-appindexing", version = "19.2.0" }
freemarker = { module = "org.freemarker:freemarker", version = "2.3.31"}
@@ -266,6 +266,7 @@
playServicesBlockstore = {module = "com.google.android.gms:play-services-auth-blockstore", version = "16.4.0"}
playServicesDevicePerformance = { module = "com.google.android.gms:play-services-deviceperformance", version = "16.0.0" }
playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "21.0.0"}
+playServicesIdentityCredentials = {module = "com.google.android.gms:play-services-identity-credentials", version = "16.0.0-alpha02"}
playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
protobuf = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
protobufCompiler = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f5a3628..2d28c739 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -12,6 +12,7 @@
<trusted-artifacts>
<trust file="apiLevels.json" reason="We do not sign this metadata"/>
<trust group="androidx[.]annotation" version="1\.[0-7]\..*" regex="true" reason="Old versions, before signing"/>
+ <trust group="androidx[.]annotation" version="1\.9\.0-alpha[0-9][2-9]" regex="true" reason="New versions, not yet signed"/>
<trust group="androidx[.]collection" version="1\.[0-3]\..*" regex="true" reason="Old versions, before signing"/>
<trust group="com.android.ndk.thirdparty" reason="b/215430394"/>
<trust group="com.android.tools" name="desugar_jdk_libs" reason="b/215430394"/>
@@ -174,6 +175,7 @@
<trusting group="com.google.testing.compile"/>
<trusting group="com.google.truth"/>
<trusting group="com.google.truth.extensions"/>
+ <trusting group="org.jspecify"/>
</trusted-key>
<trusted-key id="44FBDBBC1A00FE414F1C1873586654072EAD6677" group="org.sonatype.oss"/>
<trusted-key id="47504B76CF89C15C0512D9AFE16AB52D79FD224F">
@@ -263,6 +265,9 @@
<trusting group="org.jetbrains.kotlin"/>
<trusting group="org.jetbrains.kotlin.jvm"/>
<trusting group="org.jetbrains.kotlin.plugin.serialization"/>
+ <trusting group="" name="kotlin-native-prebuilt-linux-x86_64"/>
+ <trusting group="" name="kotlin-native-prebuilt-macos-aarch64"/>
+ <trusting group="" name="kotlin-native-prebuilt-macos-x86_64"/>
</trusted-key>
<trusted-key id="6F656B7F6BFB238D38ACF81F3C27D97B0C83A85C" group="com.google.errorprone"/>
<trusted-key id="6F7E5ACBCD02DB60DFD232E45E1F79A7C298661E">
@@ -551,21 +556,6 @@
</trusted-keys>
</configuration>
<components>
- <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="2.0.10">
- <artifact name="kotlin-native-prebuilt-linux-x86_64-2.0.10.tar.gz">
- <sha256 value="c48badc9e0a01ed084bad6b13d81315910e27d50178351445c3c81991b6f090a" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-2.0.0.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
- </artifact>
- </component>
- <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="2.0.10">
- <artifact name="kotlin-native-prebuilt-macos-aarch64-2.0.10.tar.gz">
- <sha256 value="3a8f31ab752acf52324299d945df9d32ad65043411701e0be47b51fef75decbb" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-2.0.0.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
- </artifact>
- </component>
- <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="2.0.10">
- <artifact name="kotlin-native-prebuilt-macos-x86_64-2.0.10.tar.gz">
- <sha256 value="3162f53b70aeaaa4a8d47217ea5edad21b2b4ccb9e30663d5462a04df40ad9ef" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-2.0.0.tar.gz" reason="https://youtrack.jetbrains.com/issue/KT-52483"/>
- </artifact>
- </component>
<component group="androidx.compose.compiler" name="compiler" version="1.5.14">
<artifact name="compiler-1.5.14.jar">
<sha256 value="1ed55641e81177b693aa01f832bb1fd80bfb354caa8a9307c390f7a6738c803c" origin="Generated by Gradle" reason="Artifact is not signed"/>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt
index 179b70c..3f41299 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/CanvasFrontBufferedRendererTest.kt
@@ -101,9 +101,7 @@
}
) { scenario, renderer, surfaceView ->
renderLatch.set(CountDownLatch(1))
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
- renderer.renderFrontBufferedLayer(Any())
- }
+ scenario.onActivity { renderer.renderFrontBufferedLayer(Any()) }
Assert.assertTrue(renderLatch.get()!!.await(3000, TimeUnit.MILLISECONDS))
val coords = IntArray(2)
@@ -174,15 +172,13 @@
// NO-OP
}
}
- ) { scenario, _, surfaceView ->
+ ) { _, _, surfaceView ->
val paramLatch = CountDownLatch(1)
surfaceView.post {
surfaceView.layoutParams = FrameLayout.LayoutParams(width, height)
paramLatch.countDown()
}
paramLatch.await()
-
- scenario.moveToState(Lifecycle.State.RESUMED)
}
}
@@ -232,7 +228,7 @@
}
) { scenario, renderer, surfaceView ->
renderLatch.set(CountDownLatch(1))
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ scenario.onActivity {
renderer.renderFrontBufferedLayer(Any())
renderer.commit()
}
@@ -516,7 +512,7 @@
}
) { scenario, renderer, surfaceView ->
renderLatch.set(CountDownLatch(2))
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ scenario.onActivity {
with(renderer) {
renderFrontBufferedLayer(Color.BLUE)
commit()
@@ -605,14 +601,10 @@
var renderer: CanvasFrontBufferedRenderer<Float>? = null
var surfaceView: SurfaceView? = null
try {
- val scenario =
- ActivityScenario.launch(SurfaceViewTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- surfaceView = it.getSurfaceView().apply { setZOrderOnTop(true) }
- renderer = CanvasFrontBufferedRenderer(surfaceView!!, callbacks)
- }
- scenario.moveToState(Lifecycle.State.RESUMED)
+ ActivityScenario.launch(SurfaceViewTestActivity::class.java).onActivity {
+ surfaceView = it.getSurfaceView().apply { setZOrderOnTop(true) }
+ renderer = CanvasFrontBufferedRenderer(surfaceView!!, callbacks)
+ }
assertTrue(firstRenderLatch.await(3000, TimeUnit.MILLISECONDS))
@@ -1424,14 +1416,11 @@
var scenario: ActivityScenario<SurfaceViewTestActivity>? = null
try {
scenario =
- ActivityScenario.launch(SurfaceViewTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- surfaceView = it.getSurfaceView()
- renderer = CanvasFrontBufferedRenderer<T>(surfaceView!!, wrappedCallbacks)
- it.setOnDestroyCallback { destroyLatch.countDown() }
- }
- scenario.moveToState(Lifecycle.State.RESUMED)
+ ActivityScenario.launch(SurfaceViewTestActivity::class.java).onActivity {
+ surfaceView = it.getSurfaceView()
+ renderer = CanvasFrontBufferedRenderer<T>(surfaceView!!, wrappedCallbacks)
+ it.setOnDestroyCallback { destroyLatch.countDown() }
+ }
assertTrue(firstRenderLatch.await(3000, TimeUnit.MILLISECONDS))
block(scenario, renderer!!, surfaceView!!)
} finally {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LowLatencyCanvasViewTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LowLatencyCanvasViewTest.kt
index f046ada..299c232 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LowLatencyCanvasViewTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LowLatencyCanvasViewTest.kt
@@ -114,14 +114,24 @@
scenarioCallback = { scenario ->
val resumeLatch = CountDownLatch(1)
val drawLatch = CountDownLatch(1)
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ var lowLatencyView: LowLatencyCanvasView? = null
+ scenario.onActivity {
val view = it.getLowLatencyCanvasView()
- view.clear()
view.post { drawLatch.countDown() }
resumeLatch.countDown()
+ lowLatencyView = view
}
assertTrue(resumeLatch.await(3000, TimeUnit.MILLISECONDS))
assertTrue(drawLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val clearLatch = CountDownLatch(1)
+ scenario.onActivity {
+ lowLatencyView?.let {
+ it.clear()
+ it.post { clearLatch.countDown() }
+ }
+ }
+ assertTrue(clearLatch.await(3000, TimeUnit.MILLISECONDS))
},
validateBitmap = { bitmap, left, top, right, bottom ->
Color.WHITE == bitmap.getPixel(left + (right - left) / 2, top + (bottom - top) / 2)
@@ -197,9 +207,7 @@
}
},
scenarioCallback = { scenario ->
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
- it.getLowLatencyCanvasView().renderFrontBufferedLayer()
- }
+ scenario.onActivity { it.getLowLatencyCanvasView().renderFrontBufferedLayer() }
assertTrue(renderFrontBufferLatch.await(3000, TimeUnit.MILLISECONDS))
},
validateBitmap = { bitmap, left, top, right, bottom ->
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt
index 966c457..b417a45 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlUtils.kt
@@ -49,36 +49,32 @@
var surfaceView: SurfaceView? = null
val destroyLatch = CountDownLatch(1)
val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- it.setDestroyCallback { destroyLatch.countDown() }
- val callback =
- object : SurfaceHolder.Callback {
- override fun surfaceCreated(sh: SurfaceHolder) {
- surfaceView = it.mSurfaceView
- onSurfaceCreated(surfaceView!!, setupLatch)
- }
-
- override fun surfaceChanged(
- holder: SurfaceHolder,
- format: Int,
- width: Int,
- height: Int
- ) {
- // NO-OP
- }
-
- override fun surfaceDestroyed(holder: SurfaceHolder) {
- // NO-OP
- }
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+ it.setDestroyCallback { destroyLatch.countDown() }
+ val callback =
+ object : SurfaceHolder.Callback {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+ surfaceView = it.mSurfaceView
+ onSurfaceCreated(surfaceView!!, setupLatch)
}
- it.addSurface(it.mSurfaceView, callback)
- surfaceView = it.mSurfaceView
- }
+ override fun surfaceChanged(
+ holder: SurfaceHolder,
+ format: Int,
+ width: Int,
+ height: Int
+ ) {
+ // NO-OP
+ }
- scenario.moveToState(Lifecycle.State.RESUMED)
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ // NO-OP
+ }
+ }
+
+ it.addSurface(it.mSurfaceView, callback)
+ surfaceView = it.mSurfaceView
+ }
Assert.assertTrue(setupLatch.await(3000, TimeUnit.MILLISECONDS))
val coords = intArrayOf(0, 0)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
index 4a3e5b9..dec948e 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
@@ -137,9 +137,7 @@
fun testSurfaceTransactionOnCompleteCallback() {
val listener = TransactionOnCompleteListener()
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
val destroyLatch = CountDownLatch(1)
try {
@@ -150,8 +148,6 @@
.commit()
}
- scenario.moveToState(Lifecycle.State.RESUMED)
-
listener.mLatch.await(3, TimeUnit.SECONDS)
assertEquals(0, listener.mLatch.count)
assertTrue(listener.mCallbackTime > 0)
@@ -167,9 +163,7 @@
fun testSurfaceTransactionOnCommitCallback() {
val listener = TransactionOnCommitListener()
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
val destroyLatch = CountDownLatch(1)
try {
@@ -179,7 +173,6 @@
.addTransactionCommittedListener(executor!!, listener)
.commit()
}
- scenario.moveToState(Lifecycle.State.RESUMED)
listener.mLatch.await(3, TimeUnit.SECONDS)
assertEquals(0, listener.mLatch.count)
@@ -197,9 +190,7 @@
val listener = TransactionOnCommitListener()
val listener2 = TransactionOnCommitListener()
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
val destroyLatch = CountDownLatch(1)
try {
@@ -211,8 +202,6 @@
.commit()
}
- scenario.moveToState(Lifecycle.State.RESUMED)
-
listener.mLatch.await(3, TimeUnit.SECONDS)
listener2.mLatch.await(3, TimeUnit.SECONDS)
@@ -234,9 +223,7 @@
val listener1 = TransactionOnCommitListener()
val listener2 = TransactionOnCompleteListener()
- val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
+ val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
val destroyLatch = CountDownLatch(1)
try {
@@ -248,8 +235,6 @@
.commit()
}
- scenario.moveToState(Lifecycle.State.RESUMED)
-
listener1.mLatch.await(3, TimeUnit.SECONDS)
listener2.mLatch.await(3, TimeUnit.SECONDS)
@@ -852,39 +837,37 @@
var scCompat: SurfaceControlWrapper? = null
val listener = TransactionOnCompleteListener()
val scenario =
- ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
- .moveToState(Lifecycle.State.CREATED)
- .onActivity {
- val callback =
- object : SurfaceHolderCallback() {
- override fun surfaceCreated(sh: SurfaceHolder) {
- scCompat =
- SurfaceControlWrapper.Builder()
- .setParent(it.getSurfaceView().holder.surface)
- .setDebugName("SurfaceControlCompatTest")
- .build()
+ ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+ val callback =
+ object : SurfaceHolderCallback() {
+ override fun surfaceCreated(sh: SurfaceHolder) {
+ scCompat =
+ SurfaceControlWrapper.Builder()
+ .setParent(it.getSurfaceView().holder.surface)
+ .setDebugName("SurfaceControlCompatTest")
+ .build()
- // Buffer colorspace is RGBA, so Color.BLUE will be visually Red
- val buffer =
- SurfaceControlUtils.getSolidBuffer(
- SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
- SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
- Color.BLUE
- )
+ // Buffer colorspace is RGBA, so Color.BLUE will be visually Red
+ val buffer =
+ SurfaceControlUtils.getSolidBuffer(
+ SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
+ SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
+ Color.BLUE
+ )
- SurfaceControlWrapper.Transaction()
- .addTransactionCompletedListener(listener)
- .setBuffer(scCompat!!, buffer)
- .setVisibility(scCompat!!, true)
- .setCrop(scCompat!!, Rect(20, 30, 90, 60))
- .commit()
- }
+ SurfaceControlWrapper.Transaction()
+ .addTransactionCompletedListener(listener)
+ .setBuffer(scCompat!!, buffer)
+ .setVisibility(scCompat!!, true)
+ .setCrop(scCompat!!, Rect(20, 30, 90, 60))
+ .commit()
}
+ }
- it.addSurface(it.mSurfaceView, callback)
- }
+ it.addSurface(it.mSurfaceView, callback)
+ }
- scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ scenario.onActivity {
assert(listener.mLatch.await(3000, TimeUnit.MILLISECONDS))
SurfaceControlUtils.validateOutput { bitmap ->
val coord = intArrayOf(0, 0)
diff --git a/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
index fd9bb84..c7f7c75 100644
--- a/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
+++ b/graphics/graphics-shapes/src/androidInstrumentedTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
@@ -16,6 +16,7 @@
package androidx.graphics.shapes
+import android.graphics.Matrix
import androidx.test.filters.SmallTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
@@ -200,6 +201,57 @@
assertNotEquals(preTransformCenters, postTransformCenters)
}
+ @Test
+ fun transformKeepsContiguousAnchorsEqual() {
+ val poly =
+ RoundedPolygon(radius = 1f, numVertices = 4, rounding = CornerRounding(7 / 15f))
+ .transformed(
+ Matrix().apply {
+ postRotate(45f)
+ postScale(648f, 648f)
+ postTranslate(540f, 1212f)
+ }
+ )
+ poly.cubics.indices.forEach { i ->
+ // It has to be the same point
+ assertEquals(
+ "Failed at X, index $i",
+ poly.cubics[i].anchor1X,
+ poly.cubics[(i + 1) % poly.cubics.size].anchor0X,
+ 0f
+ )
+ assertEquals(
+ "Failed at Y, index $i",
+ poly.cubics[i].anchor1Y,
+ poly.cubics[(i + 1) % poly.cubics.size].anchor0Y,
+ 0f
+ )
+ }
+ }
+
+ @Test
+ fun emptyPolygonTest() {
+ val poly = RoundedPolygon(6, radius = 0f, rounding = CornerRounding(0.1f))
+ assert(poly.cubics.size == 1)
+
+ val stillEmpty = poly.transformed(scaleTransform(10f, 20f))
+ assert(stillEmpty.cubics.size == 1)
+ assert(stillEmpty.cubics.first().zeroLength())
+ }
+
+ @Test
+ fun emptySideTest() {
+ val poly1 =
+ RoundedPolygon(
+ floatArrayOf(0f, 0f, 1f, 0f, 1f, 0f, 0f, 1f), // Triangle with one point repeated
+ )
+ val poly2 =
+ RoundedPolygon(
+ floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f), // Triangle
+ )
+ assertCubicListsEqualish(poly1.cubics, poly2.cubics)
+ }
+
private fun nonzeroCubics(original: List<Cubic>): List<Cubic> {
val result = mutableListOf<Cubic>()
for (i in original.indices) {
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt
index b2fc8f2..5ada39e 100644
--- a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt
@@ -67,13 +67,14 @@
// enough discontinuity to throw an exception later, even though the
// distances are quite small. Account for that by making the last
// cubic use the latest anchor point, always.
+ lastCubic = Cubic(lastCubic.points.copyOf()) // Make a copy before mutating
lastCubic.points[6] = cubic.anchor1X
lastCubic.points[7] = cubic.anchor1Y
}
}
}
}
- if (lastCubic != null && firstCubic != null)
+ if (lastCubic != null && firstCubic != null) {
add(
Cubic(
lastCubic.anchor0X,
@@ -86,6 +87,21 @@
firstCubic.anchor0Y
)
)
+ } else {
+ // Empty / 0-sized polygon.
+ add(
+ Cubic(
+ centerX,
+ centerY,
+ centerX,
+ centerY,
+ centerX,
+ centerY,
+ centerX,
+ centerY,
+ )
+ )
+ }
}
init {
@@ -99,12 +115,11 @@
abs(cubic.anchor0Y - prevCubic.anchor1Y) > DistanceEpsilon
) {
debugLog("RoundedPolygon") {
- "Ix: $index | (${cubic.anchor0X},${cubic.anchor0Y}) vs " + "$prevCubic"
+ "Ix: $index | (${cubic.anchor0X},${cubic.anchor0Y}) vs $prevCubic"
}
throw IllegalArgumentException(
- "RoundedPolygon must be contiguous, with the " +
- "anchor points of all curves matching the anchor points of the preceding " +
- "and succeeding cubics"
+ "RoundedPolygon must be contiguous, with the anchor points of all curves " +
+ "matching the anchor points of the preceding and succeeding cubics"
)
}
prevCubic = cubic
@@ -482,27 +497,51 @@
val p2: Point,
val rounding: CornerRounding? = null
) {
- val d1 = (p0 - p1).getDirection()
- val d2 = (p2 - p1).getDirection()
- val cornerRadius = rounding?.radius ?: 0f
- val smoothing = rounding?.smoothing ?: 0f
+ val d1: Point
+ val d2: Point
+ val cornerRadius: Float
+ val smoothing: Float
+ val cosAngle: Float
+ val sinAngle: Float
+ val expectedRoundCut: Float
- // cosine of angle at p1 is dot product of unit vectors to the other two vertices
- val cosAngle = d1.dotProduct(d2)
+ init {
+ val v01 = p0 - p1
+ val v21 = p2 - p1
+ val d01 = v01.getDistance()
+ val d21 = v21.getDistance()
+ if (d01 > 0f && d21 > 0f) {
+ d1 = v01 / d01
+ d2 = v21 / d21
+ cornerRadius = rounding?.radius ?: 0f
+ smoothing = rounding?.smoothing ?: 0f
- // identity: sin^2 + cos^2 = 1
- // sinAngle gives us the intersection
- val sinAngle = sqrt(1 - square(cosAngle))
+ // cosine of angle at p1 is dot product of unit vectors to the other two vertices
+ cosAngle = d1.dotProduct(d2)
- // How much we need to cut, as measured on a side, to get the required radius
- // calculating where the rounding circle hits the edge
- // This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
- val expectedRoundCut =
- if (sinAngle > 1e-3) {
- cornerRadius * (cosAngle + 1) / sinAngle
+ // identity: sin^2 + cos^2 = 1
+ // sinAngle gives us the intersection
+ sinAngle = sqrt(1 - square(cosAngle))
+ // How much we need to cut, as measured on a side, to get the required radius
+ // calculating where the rounding circle hits the edge
+ // This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
+ expectedRoundCut =
+ if (sinAngle > 1e-3) {
+ cornerRadius * (cosAngle + 1) / sinAngle
+ } else {
+ 0f
+ }
} else {
- 0f
+ // One (or both) of the sides is empty, not much we can do.
+ d1 = Point(0f, 0f)
+ d2 = Point(0f, 0f)
+ cornerRadius = 0f
+ smoothing = 0f
+ cosAngle = 0f
+ sinAngle = 0f
+ expectedRoundCut = 0f
}
+ }
// smoothing changes the actual cut. 0 is same as expectedRoundCut, 1 doubles it
val expectedCut: Float
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 4fabbb6..d5d6059 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -9,7 +9,7 @@
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public androidx.health.connect.client.HealthConnectFeatures getFeatures();
+ method public default androidx.health.connect.client.HealthConnectFeatures getFeatures();
method public static android.content.Intent getHealthConnectManageDataIntent(android.content.Context context);
method public static android.content.Intent getHealthConnectManageDataIntent(android.content.Context context, optional String providerPackageName);
method public static String getHealthConnectSettingsAction();
@@ -23,7 +23,7 @@
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public static String ACTION_HEALTH_CONNECT_SETTINGS;
- property public abstract androidx.health.connect.client.HealthConnectFeatures features;
+ property @SuppressCompatibility @androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi public default androidx.health.connect.client.HealthConnectFeatures features;
property public abstract androidx.health.connect.client.PermissionController permissionController;
field public static final androidx.health.connect.client.HealthConnectClient.Companion Companion;
field public static final int SDK_AVAILABLE = 3; // 0x3
@@ -1133,42 +1133,6 @@
public static final class SexualActivityRecord.Companion {
}
- public final class SkinTemperatureRecord implements androidx.health.connect.client.records.Record {
- ctor public SkinTemperatureRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.SkinTemperatureRecord.Delta> deltas, optional androidx.health.connect.client.units.Temperature? baseline, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Temperature? getBaseline();
- method public java.util.List<androidx.health.connect.client.records.SkinTemperatureRecord.Delta> getDeltas();
- method public java.time.Instant getEndTime();
- method public java.time.ZoneOffset? getEndZoneOffset();
- method public int getMeasurementLocation();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getStartTime();
- method public java.time.ZoneOffset? getStartZoneOffset();
- property public final androidx.health.connect.client.units.Temperature? baseline;
- property public final java.util.List<androidx.health.connect.client.records.SkinTemperatureRecord.Delta> deltas;
- property public java.time.Instant endTime;
- property public java.time.ZoneOffset? endZoneOffset;
- property public final int measurementLocation;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant startTime;
- property public java.time.ZoneOffset? startZoneOffset;
- field public static final androidx.health.connect.client.records.SkinTemperatureRecord.Companion Companion;
- field public static final int MEASUREMENT_LOCATION_FINGER = 1; // 0x1
- field public static final int MEASUREMENT_LOCATION_TOE = 2; // 0x2
- field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
- field public static final int MEASUREMENT_LOCATION_WRIST = 3; // 0x3
- }
-
- public static final class SkinTemperatureRecord.Companion {
- }
-
- public static final class SkinTemperatureRecord.Delta {
- ctor public SkinTemperatureRecord.Delta(java.time.Instant time, androidx.health.connect.client.units.TemperatureDelta delta);
- method public androidx.health.connect.client.units.TemperatureDelta getDelta();
- method public java.time.Instant getTime();
- property public final androidx.health.connect.client.units.TemperatureDelta delta;
- property public final java.time.Instant time;
- }
-
public final class SleepSessionRecord implements androidx.health.connect.client.records.Record {
ctor public SleepSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional String? title, optional String? notes, optional java.util.List<androidx.health.connect.client.records.SleepSessionRecord.Stage> stages, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
@@ -1680,22 +1644,6 @@
method public androidx.health.connect.client.units.Temperature fahrenheit(double value);
}
- public final class TemperatureDelta implements java.lang.Comparable<androidx.health.connect.client.units.TemperatureDelta> {
- method public static androidx.health.connect.client.units.TemperatureDelta celsius(double value);
- method public int compareTo(androidx.health.connect.client.units.TemperatureDelta other);
- method public static androidx.health.connect.client.units.TemperatureDelta fahrenheit(double value);
- method public double getCelsius();
- method public double getFahrenheit();
- property public final double inCelsius;
- property public final double inFahrenheit;
- field public static final androidx.health.connect.client.units.TemperatureDelta.Companion Companion;
- }
-
- public static final class TemperatureDelta.Companion {
- method public androidx.health.connect.client.units.TemperatureDelta celsius(double value);
- method public androidx.health.connect.client.units.TemperatureDelta fahrenheit(double value);
- }
-
public final class Velocity implements java.lang.Comparable<androidx.health.connect.client.units.Velocity> {
method public int compareTo(androidx.health.connect.client.units.Velocity other);
method public double getKilometersPerHour();
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 3756d80..518453c 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -9,7 +9,7 @@
method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> recordIdsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public androidx.health.connect.client.HealthConnectFeatures getFeatures();
+ method public default androidx.health.connect.client.HealthConnectFeatures getFeatures();
method public static android.content.Intent getHealthConnectManageDataIntent(android.content.Context context);
method public static android.content.Intent getHealthConnectManageDataIntent(android.content.Context context, optional String providerPackageName);
method public static String getHealthConnectSettingsAction();
@@ -23,7 +23,7 @@
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public static String ACTION_HEALTH_CONNECT_SETTINGS;
- property public abstract androidx.health.connect.client.HealthConnectFeatures features;
+ property @SuppressCompatibility @androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi public default androidx.health.connect.client.HealthConnectFeatures features;
property public abstract androidx.health.connect.client.PermissionController permissionController;
field public static final androidx.health.connect.client.HealthConnectClient.Companion Companion;
field public static final int SDK_AVAILABLE = 3; // 0x3
@@ -1156,42 +1156,6 @@
public static final class SexualActivityRecord.Companion {
}
- public final class SkinTemperatureRecord implements androidx.health.connect.client.records.IntervalRecord {
- ctor public SkinTemperatureRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List<androidx.health.connect.client.records.SkinTemperatureRecord.Delta> deltas, optional androidx.health.connect.client.units.Temperature? baseline, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Temperature? getBaseline();
- method public java.util.List<androidx.health.connect.client.records.SkinTemperatureRecord.Delta> getDeltas();
- method public java.time.Instant getEndTime();
- method public java.time.ZoneOffset? getEndZoneOffset();
- method public int getMeasurementLocation();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getStartTime();
- method public java.time.ZoneOffset? getStartZoneOffset();
- property public final androidx.health.connect.client.units.Temperature? baseline;
- property public final java.util.List<androidx.health.connect.client.records.SkinTemperatureRecord.Delta> deltas;
- property public java.time.Instant endTime;
- property public java.time.ZoneOffset? endZoneOffset;
- property public final int measurementLocation;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant startTime;
- property public java.time.ZoneOffset? startZoneOffset;
- field public static final androidx.health.connect.client.records.SkinTemperatureRecord.Companion Companion;
- field public static final int MEASUREMENT_LOCATION_FINGER = 1; // 0x1
- field public static final int MEASUREMENT_LOCATION_TOE = 2; // 0x2
- field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
- field public static final int MEASUREMENT_LOCATION_WRIST = 3; // 0x3
- }
-
- public static final class SkinTemperatureRecord.Companion {
- }
-
- public static final class SkinTemperatureRecord.Delta {
- ctor public SkinTemperatureRecord.Delta(java.time.Instant time, androidx.health.connect.client.units.TemperatureDelta delta);
- method public androidx.health.connect.client.units.TemperatureDelta getDelta();
- method public java.time.Instant getTime();
- property public final androidx.health.connect.client.units.TemperatureDelta delta;
- property public final java.time.Instant time;
- }
-
public final class SleepSessionRecord implements androidx.health.connect.client.records.IntervalRecord {
ctor public SleepSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional String? title, optional String? notes, optional java.util.List<androidx.health.connect.client.records.SleepSessionRecord.Stage> stages, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
@@ -1703,22 +1667,6 @@
method public androidx.health.connect.client.units.Temperature fahrenheit(double value);
}
- public final class TemperatureDelta implements java.lang.Comparable<androidx.health.connect.client.units.TemperatureDelta> {
- method public static androidx.health.connect.client.units.TemperatureDelta celsius(double value);
- method public int compareTo(androidx.health.connect.client.units.TemperatureDelta other);
- method public static androidx.health.connect.client.units.TemperatureDelta fahrenheit(double value);
- method public double getCelsius();
- method public double getFahrenheit();
- property public final double inCelsius;
- property public final double inFahrenheit;
- field public static final androidx.health.connect.client.units.TemperatureDelta.Companion Companion;
- }
-
- public static final class TemperatureDelta.Companion {
- method public androidx.health.connect.client.units.TemperatureDelta celsius(double value);
- method public androidx.health.connect.client.units.TemperatureDelta fahrenheit(double value);
- }
-
public final class Velocity implements java.lang.Comparable<androidx.health.connect.client.units.Velocity> {
method public int compareTo(androidx.health.connect.client.units.Velocity other);
method public double getKilometersPerHour();
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index ac23114..14f303c 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -27,6 +27,7 @@
import androidx.health.connect.client.changes.UpsertionChange
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
import androidx.health.connect.client.impl.converters.datatype.RECORDS_CLASS_NAME_MAP
+import androidx.health.connect.client.impl.platform.aggregate.AGGREGATE_METRICS_ADDED_IN_SDK_EXT_10
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
import androidx.health.connect.client.readRecord
import androidx.health.connect.client.records.BloodPressureRecord
@@ -59,8 +60,10 @@
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.After
+import org.junit.Assert.assertThrows
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -420,6 +423,45 @@
}
}
+ // TODO(b/361297592): Remove once the aggregation bug is fixed
+ @Test
+ fun aggregateRecords_unsupportedMetrics_throwsUOE() = runTest {
+ for (metric in AGGREGATE_METRICS_ADDED_IN_SDK_EXT_10) {
+ assertThrows(UnsupportedOperationException::class.java) {
+ runBlocking {
+ healthConnectClient.aggregate(
+ AggregateRequest(setOf(metric), TimeRangeFilter.none())
+ )
+ }
+ }
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ runBlocking {
+ healthConnectClient.aggregateGroupByDuration(
+ AggregateGroupByDurationRequest(
+ setOf(metric),
+ TimeRangeFilter.none(),
+ Duration.ofDays(1)
+ )
+ )
+ }
+ }
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ runBlocking {
+ healthConnectClient.aggregateGroupByPeriod(
+ AggregateGroupByPeriodRequest(
+ setOf(metric),
+ TimeRangeFilter.none(),
+ Period.ofDays(1)
+ )
+ )
+ }
+ }
+ }
+ }
+
+ @Ignore("b/326414908")
@Test
fun aggregateRecords_belowSdkExt10() = runTest {
assumeFalse(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10)
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 6a51c31..d8c56c7 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
@@ -30,6 +30,7 @@
import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
+import androidx.health.connect.client.feature.HealthConnectFeaturesUnavailableImpl
import androidx.health.connect.client.impl.HealthConnectClientImpl
import androidx.health.connect.client.impl.HealthConnectClientUpsideDownImpl
import androidx.health.connect.client.records.Record
@@ -55,7 +56,9 @@
val permissionController: PermissionController
/** Access operations related to feature availability. */
- @ExperimentalFeatureAvailabilityApi val features: HealthConnectFeatures
+ @ExperimentalFeatureAvailabilityApi
+ val features: HealthConnectFeatures
+ get() = HealthConnectFeaturesUnavailableImpl
/**
* Inserts one or more [Record] and returns newly assigned
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
index 13cbe90..e6f24bb 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
@@ -35,6 +35,7 @@
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.aggregate.AggregateMetric
import androidx.health.connect.client.aggregate.AggregationResult
import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
@@ -42,6 +43,7 @@
import androidx.health.connect.client.changes.UpsertionChange
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
import androidx.health.connect.client.feature.HealthConnectFeaturesPlatformImpl
+import androidx.health.connect.client.impl.platform.aggregate.AGGREGATE_METRICS_ADDED_IN_SDK_EXT_10
import androidx.health.connect.client.impl.platform.aggregate.aggregateFallback
import androidx.health.connect.client.impl.platform.aggregate.platformMetrics
import androidx.health.connect.client.impl.platform.aggregate.plus
@@ -216,9 +218,7 @@
}
override suspend fun aggregate(request: AggregateRequest): AggregationResult {
- if (request.metrics.isEmpty()) {
- throw IllegalArgumentException("Requested record types must not be empty.")
- }
+ verifyAggregationMetrics(request.metrics)
val fallbackResponse = aggregateFallback(request)
@@ -244,6 +244,8 @@
override suspend fun aggregateGroupByDuration(
request: AggregateGroupByDurationRequest
): List<AggregationResultGroupedByDuration> {
+ verifyAggregationMetrics(request.metrics)
+
return wrapPlatformException {
suspendCancellableCoroutine { continuation ->
healthConnectManager.aggregateGroupByDuration(
@@ -260,6 +262,8 @@
override suspend fun aggregateGroupByPeriod(
request: AggregateGroupByPeriodRequest
): List<AggregationResultGroupedByPeriod> {
+ verifyAggregationMetrics(request.metrics)
+
return wrapPlatformException {
suspendCancellableCoroutine { continuation ->
healthConnectManager.aggregateGroupByPeriod(
@@ -299,6 +303,12 @@
}
}
+ private fun verifyAggregationMetrics(metrics: Set<AggregateMetric<*>>) {
+ AGGREGATE_METRICS_ADDED_IN_SDK_EXT_10.intersect(metrics).firstOrNull()?.let {
+ throw UnsupportedOperationException("Unsupported metric type ${it.metricKey}")
+ }
+ }
+
override suspend fun getChangesToken(request: ChangesTokenRequest): String {
return wrapPlatformException {
suspendCancellableCoroutine { continuation ->
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index 68b8036..d3b5712b 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -214,6 +214,7 @@
internal const val READ_OXYGEN_SATURATION = PERMISSION_PREFIX + "READ_OXYGEN_SATURATION"
internal const val READ_RESPIRATORY_RATE = PERMISSION_PREFIX + "READ_RESPIRATORY_RATE"
internal const val READ_RESTING_HEART_RATE = PERMISSION_PREFIX + "READ_RESTING_HEART_RATE"
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val READ_SKIN_TEMPERATURE = PERMISSION_PREFIX + "READ_SKIN_TEMPERATURE"
// Write permissions for ACTIVITY.
@@ -272,6 +273,8 @@
internal const val WRITE_OXYGEN_SATURATION = PERMISSION_PREFIX + "WRITE_OXYGEN_SATURATION"
internal const val WRITE_RESPIRATORY_RATE = PERMISSION_PREFIX + "WRITE_RESPIRATORY_RATE"
internal const val WRITE_RESTING_HEART_RATE = PERMISSION_PREFIX + "WRITE_RESTING_HEART_RATE"
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
internal const val WRITE_SKIN_TEMPERATURE = PERMISSION_PREFIX + "WRITE_SKIN_TEMPERATURE"
internal const val READ_PERMISSION_PREFIX = PERMISSION_PREFIX + "READ_"
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt
index 9be96d0..6a1b829 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SkinTemperatureRecord.kt
@@ -49,6 +49,7 @@
* [SkinTemperatureMeasurementLocation].
* @param metadata set of common metadata associated with the written record.
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
class SkinTemperatureRecord(
override val startTime: Instant,
override val startZoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/units/TemperatureDelta.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/units/TemperatureDelta.kt
index 0d5f9ef..abf4905 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/units/TemperatureDelta.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/units/TemperatureDelta.kt
@@ -16,11 +16,14 @@
package androidx.health.connect.client.units
+import androidx.annotation.RestrictTo
+
/**
* Represents a unit of TemperatureDelta difference. Supported units:
* - Celsius - see [TemperatureDelta.celsius], [Double.celsius]
* - Fahrenheit - see [TemperatureDelta.fahrenheit], [Double.fahrenheit]
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
class TemperatureDelta
private constructor(
private val value: Double,
diff --git a/health/connect/connect-testing/api/current.txt b/health/connect/connect-testing/api/current.txt
index 9834c8e..a6ddad6 100644
--- a/health/connect/connect-testing/api/current.txt
+++ b/health/connect/connect-testing/api/current.txt
@@ -21,7 +21,6 @@
method public void expireToken(String token);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public androidx.health.connect.client.HealthConnectFeatures getFeatures();
method public androidx.health.connect.client.testing.FakeHealthConnectClientOverrides getOverrides();
method public int getPageSizeGetChanges();
method public androidx.health.connect.client.PermissionController getPermissionController();
diff --git a/health/connect/connect-testing/api/restricted_current.txt b/health/connect/connect-testing/api/restricted_current.txt
index 9834c8e..a6ddad6 100644
--- a/health/connect/connect-testing/api/restricted_current.txt
+++ b/health/connect/connect-testing/api/restricted_current.txt
@@ -21,7 +21,6 @@
method public void expireToken(String token);
method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
- method public androidx.health.connect.client.HealthConnectFeatures getFeatures();
method public androidx.health.connect.client.testing.FakeHealthConnectClientOverrides getOverrides();
method public int getPageSizeGetChanges();
method public androidx.health.connect.client.PermissionController getPermissionController();
diff --git a/hilt/hilt-navigation-fragment/src/main/java/androidx/hilt/navigation/fragment/HiltNavGraphViewModelLazy.kt b/hilt/hilt-navigation-fragment/src/main/java/androidx/hilt/navigation/fragment/HiltNavGraphViewModelLazy.kt
index 967f6f4..2a0a653 100644
--- a/hilt/hilt-navigation-fragment/src/main/java/androidx/hilt/navigation/fragment/HiltNavGraphViewModelLazy.kt
+++ b/hilt/hilt-navigation-fragment/src/main/java/androidx/hilt/navigation/fragment/HiltNavGraphViewModelLazy.kt
@@ -42,7 +42,6 @@
* @param navGraphId ID of a NavGraph that exists on the [NavController] back stack
*/
@MainThread
-@Suppress("MissingNullability") // Due to https://youtrack.jetbrains.com/issue/KT-39209
public inline fun <reified VM : ViewModel> Fragment.hiltNavGraphViewModels(
@IdRes navGraphId: Int
): Lazy<VM> {
@@ -79,7 +78,6 @@
* HiltViewModel using @AssistedInject-annotated constructor.
*/
@MainThread
-@Suppress("MissingNullability") // Due to https://youtrack.jetbrains.com/issue/KT-39209
public inline fun <reified VM : ViewModel, reified VMF : Any> Fragment.hiltNavGraphViewModels(
@IdRes navGraphId: Int,
noinline creationCallback: (VMF) -> VM
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index c10c574..7582fcf 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -2,11 +2,78 @@
package androidx.ink.brush {
public final class Brush {
- ctor public Brush(long color, float size);
- method public long getColor();
+ ctor public Brush(androidx.ink.brush.BrushFamily family, float size, float epsilon);
+ method public static androidx.ink.brush.Brush.Builder builder();
+ method public androidx.ink.brush.Brush copy();
+ method public androidx.ink.brush.Brush copy(optional androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush copy(optional androidx.ink.brush.BrushFamily family, optional float size);
+ method public androidx.ink.brush.Brush copy(optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb, optional androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb, optional androidx.ink.brush.BrushFamily family, optional float size);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong, optional androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong, optional androidx.ink.brush.BrushFamily family, optional float size);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method public static androidx.ink.brush.Brush createWithColorIntArgb(androidx.ink.brush.BrushFamily family, @ColorInt int colorIntArgb, float size, float epsilon);
+ method public static androidx.ink.brush.Brush createWithColorLong(androidx.ink.brush.BrushFamily family, @ColorLong long colorLong, float size, float epsilon);
+ method protected void finalize();
+ method @ColorInt public int getColorIntArgb();
+ method @ColorLong public long getColorLong();
+ method public float getEpsilon();
+ method public androidx.ink.brush.BrushFamily getFamily();
method public float getSize();
- property public final long color;
+ method public androidx.ink.brush.Brush.Builder toBuilder();
+ property @ColorInt public final int colorIntArgb;
+ property @ColorLong public final long colorLong;
+ property public final float epsilon;
+ property public final androidx.ink.brush.BrushFamily family;
property public final float size;
+ field public static final androidx.ink.brush.Brush.Companion Companion;
+ }
+
+ public static final class Brush.Builder {
+ ctor public Brush.Builder();
+ method public androidx.ink.brush.Brush build();
+ method public androidx.ink.brush.Brush.Builder setColorIntArgb(@ColorInt int colorIntArgb);
+ method public androidx.ink.brush.Brush.Builder setColorLong(@ColorLong long colorLong);
+ method public androidx.ink.brush.Brush.Builder setEpsilon(@FloatRange(from=0.0, fromInclusive=false, to=kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY, toInclusive=false) float epsilon);
+ method public androidx.ink.brush.Brush.Builder setFamily(androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush.Builder setSize(@FloatRange(from=0.0, fromInclusive=false, to=kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY, toInclusive=false) float size);
+ }
+
+ public static final class Brush.Companion {
+ method public androidx.ink.brush.Brush.Builder builder();
+ method public androidx.ink.brush.Brush createWithColorIntArgb(androidx.ink.brush.BrushFamily family, @ColorInt int colorIntArgb, float size, float epsilon);
+ method public androidx.ink.brush.Brush createWithColorLong(androidx.ink.brush.BrushFamily family, @ColorLong long colorLong, float size, float epsilon);
+ }
+
+ public final class BrushFamily {
+ method protected void finalize();
+ field public static final androidx.ink.brush.BrushFamily.Companion Companion;
+ }
+
+ public static final class BrushFamily.Companion {
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
+ }
+
+ public final class StockBrushes {
+ method public static androidx.ink.brush.BrushFamily getHighlighterLatest();
+ method public static androidx.ink.brush.BrushFamily getHighlighterV1();
+ method public static androidx.ink.brush.BrushFamily getMarkerLatest();
+ method public static androidx.ink.brush.BrushFamily getMarkerV1();
+ method public static androidx.ink.brush.BrushFamily getPressurePenLatest();
+ method public static androidx.ink.brush.BrushFamily getPressurePenV1();
+ property public static final androidx.ink.brush.BrushFamily highlighterLatest;
+ property public static final androidx.ink.brush.BrushFamily highlighterV1;
+ property public static final androidx.ink.brush.BrushFamily markerLatest;
+ property public static final androidx.ink.brush.BrushFamily markerV1;
+ property public static final androidx.ink.brush.BrushFamily pressurePenLatest;
+ property public static final androidx.ink.brush.BrushFamily pressurePenV1;
+ field public static final androidx.ink.brush.StockBrushes INSTANCE;
}
}
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index c10c574..7582fcf 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -2,11 +2,78 @@
package androidx.ink.brush {
public final class Brush {
- ctor public Brush(long color, float size);
- method public long getColor();
+ ctor public Brush(androidx.ink.brush.BrushFamily family, float size, float epsilon);
+ method public static androidx.ink.brush.Brush.Builder builder();
+ method public androidx.ink.brush.Brush copy();
+ method public androidx.ink.brush.Brush copy(optional androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush copy(optional androidx.ink.brush.BrushFamily family, optional float size);
+ method public androidx.ink.brush.Brush copy(optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb, optional androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb, optional androidx.ink.brush.BrushFamily family, optional float size);
+ method public androidx.ink.brush.Brush copyWithColorIntArgb(@ColorInt int colorIntArgb, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong, optional androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong, optional androidx.ink.brush.BrushFamily family, optional float size);
+ method public androidx.ink.brush.Brush copyWithColorLong(@ColorLong long colorLong, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method public static androidx.ink.brush.Brush createWithColorIntArgb(androidx.ink.brush.BrushFamily family, @ColorInt int colorIntArgb, float size, float epsilon);
+ method public static androidx.ink.brush.Brush createWithColorLong(androidx.ink.brush.BrushFamily family, @ColorLong long colorLong, float size, float epsilon);
+ method protected void finalize();
+ method @ColorInt public int getColorIntArgb();
+ method @ColorLong public long getColorLong();
+ method public float getEpsilon();
+ method public androidx.ink.brush.BrushFamily getFamily();
method public float getSize();
- property public final long color;
+ method public androidx.ink.brush.Brush.Builder toBuilder();
+ property @ColorInt public final int colorIntArgb;
+ property @ColorLong public final long colorLong;
+ property public final float epsilon;
+ property public final androidx.ink.brush.BrushFamily family;
property public final float size;
+ field public static final androidx.ink.brush.Brush.Companion Companion;
+ }
+
+ public static final class Brush.Builder {
+ ctor public Brush.Builder();
+ method public androidx.ink.brush.Brush build();
+ method public androidx.ink.brush.Brush.Builder setColorIntArgb(@ColorInt int colorIntArgb);
+ method public androidx.ink.brush.Brush.Builder setColorLong(@ColorLong long colorLong);
+ method public androidx.ink.brush.Brush.Builder setEpsilon(@FloatRange(from=0.0, fromInclusive=false, to=kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY, toInclusive=false) float epsilon);
+ method public androidx.ink.brush.Brush.Builder setFamily(androidx.ink.brush.BrushFamily family);
+ method public androidx.ink.brush.Brush.Builder setSize(@FloatRange(from=0.0, fromInclusive=false, to=kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY, toInclusive=false) float size);
+ }
+
+ public static final class Brush.Companion {
+ method public androidx.ink.brush.Brush.Builder builder();
+ method public androidx.ink.brush.Brush createWithColorIntArgb(androidx.ink.brush.BrushFamily family, @ColorInt int colorIntArgb, float size, float epsilon);
+ method public androidx.ink.brush.Brush createWithColorLong(androidx.ink.brush.BrushFamily family, @ColorLong long colorLong, float size, float epsilon);
+ }
+
+ public final class BrushFamily {
+ method protected void finalize();
+ field public static final androidx.ink.brush.BrushFamily.Companion Companion;
+ }
+
+ public static final class BrushFamily.Companion {
+ }
+
+ @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
+ }
+
+ public final class StockBrushes {
+ method public static androidx.ink.brush.BrushFamily getHighlighterLatest();
+ method public static androidx.ink.brush.BrushFamily getHighlighterV1();
+ method public static androidx.ink.brush.BrushFamily getMarkerLatest();
+ method public static androidx.ink.brush.BrushFamily getMarkerV1();
+ method public static androidx.ink.brush.BrushFamily getPressurePenLatest();
+ method public static androidx.ink.brush.BrushFamily getPressurePenV1();
+ property public static final androidx.ink.brush.BrushFamily highlighterLatest;
+ property public static final androidx.ink.brush.BrushFamily highlighterV1;
+ property public static final androidx.ink.brush.BrushFamily markerLatest;
+ property public static final androidx.ink.brush.BrushFamily markerV1;
+ property public static final androidx.ink.brush.BrushFamily pressurePenLatest;
+ property public static final androidx.ink.brush.BrushFamily pressurePenV1;
+ field public static final androidx.ink.brush.StockBrushes INSTANCE;
}
}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 3beaaaa..1137891 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -27,12 +27,20 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
}
androidXMultiplatform {
- android()
jvm()
+ androidLibrary {
+ namespace = "androidx.ink.brush"
+ withAndroidTestOnDeviceBuilder {
+ it.compilationName = "instrumentedTest"
+ it.defaultSourceSetName = "androidInstrumentedTest"
+ it.sourceSetTreeName = "test"
+ }
+ compileSdk = 35
+ aarMetadata.minCompileSdk = 35
+ }
defaultPlatform(PlatformIdentifier.JVM)
@@ -47,13 +55,17 @@
implementation(libs.kotlinStdlib)
api(libs.androidx.annotation)
implementation(project(":collection:collection"))
+ implementation(project(":ink:ink-geometry"))
+ implementation(project(":ink:ink-nativeloader"))
}
}
jvmAndroidTest {
dependsOn(commonTest)
dependencies {
+ implementation(libs.junit)
implementation(libs.kotlinTest)
+ implementation(libs.truth)
}
}
@@ -64,7 +76,12 @@
androidInstrumentedTest {
dependsOn(jvmAndroidTest)
dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testRules)
implementation(libs.testRunner)
+ implementation(libs.espressoCore)
+ implementation(libs.junit)
+ implementation(libs.truth)
}
}
@@ -78,11 +95,6 @@
}
}
-android {
- compileSdk 35
- namespace = "androidx.ink.brush"
-}
-
androidx {
name = "Ink Brush"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/ink/ink-brush/lint-baseline.xml b/ink/ink-brush/lint-baseline.xml
new file mode 100644
index 0000000..73ba869
--- /dev/null
+++ b/ink/ink-brush/lint-baseline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
+
+ <issue
+ id="Range"
+ message="Expected length ≥ 1 (was 0)"
+ errorLine1=" Rgb("", FloatArray(6), WhitePoint(0f, 0f), sIdentity, sIdentity, 0.0f, 1.0f)"
+ errorLine2=" ~~">
+ <location
+ file="src/jvmAndroidTest/kotlin/androidx/ink/brush/color/ColorSpaceTest.kt"/>
+ </issue>
+
+</issues>
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
new file mode 100644
index 0000000..b6b5bea
--- /dev/null
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import android.graphics.Color as AndroidColor
+import android.graphics.ColorSpace as AndroidColorSpace
+import android.os.Build
+import androidx.annotation.ColorLong
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class BrushExtensionsTest {
+ private val displayP3 = AndroidColorSpace.get(AndroidColorSpace.Named.DISPLAY_P3)
+ private val adobeRgb = AndroidColorSpace.get(AndroidColorSpace.Named.ADOBE_RGB)
+
+ private val testColor = AndroidColor.valueOf(0.4f, 0.6f, 0.8f, 0.2f, displayP3)
+ @ColorLong private val testColorLong = testColor.pack()
+
+ @OptIn(ExperimentalInkCustomBrushApi::class)
+ private val testFamily = BrushFamily(uri = "/brush-family:pencil")
+
+ @Test
+ fun brushGetAndroidColor_getsCorrectColor() {
+ val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
+
+ // Note that expectedColor is not necessarily the same as testColor, because of precision
+ // loss
+ // when converting from testColor to testColorLong (which is necessary, because Brush stores
+ // the
+ // color internally as a ColorLong anyway).
+ val expectedColor = AndroidColor.valueOf(testColorLong)
+ assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+ }
+
+ @Test
+ fun brushCopyWithAndroidColor_setsColor() {
+ val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 1f, 1f)
+
+ val newBrush = brush.copyWithAndroidColor(color = testColor)
+
+ assertThat(newBrush.family).isEqualTo(brush.family)
+ assertThat(newBrush.colorLong).isEqualTo(testColorLong)
+ assertThat(newBrush.size).isEqualTo(brush.size)
+ assertThat(newBrush.epsilon).isEqualTo(brush.epsilon)
+ }
+
+ @OptIn(ExperimentalInkCustomBrushApi::class)
+ @Test
+ fun brushCopyWithAndroidColor_andOtherChangedValues_createsBrushWithColor() {
+ val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 1f, 1f)
+
+ val newBrush =
+ brush.copyWithAndroidColor(
+ color = testColor,
+ family = BrushFamily(),
+ size = 2f,
+ epsilon = 0.2f,
+ )
+
+ assertThat(newBrush.family).isEqualTo(BrushFamily())
+ assertThat(newBrush.colorLong).isEqualTo(testColorLong)
+ assertThat(newBrush.size).isEqualTo(2f)
+ assertThat(newBrush.epsilon).isEqualTo(0.2f)
+ }
+
+ @Test
+ fun brushCopyWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 1f, 1f)
+
+ val newColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
+ val newBrush = brush.copyWithAndroidColor(color = newColor)
+
+ // newColor gets converted to ColorLong (losing precision) and then to Display P3.
+ val expectedColor = AndroidColor.valueOf(newColor.pack()).convert(displayP3)
+ assertThat(newBrush.colorLong).isEqualTo(expectedColor.pack())
+ }
+
+ @Test
+ fun brushBuilderAndroidColor_setsColor() {
+ val brush =
+ Brush.builder()
+ .setFamily(testFamily)
+ .setAndroidColor(testColor)
+ .setSize(1f)
+ .setEpsilon(1f)
+ .build()
+
+ assertThat(brush.colorLong).isEqualTo(testColorLong)
+ }
+
+ @Test
+ fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
+ val brush =
+ Brush.builder()
+ .setFamily(testFamily)
+ .setAndroidColor(unsupportedColor)
+ .setSize(1f)
+ .setEpsilon(1f)
+ .build()
+
+ // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
+ val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
+ assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
+ }
+
+ @Test
+ fun brushWithAndroidColor_createsBrushWithColor() {
+ val brush = Brush.withAndroidColor(testFamily, testColor, 1f, 1f)
+ assertThat(brush.colorLong).isEqualTo(testColorLong)
+ }
+
+ @Test
+ fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
+ val brush = Brush.withAndroidColor(testFamily, unsupportedColor, 1f, 1f)
+
+ // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
+ val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
+ assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
+ }
+
+ @Test
+ fun brushUtilGetAndroidColor_getsCorrectColor() {
+ val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
+
+ // Note that expectedColor is not necessarily the same as testColor, because of precision
+ // loss
+ // when converting from testColor to testColorLong.
+ val expectedColor = AndroidColor.valueOf(testColorLong)
+ assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
+ }
+
+ @Test
+ fun brushUtilToBuilderWithAndroidColor_setsColor() {
+ val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
+
+ val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+
+ assertThat(newBrush.colorLong).isEqualTo(testColorLong)
+ assertThat(brush.family).isEqualTo(testFamily)
+ assertThat(brush.size).isEqualTo(2f)
+ assertThat(brush.epsilon).isEqualTo(0.2f)
+ }
+
+ @Test
+ fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
+
+ val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
+ val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+
+ // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
+ val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
+ assertThat(newBrush.colorLong).isEqualTo(expectedColor.pack())
+
+ assertThat(brush.family).isEqualTo(testFamily)
+ assertThat(brush.size).isEqualTo(2f)
+ assertThat(brush.epsilon).isEqualTo(0.2f)
+ }
+
+ @Test
+ fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+ val brush =
+ BrushUtil.makeBuilderWithAndroidColor(testColor)
+ .setFamily(testFamily)
+ .setSize(2f)
+ .setEpsilon(0.2f)
+ .build()
+
+ assertThat(brush.family).isEqualTo(testFamily)
+ assertThat(brush.colorLong).isEqualTo(testColorLong)
+ assertThat(brush.size).isEqualTo(2f)
+ assertThat(brush.epsilon).isEqualTo(0.2f)
+ }
+
+ @Test
+ fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
+ val brush =
+ BrushUtil.makeBuilderWithAndroidColor(unsupportedColor)
+ .setFamily(testFamily)
+ .setSize(2f)
+ .setEpsilon(0.2f)
+ .build()
+
+ // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
+ val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
+ assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
+ }
+
+ @Test
+ fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
+ val brush = BrushUtil.makeBrushWithAndroidColor(testFamily, testColor, 1f, 1f)
+ assertThat(brush.colorLong).isEqualTo(testColorLong)
+ }
+
+ @Test
+ fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
+ val brush = BrushUtil.makeBrushWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
+
+ // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
+ val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
+ assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
+ }
+}
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
new file mode 100644
index 0000000..c7dd5f7
--- /dev/null
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+
+package androidx.ink.brush
+
+import android.graphics.Color as AndroidColor
+import android.os.Build
+import androidx.annotation.CheckResult
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+
+/**
+ * The brush color as an [android.graphics.Color] instance, which can express colors in several
+ * different color spaces. sRGB and Display P3 are supported; a color in any other color space will
+ * be converted to Display P3.
+ */
+@JvmSynthetic
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+
+/**
+ * Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
+ * unchanged. The color is specified as an [android.graphics.Color] instance, which can encode
+ * several different color spaces. sRGB and Display P3 are supported; a color in any other color
+ * space will be converted to Display P3.
+ */
+@JvmSynthetic
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.copyWithAndroidColor(
+ color: AndroidColor,
+ family: BrushFamily = this.family,
+ size: Float = this.size,
+ epsilon: Float = this.epsilon,
+): Brush = copyWithColorLong(color.pack(), family, size, epsilon)
+
+/**
+ * Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
+ * are supported; a color in any other color space will be converted to Display P3.
+ */
+@JvmSynthetic
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
+ setColorLong(color.pack())
+
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ */
+@JvmSynthetic
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.Companion.withAndroidColor(
+ family: BrushFamily,
+ color: AndroidColor,
+ size: Float,
+ epsilon: Float,
+): Brush = BrushUtil.makeBrushWithAndroidColor(family, color, size, epsilon)
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+public object BrushUtil {
+
+ /**
+ * The brush color as an [android.graphics.Color] instance, which can express colors in several
+ * different color spaces. sRGB and Display P3 are supported; a color in any other color space
+ * will be converted to Display P3.
+ */
+ @JvmStatic
+ @CheckResult
+ @RequiresApi(Build.VERSION_CODES.O)
+ public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
+
+ /**
+ * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3.
+ * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ */
+ @JvmStatic
+ @CheckResult
+ @RequiresApi(Build.VERSION_CODES.O)
+ public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
+ brush.toBuilder().setAndroidColor(color)
+
+ /**
+ * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
+ * a color in any other color space will be converted to Display P3.
+ */
+ @JvmStatic
+ @CheckResult
+ @RequiresApi(Build.VERSION_CODES.O)
+ public fun makeBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ Brush.Builder().setAndroidColor(color)
+
+ /**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
+ * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
+ * other color space will be converted to Display P3.
+ */
+ @JvmStatic
+ @CheckResult
+ @RequiresApi(Build.VERSION_CODES.O)
+ public fun makeBrushWithAndroidColor(
+ family: BrushFamily,
+ color: AndroidColor,
+ size: Float,
+ epsilon: Float,
+ ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/Brush.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/Brush.kt
index e1d6502..6ddee13 100644
--- a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/Brush.kt
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/Brush.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,365 @@
package androidx.ink.brush
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorLong
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
import androidx.ink.brush.color.Color as ComposeColor
+import androidx.ink.brush.color.toArgb
+import androidx.ink.nativeloader.NativeLoader
+import kotlin.Float
+import kotlin.jvm.JvmStatic
/**
* Defines how stroke inputs are interpreted to create the visual representation of a stroke.
*
- * A [Brush] completely describes how inputs are used to create stroke meshes, and how those meshes
- * should be drawn by stroke renderers.
- *
- * Note: This is a placeholder implementation/API until the real code is migrated here.
- *
- * @param color The color of the stroke.
- * @param size The overall thickness of strokes created with a given brush, in the same units as the
- * stroke coordinate system.
+ * The type completely describes how inputs are used to create stroke meshes, and how those meshes
+ * should be drawn by stroke renderers. In an analogous way to "font" and "font family", a [Brush]
+ * can be considered an instance of a [BrushFamily] with a particular [color], [size], and an extra
+ * parameter controlling visual fidelity, called [epsilon].
*/
-public class Brush(public val color: Long, public val size: Float) {
- private val colorObj = ComposeColor(color.toULong())
+@Suppress("NotCloseable") // Finalize is only used to free the native peer.
+public class Brush
+internal constructor(
+ /** The [BrushFamily] for this brush. See [StockBrushes] for available [BrushFamily] values. */
+ public val family: BrushFamily,
+ composeColor: ComposeColor,
+ /**
+ * The overall thickness of strokes created with a given brush, in the same units as the stroke
+ * coordinate system. This must be at least as big as [epsilon].
+ */
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false
+ )
+ public val size: Float,
+ /**
+ * The smallest distance for which two points should be considered visually distinct for stroke
+ * generation geometry purposes. Effectively, it is the visual fidelity of strokes created with
+ * this brush, where any (lack of) visual fidelity can be observed by a user the further zoomed
+ * in they are on the stroke. Lower values of [epsilon] result in higher fidelity strokes at the
+ * cost of somewhat higher memory usage. This value, like [size], is in the same units as the
+ * stroke coordinate system. A size of 0.1 physical pixels at the default zoom level is a good
+ * starting point that can tolerate a reasonable amount of zooming in with high quality visual
+ * results.
+ */
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false
+ )
+ public val epsilon: Float,
+) {
+
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public val composeColor: ComposeColor = composeColor.toColorInInkSupportedColorSpace()
+
+ /**
+ * The default color of a [Brush] is pure black. To set a custom color, use
+ * [createWithColorLong] or [createWithColorIntArgb].
+ */
+ public constructor(
+ family: BrushFamily,
+ size: Float,
+ epsilon: Float,
+ ) : this(family, DEFAULT_COMPOSE_COLOR, size, epsilon)
+
+ /**
+ * The brush color as a [ColorLong], which can express colors in several different color spaces.
+ * sRGB and Display P3 are supported; a color in any other color space will be converted to
+ * Display P3.
+ */
+ public val colorLong: Long
+ @ColorLong get(): Long = composeColor.value.toLong()
+
+ /**
+ * The brush color as a [ColorInt], which can only express colors in the sRGB color space. For
+ * clients that want to support wide-gamut colors, use [colorLong].
+ */
+ public val colorIntArgb: Int
+ @ColorInt get(): Int = composeColor.toArgb()
+
+ /** A handle to the underlying native [Brush] object. */
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public val nativePointer: Long =
+ nativeCreateBrush(
+ family.nativePointer,
+ this.composeColor.red,
+ this.composeColor.green,
+ this.composeColor.blue,
+ this.composeColor.alpha,
+ this.composeColor.colorSpace.toInkColorSpaceId(),
+ size,
+ epsilon,
+ )
+
+ // Base implementation of copy() that all public versions call.
+ private fun copy(family: BrushFamily, color: ComposeColor, size: Float, epsilon: Float): Brush {
+ return if (
+ family == this.family &&
+ color == this.composeColor &&
+ size == this.size &&
+ epsilon == this.epsilon
+ ) {
+ // For a pure copy, return the same object, since it is immutable.
+ this
+ } else {
+ Brush(family, color, size, epsilon)
+ }
+ }
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged. To change the color, use [copyWithColorLong] or [copyWithColorIntArgb].
+ */
+ @JvmOverloads
+ public fun copy(
+ family: BrushFamily = this.family,
+ size: Float = this.size,
+ epsilon: Float = this.epsilon,
+ ): Brush = copy(family, this.composeColor, size, epsilon)
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged. The color is specified as a [ColorLong], which can encode several different color
+ * spaces. sRGB and Display P3 are supported; a color in any other color space will be converted
+ * to Display P3.
+ *
+ * Some libraries (notably Jetpack UI Graphics) use [ULong] for [ColorLong]s, so the caller must
+ * call [ULong.toLong] on such a value before passing it to this method.
+ */
+ @JvmOverloads
+ public fun copyWithColorLong(
+ @ColorLong colorLong: Long,
+ family: BrushFamily = this.family,
+ size: Float = this.size,
+ epsilon: Float = this.epsilon,
+ ): Brush = copy(family, ComposeColor(colorLong.toULong()), size, epsilon)
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged. The color is specified as a [ColorInt], which is in the sRGB color space by
+ * definition. Note that the [ColorInt] channel order puts alpha first (in the most significant
+ * byte).
+ *
+ * Kotlin interprets integer literals greater than `0x7fffffff` as [Long]s, so callers that want
+ * to specify a literal [ColorInt] with alpha >= 0x80 must call [Long.toInt] on the literal.
+ */
+ @JvmOverloads
+ public fun copyWithColorIntArgb(
+ @ColorInt colorIntArgb: Int,
+ family: BrushFamily = this.family,
+ size: Float = this.size,
+ epsilon: Float = this.epsilon,
+ ): Brush = copy(family, ComposeColor(colorIntArgb), size, epsilon)
+
+ /**
+ * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned
+ * builder to build a copy of a Brush. Kotlin developers, see [copy] method.
+ */
+ public fun toBuilder(): Builder =
+ Builder().setFamily(family).setComposeColor(composeColor).setSize(size).setEpsilon(epsilon)
+
+ /**
+ * Builder for [Brush].
+ *
+ * Use Brush.Builder to construct a [Brush] with default values, overriding only as needed.
+ */
+ public class Builder {
+ private var family: BrushFamily? = null
+ private var composeColor: ComposeColor = DEFAULT_COMPOSE_COLOR
+
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ private var size: Float? = null
+
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ private var epsilon: Float? = null
+
+ /**
+ * Sets the [BrushFamily] for this brush. See [StockBrushes] for available [BrushFamily]
+ * values.
+ */
+ public fun setFamily(family: BrushFamily): Builder {
+ this.family = family
+ return this
+ }
+
+ internal fun setComposeColor(color: ComposeColor): Builder {
+ this.composeColor = color
+ return this
+ }
+
+ /**
+ * Sets the color using a [ColorLong], which can encode several different color spaces. sRGB
+ * and Display P3 are supported; a color in any other color space will be converted to
+ * Display P3.
+ *
+ * Some libraries (notably Jetpack UI Graphics) use [ULong] for [ColorLong]s, so the caller
+ * must call [ULong.toLong] on such a value before passing it to this method.
+ */
+ public fun setColorLong(@ColorLong colorLong: Long): Builder {
+ this.composeColor = ComposeColor(colorLong.toULong())
+ return this
+ }
+
+ /**
+ * Sets the color using a [ColorInt], which is in the sRGB color space by definition. Note
+ * that the [ColorInt] channel order puts alpha first (in the most significant byte).
+ *
+ * Kotlin interprets integer literals greater than `0x7fffffff` as [Long]s, so Kotlin
+ * callers that want to specify a literal [ColorInt] with alpha >= 0x80 must call
+ * [Long.toInt] on the literal.
+ */
+ public fun setColorIntArgb(@ColorInt colorIntArgb: Int): Builder {
+ this.composeColor = ComposeColor(colorIntArgb)
+ return this
+ }
+
+ public fun setSize(
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ size: Float
+ ): Builder {
+ this.size = size
+ return this
+ }
+
+ public fun setEpsilon(
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ epsilon: Float
+ ): Builder {
+ this.epsilon = epsilon
+ return this
+ }
+
+ public fun build(): Brush =
+ Brush(
+ family =
+ checkNotNull(family) {
+ "brush family must be specified before calling build()"
+ },
+ composeColor = composeColor,
+ size = checkNotNull(size) { "brush size must be specified before calling build()" },
+ epsilon =
+ checkNotNull(epsilon) {
+ "brush epsilon must be specified before calling build()"
+ },
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Brush) return false
+
+ if (family != other.family) return false
+ if (composeColor != other.composeColor) return false
+ if (size != other.size) return false
+ if (epsilon != other.epsilon) return false
+
+ return true
+ }
+
+ // NOMUTANTS -- not testing exact hashCode values, just that equality implies the same hashCode.
+ override fun hashCode(): Int {
+ var result = family.hashCode()
+ result = 31 * result + composeColor.hashCode()
+ result = 31 * result + size.hashCode()
+ result = 31 * result + epsilon.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "Brush(family=$family, color=$composeColor, size=$size, epsilon=$epsilon)"
+ }
+
+ /** Delete native Brush memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeBrush(nativePointer)
+ }
+
+ /** Create underlying native object and return reference for all subsequent native calls. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeCreateBrush(
+ familyNativePointer: Long,
+ colorRed: Float,
+ colorGreen: Float,
+ colorBlue: Float,
+ colorAlpha: Float,
+ colorSpace: Int,
+ size: Float,
+ epsilon: Float,
+ ): Long
+
+ /** Release the underlying memory allocated in [nativeCreateBrush]. */
+ private external fun nativeFreeBrush(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ public companion object {
+ init {
+ NativeLoader.load()
+ }
+
+ private val DEFAULT_COMPOSE_COLOR = ComposeColor.Black
+
+ /**
+ * Returns a new [Brush] with the color specified by a [ColorLong], which can encode several
+ * different color spaces. sRGB and Display P3 are supported; a color in any other color
+ * space will be converted to Display P3.
+ *
+ * Some libraries (notably Jetpack UI Graphics) use [ULong] for [ColorLong]s, so the caller
+ * must call [ULong.toLong] on such a value before passing it to this method.
+ */
+ @JvmStatic
+ public fun createWithColorLong(
+ family: BrushFamily,
+ @ColorLong colorLong: Long,
+ size: Float,
+ epsilon: Float,
+ ): Brush = Brush(family, ComposeColor(colorLong.toULong()), size, epsilon)
+
+ /**
+ * Returns a new [Brush] with the color specified by a [ColorInt], which is in the sRGB
+ * color space by definition. Note that the [ColorInt] channel order puts alpha first (in
+ * the most significant byte).
+ *
+ * Kotlin interprets integer literals greater than `0x7fffffff` as [Long]s, so callers that
+ * want to specify a literal [ColorInt] with alpha >= 0x80 must call [Long.toInt] on the
+ * literal.
+ */
+ @JvmStatic
+ public fun createWithColorIntArgb(
+ family: BrushFamily,
+ @ColorInt colorIntArgb: Int,
+ size: Float,
+ epsilon: Float,
+ ): Brush = Brush(family, ComposeColor(colorIntArgb), size, epsilon)
+
+ /** Returns a new [Brush.Builder]. */
+ @JvmStatic public fun builder(): Builder = Builder()
+ }
}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt
new file mode 100644
index 0000000..3b7d766
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt
@@ -0,0 +1,1485 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.RestrictTo
+import androidx.ink.nativeloader.NativeLoader
+import java.util.Collections.unmodifiableList
+import java.util.Collections.unmodifiableSet
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
+import kotlin.reflect.KClass
+
+/**
+ * A behavior describing how stroke input properties should affect the shape and color of the brush
+ * tip.
+ *
+ * The behavior is conceptually a graph made from the various node types defined below. Each edge of
+ * the graph represents passing a nullable floating point value between nodes, and each node in the
+ * graph fits into one of the following categories:
+ * 1. Leaf nodes generate an output value without graph inputs. For example, they can create a value
+ * from properties of stroke input.
+ * 2. Filter nodes can conditionally toggle branches of the graph "on" by outputting their input
+ * value, or "off" by outputting a null value.
+ * 3. Operator nodes take in one or more input values and generate an output. For example, by
+ * mapping input to output with an easing function.
+ * 4. Target nodes apply an input value to chosen properties of the brush tip.
+ *
+ * For each input in a stroke, [BrushTip.behaviors] are applied as follows:
+ * 1. The actual target modifier (as calculated above) for each tip property is accumulated from
+ * every [BrushBehavior] present on the current [BrushTip]. Multiple behaviors can affect the
+ * same [Target]. Depending on the [Target], modifiers from multiple behaviors will stack either
+ * additively or multiplicatively, according to the documentation for that [Target]. Regardless,
+ * the order of specified behaviors does not affect the result.
+ * 2. The modifiers are applied to the shape and color shift values of the tip's state according to
+ * the documentation for each [Target]. The resulting tip property values are then clamped or
+ * normalized to within their valid range of values. E.g. the final value of
+ * [BrushTip.cornerRounding] will be clamped within [0, 1]. Generally: The affected shape values
+ * are those found in [BrushTip] members. The color shift values remain in the range -100% to
+ * +100%. Note that when stored on a vertex, the color shift is encoded such that each channel is
+ * in the range [0, 1], where 0.5 represents a 0% shift.
+ *
+ * Note that the accumulated tip shape property modifiers may be adjusted by the implementation
+ * before being applied: The rates of change of shape properties may be constrained to keep them
+ * from changing too rapidly with respect to distance traveled from one input to the next.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@ExperimentalInkCustomBrushApi
+// NotCloseable: Finalize is only used to free the native peer.
+// Deprecation: b/356424519 Migrate to targetNodes
+@Suppress("NotCloseable", "DEPRECATION")
+public class BrushBehavior(
+ // The [targetNodes] val below is a defensive copy of this parameter.
+ targetNodes: List<TargetNode>
+) {
+ public val targetNodes: List<TargetNode> = unmodifiableList(targetNodes.toList())
+
+ /** A handle to the underlying native [BrushBehavior] object. */
+ internal val nativePointer: Long = createNativeBrushBehavior(targetNodes)
+
+ /**
+ * Constructs a simple [BrushBehavior] using whatever [Node]s are necessary for the specified
+ * fields.
+ */
+ public constructor(
+ source: Source,
+ target: Target,
+ sourceValueRangeLowerBound: Float,
+ sourceValueRangeUpperBound: Float,
+ targetModifierRangeLowerBound: Float,
+ targetModifierRangeUpperBound: Float,
+ sourceOutOfRangeBehavior: OutOfRange = OutOfRange.CLAMP,
+ responseCurve: EasingFunction = EasingFunction.Predefined.LINEAR,
+ responseTimeMillis: Long = 0L,
+ enabledToolTypes: Set<InputToolType> = ALL_TOOL_TYPES,
+ isFallbackFor: OptionalInputProperty? = null,
+ ) : this(
+ run<List<TargetNode>> {
+ var node: ValueNode =
+ SourceNode(
+ source,
+ sourceValueRangeLowerBound,
+ sourceValueRangeUpperBound,
+ sourceOutOfRangeBehavior,
+ )
+ if (enabledToolTypes != ALL_TOOL_TYPES) {
+ node = ToolTypeFilterNode(enabledToolTypes, node)
+ }
+ if (isFallbackFor != null) {
+ node = FallbackFilterNode(isFallbackFor, node)
+ }
+ // [EasingFunction.Predefined.LINEAR] is the identity function, so no need to add a
+ // [ResponseNode] with that function.
+ if (responseCurve != EasingFunction.Predefined.LINEAR) {
+ node = ResponseNode(responseCurve, node)
+ }
+ if (responseTimeMillis != 0L) {
+ node =
+ DampingNode(
+ DampingSource.TIME_IN_SECONDS,
+ responseTimeMillis.toFloat() / 1000.0f,
+ node
+ )
+ }
+ listOf(
+ TargetNode(
+ target,
+ targetModifierRangeLowerBound,
+ targetModifierRangeUpperBound,
+ node
+ )
+ )
+ }
+ )
+
+ /**
+ * Returns a node in the behavior of the given [Node] subclass, matching the given predicate (if
+ * any).
+ */
+ // TODO: b/356424519 - Remove this method once the below legacy properties are removed.
+ private fun <T : Node> findNode(
+ nodeClass: KClass<T>,
+ predicate: (T) -> Boolean = { true }
+ ): T? {
+ val stack = ArrayDeque<Node>(targetNodes)
+ while (!stack.isEmpty()) {
+ val node = stack.removeLast()
+ // [KClass.safeCast] is apparently discouraged on Android for performance reasons.
+ if (nodeClass.isInstance(node)) {
+ @Suppress("UNCHECKED_CAST") // cast is protected by enclosing if statement
+ val result = node as T
+ if (predicate(result)) return result
+ }
+ stack.addAll(node.inputs())
+ }
+ return null
+ }
+
+ // The below properties are implemented so as to give the correct answer in cases where the
+ // [BrushBehavior] was created using the legacy convenience constructor, and to give _some_ kind
+ // of plausible answer in the more general case of a [BrushBehavior] created with an arbitrary
+ // node graph. Once existing callers of these properties are migrated to working with [Node]s
+ // instead, we can remove them.
+ //
+ // TODO: b/356424519 - Remove the below getters once we no longer need them.
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val source: Source
+ get() = findNode(SourceNode::class)?.source ?: Source.NORMALIZED_PRESSURE
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val target: Target
+ get() = findNode(TargetNode::class)?.target ?: Target.SIZE_MULTIPLIER
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val sourceValueRangeLowerBound: Float
+ get() = findNode(SourceNode::class)?.sourceValueRangeLowerBound ?: 0.0f
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val sourceValueRangeUpperBound: Float
+ get() = findNode(SourceNode::class)?.sourceValueRangeUpperBound ?: 1.0f
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val targetModifierRangeLowerBound: Float
+ get() = findNode(TargetNode::class)?.targetModifierRangeLowerBound ?: 0.0f
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val targetModifierRangeUpperBound: Float
+ get() = findNode(TargetNode::class)?.targetModifierRangeUpperBound ?: 1.0f
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val sourceOutOfRangeBehavior: OutOfRange
+ get() = findNode(SourceNode::class)?.sourceOutOfRangeBehavior ?: OutOfRange.CLAMP
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val responseCurve: EasingFunction
+ get() = findNode(ResponseNode::class)?.responseCurve ?: EasingFunction.Predefined.LINEAR
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val responseTimeMillis: Long
+ get() =
+ ((findNode(DampingNode::class, { it.dampingSource == DampingSource.TIME_IN_SECONDS })
+ ?.dampingGap ?: 0.0f) * 1000.0f)
+ .toLong()
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val enabledToolTypes: Set<InputToolType>
+ get() = findNode(ToolTypeFilterNode::class)?.enabledToolTypes ?: ALL_TOOL_TYPES
+
+ @Deprecated("Prefer using targetNodes instead.")
+ public val isFallbackFor: OptionalInputProperty?
+ get() = findNode(FallbackFilterNode::class)?.isFallbackFor
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @JvmSynthetic
+ public fun copy(
+ source: Source = this.source,
+ target: Target = this.target,
+ sourceOutOfRangeBehavior: OutOfRange = this.sourceOutOfRangeBehavior,
+ sourceValueRangeLowerBound: Float = this.sourceValueRangeLowerBound,
+ sourceValueRangeUpperBound: Float = this.sourceValueRangeUpperBound,
+ targetModifierRangeLowerBound: Float = this.targetModifierRangeLowerBound,
+ targetModifierRangeUpperBound: Float = this.targetModifierRangeUpperBound,
+ responseCurve: EasingFunction = this.responseCurve,
+ responseTimeMillis: Long = this.responseTimeMillis,
+ enabledToolTypes: Set<InputToolType> = this.enabledToolTypes,
+ isFallbackFor: OptionalInputProperty? = this.isFallbackFor,
+ ): BrushBehavior =
+ BrushBehavior(
+ source,
+ target,
+ sourceValueRangeLowerBound,
+ sourceValueRangeUpperBound,
+ targetModifierRangeLowerBound,
+ targetModifierRangeUpperBound,
+ sourceOutOfRangeBehavior,
+ responseCurve,
+ responseTimeMillis,
+ enabledToolTypes,
+ isFallbackFor,
+ )
+
+ /**
+ * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned
+ * builder to build a copy of a BrushBehavior.
+ */
+ public fun toBuilder(): Builder =
+ Builder()
+ .setSource(source)
+ .setTarget(target)
+ .setSourceOutOfRangeBehavior(sourceOutOfRangeBehavior)
+ .setSourceValueRangeLowerBound(sourceValueRangeLowerBound)
+ .setSourceValueRangeUpperBound(sourceValueRangeUpperBound)
+ .setTargetModifierRangeLowerBound(targetModifierRangeLowerBound)
+ .setTargetModifierRangeUpperBound(targetModifierRangeUpperBound)
+ .setResponseCurve(responseCurve)
+ .setResponseTimeMillis(responseTimeMillis)
+ .setEnabledToolTypes(enabledToolTypes)
+ .setIsFallbackFor(isFallbackFor)
+
+ /**
+ * Builder for [BrushBehavior].
+ *
+ * For Java developers, use BrushBehavior.Builder to construct a [BrushBehavior] with default
+ * values, overriding only as needed. For example:
+ * ```
+ * BrushBehavior behavior = new BrushBehavior.Builder()
+ * .setSource(...)
+ * .setTarget(...)
+ * .setSourceOutOfRangeBehavior(...)
+ * .setSourceValueRangeLowerBound(...)
+ * .build();
+ * ```
+ */
+ @Suppress("ScopeReceiverThis")
+ public class Builder {
+ private var source: Source = Source.NORMALIZED_PRESSURE
+ private var target: Target = Target.SIZE_MULTIPLIER
+ private var sourceOutOfRangeBehavior: OutOfRange = OutOfRange.CLAMP
+ private var sourceValueRangeLowerBound: Float = 0f
+ private var sourceValueRangeUpperBound: Float = 1f
+ private var targetModifierRangeLowerBound: Float = 0f
+ private var targetModifierRangeUpperBound: Float = 1f
+ private var responseCurve: EasingFunction = EasingFunction.Predefined.LINEAR
+ private var responseTimeMillis: Long = 0L
+ private var enabledToolTypes: Set<InputToolType> = ALL_TOOL_TYPES
+ private var isFallbackFor: OptionalInputProperty? = null
+
+ public fun setSource(source: Source): Builder = apply { this.source = source }
+
+ public fun setTarget(target: Target): Builder = apply { this.target = target }
+
+ public fun setSourceOutOfRangeBehavior(sourceOutOfRangeBehavior: OutOfRange): Builder =
+ apply {
+ this.sourceOutOfRangeBehavior = sourceOutOfRangeBehavior
+ }
+
+ public fun setSourceValueRangeLowerBound(sourceValueRangeLowerBound: Float): Builder =
+ apply {
+ this.sourceValueRangeLowerBound = sourceValueRangeLowerBound
+ }
+
+ public fun setSourceValueRangeUpperBound(sourceValueRangeUpperBound: Float): Builder =
+ apply {
+ this.sourceValueRangeUpperBound = sourceValueRangeUpperBound
+ }
+
+ public fun setTargetModifierRangeLowerBound(targetModifierRangeLowerBound: Float): Builder =
+ apply {
+ this.targetModifierRangeLowerBound = targetModifierRangeLowerBound
+ }
+
+ public fun setTargetModifierRangeUpperBound(targetModifierRangeUpperBound: Float): Builder =
+ apply {
+ this.targetModifierRangeUpperBound = targetModifierRangeUpperBound
+ }
+
+ public fun setResponseCurve(responseCurve: EasingFunction): Builder = apply {
+ this.responseCurve = responseCurve
+ }
+
+ public fun setResponseTimeMillis(responseTimeMillis: Long): Builder = apply {
+ this.responseTimeMillis = responseTimeMillis
+ }
+
+ public fun setEnabledToolTypes(enabledToolTypes: Set<InputToolType>): Builder = apply {
+ this.enabledToolTypes = enabledToolTypes.toSet()
+ }
+
+ public fun setIsFallbackFor(isFallbackFor: OptionalInputProperty?): Builder = apply {
+ this.isFallbackFor = isFallbackFor
+ }
+
+ public fun build(): BrushBehavior =
+ BrushBehavior(
+ source,
+ target,
+ sourceValueRangeLowerBound,
+ sourceValueRangeUpperBound,
+ targetModifierRangeLowerBound,
+ targetModifierRangeUpperBound,
+ sourceOutOfRangeBehavior,
+ responseCurve,
+ responseTimeMillis,
+ enabledToolTypes,
+ isFallbackFor,
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ // NOMUTANTS -- Check the instance first to short circuit faster.
+ if (other === this) return true
+ if (other == null || other !is BrushBehavior) return false
+ return (source == other.source &&
+ target == other.target &&
+ sourceOutOfRangeBehavior == other.sourceOutOfRangeBehavior &&
+ sourceValueRangeLowerBound == other.sourceValueRangeLowerBound &&
+ sourceValueRangeUpperBound == other.sourceValueRangeUpperBound &&
+ targetModifierRangeLowerBound == other.targetModifierRangeLowerBound &&
+ targetModifierRangeUpperBound == other.targetModifierRangeUpperBound &&
+ responseCurve == other.responseCurve &&
+ responseTimeMillis == other.responseTimeMillis &&
+ enabledToolTypes == other.enabledToolTypes &&
+ isFallbackFor == other.isFallbackFor)
+ }
+
+ override fun hashCode(): Int {
+ var result = source.hashCode()
+ result = 31 * result + target.hashCode()
+ result = 31 * result + sourceOutOfRangeBehavior.hashCode()
+ result = 31 * result + sourceValueRangeLowerBound.hashCode()
+ result = 31 * result + sourceValueRangeUpperBound.hashCode()
+ result = 31 * result + targetModifierRangeLowerBound.hashCode()
+ result = 31 * result + targetModifierRangeUpperBound.hashCode()
+ result = 31 * result + responseCurve.hashCode()
+ result = 31 * result + responseTimeMillis.hashCode()
+ result = 31 * result + (isFallbackFor?.hashCode() ?: 0)
+ result = 31 * result + enabledToolTypes.hashCode()
+ return result
+ }
+
+ override fun toString(): String =
+ "BrushBehavior(source=$source, target=$target, " +
+ "sourceOutOfRangeBehavior=$sourceOutOfRangeBehavior, " +
+ "sourceValueRangeLowerBound=$sourceValueRangeLowerBound, " +
+ "sourceValueRangeUpperBound=$sourceValueRangeUpperBound, " +
+ "targetModifierRangeLowerBound=$targetModifierRangeLowerBound, " +
+ "targetModifierRangeUpperBound=$targetModifierRangeUpperBound, " +
+ "responseCurve=$responseCurve, responseTimeMillis=$responseTimeMillis, " +
+ "enabledToolTypes=$enabledToolTypes, isFallbackFor=$isFallbackFor)"
+
+ /** Delete native BrushBehavior memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeBrushBehavior(nativePointer)
+ }
+
+ private fun createNativeBrushBehavior(targetNodes: List<TargetNode>): Long {
+ // TODO: b/356424519 - Use dup/swap nodes to avoid repeating common subexpressions.
+ val orderedNodes = ArrayDeque<Node>()
+ val stack = ArrayDeque<Node>(targetNodes)
+ while (!stack.isEmpty()) {
+ stack.removeLast().let { node ->
+ orderedNodes.addFirst(node)
+ stack.addAll(node.inputs())
+ }
+ }
+
+ val nativePointer = nativeCreateEmptyBrushBehavior()
+ for (node in orderedNodes) {
+ node.appendToNativeBrushBehavior(nativePointer)
+ }
+ return nativeValidateOrDeleteAndThrow(nativePointer)
+ }
+
+ /** Creates an underlying native brush behavior with no nodes and returns its memory address. */
+ private external fun nativeCreateEmptyBrushBehavior():
+ Long // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /**
+ * Validates a native `BrushBehavior` and returns the pointer back, or deletes the native
+ * `BrushBehavior` and throws an exception if it's not valid.
+ */
+ private external fun nativeValidateOrDeleteAndThrow(
+ nativePointer: Long
+ ): Long // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /**
+ * Release the underlying memory allocated in [nativeCreateBrushBehaviorLinear],
+ * [nativeCreateBrushBehaviorPredefined], [nativeCreateBrushBehaviorSteps], or
+ * [nativeCreateBrushBehaviorCubicBezier].
+ */
+ private external fun nativeFreeBrushBehavior(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ public companion object {
+ init {
+ NativeLoader.load()
+ }
+
+ /** Returns a new [BrushBehavior.Builder]. */
+ @JvmStatic public fun builder(): Builder = Builder()
+
+ @JvmField
+ public val ALL_TOOL_TYPES: Set<InputToolType> =
+ setOf(
+ InputToolType.STYLUS,
+ InputToolType.UNKNOWN,
+ InputToolType.MOUSE,
+ InputToolType.TOUCH
+ )
+ }
+
+ /**
+ * List of input properties along with their units that can act as sources for a
+ * [BrushBehavior].
+ */
+ public class Source private constructor(@JvmField internal val value: Int) {
+ public fun toSimpleString(): String =
+ when (this) {
+ CONSTANT_ZERO -> "CONSTANT_ZERO"
+ NORMALIZED_PRESSURE -> "NORMALIZED_PRESSURE"
+ TILT_IN_RADIANS -> "TILT_IN_RADIANS"
+ TILT_X_IN_RADIANS -> "TILT_X_IN_RADIANS"
+ TILT_Y_IN_RADIANS -> "TILT_Y_IN_RADIANS"
+ ORIENTATION_IN_RADIANS -> "ORIENTATION_IN_RADIANS"
+ ORIENTATION_ABOUT_ZERO_IN_RADIANS -> "ORIENTATION_ABOUT_ZERO_IN_RADIANS"
+ SPEED_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND ->
+ "SPEED_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND"
+ VELOCITY_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND ->
+ "VELOCITY_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND"
+ VELOCITY_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND ->
+ "VELOCITY_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND"
+ DIRECTION_IN_RADIANS -> "DIRECTION_IN_RADIANS"
+ DIRECTION_ABOUT_ZERO_IN_RADIANS -> "DIRECTION_ABOUT_ZERO_IN_RADIANS"
+ NORMALIZED_DIRECTION_X -> "NORMALIZED_DIRECTION_X"
+ NORMALIZED_DIRECTION_Y -> "NORMALIZED_DIRECTION_Y"
+ DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE"
+ TIME_OF_INPUT_IN_SECONDS -> "TIME_OF_INPUT_IN_SECONDS"
+ TIME_OF_INPUT_IN_MILLIS -> "TIME_OF_INPUT_IN_MILLIS"
+ PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE"
+ PREDICTED_TIME_ELAPSED_IN_SECONDS -> "PREDICTED_TIME_ELAPSED_IN_SECONDS"
+ PREDICTED_TIME_ELAPSED_IN_MILLIS -> "PREDICTED_TIME_ELAPSED_IN_MILLIS"
+ DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE"
+ TIME_SINCE_INPUT_IN_SECONDS -> "TIME_SINCE_INPUT_IN_SECONDS"
+ TIME_SINCE_INPUT_IN_MILLIS -> "TIME_SINCE_INPUT_IN_MILLIS"
+ ACCELERATION_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED ->
+ "ACCELERATION_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ ACCELERATION_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED ->
+ "ACCELERATION_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ ACCELERATION_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED ->
+ "ACCELERATION_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ ACCELERATION_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED ->
+ "ACCELERATION_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ ACCELERATION_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED ->
+ "ACCELERATION_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ INPUT_SPEED_IN_CENTIMETERS_PER_SECOND -> "INPUT_SPEED_IN_CENTIMETERS_PER_SECOND"
+ INPUT_VELOCITY_X_IN_CENTIMETERS_PER_SECOND ->
+ "INPUT_VELOCITY_X_IN_CENTIMETERS_PER_SECOND"
+ INPUT_VELOCITY_Y_IN_CENTIMETERS_PER_SECOND ->
+ "INPUT_VELOCITY_Y_IN_CENTIMETERS_PER_SECOND"
+ INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS -> "INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS"
+ PREDICTED_INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS ->
+ "PREDICTED_INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS"
+ INPUT_ACCELERATION_IN_CENTIMETERS_PER_SECOND_SQUARED ->
+ "INPUT_ACCELERATION_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ INPUT_ACCELERATION_X_IN_CENTIMETERS_PER_SECOND_SQUARED ->
+ "INPUT_ACCELERATION_X_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ INPUT_ACCELERATION_Y_IN_CENTIMETERS_PER_SECOND_SQUARED ->
+ "INPUT_ACCELERATION_Y_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ INPUT_ACCELERATION_FORWARD_IN_CENTIMETERS_PER_SECOND_SQUARED ->
+ "INPUT_ACCELERATION_FORWARD_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED ->
+ "INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is Source) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+
+ /**
+ * A source whose value is always zero. This can be used to provide a constant modifier
+ * to a target value. Normally this is not needed, because you can just set those
+ * modifiers directly on the [BrushTip], but it can become useful when combined with the
+ * [enabledToolTypes] and/or [isFallbackFor] fields to only conditionally enable it.
+ */
+ @JvmField public val CONSTANT_ZERO: Source = Source(0)
+ /** Stylus or touch pressure with values reported in the range [0, 1]. */
+ @JvmField public val NORMALIZED_PRESSURE: Source = Source(1)
+ /** Stylus tilt with values reported in the range [0, π/2] radians. */
+ @JvmField public val TILT_IN_RADIANS: Source = Source(2)
+ /**
+ * Stylus tilt along the x axis in the range [-π/2, π/2], with a positive value
+ * corresponding to tilt toward the respective positive axis. In order for those values
+ * to be reported, both tilt and orientation have to be populated on the StrokeInput.
+ */
+ @JvmField public val TILT_X_IN_RADIANS: Source = Source(3)
+ /**
+ * Stylus tilt along the y axis in the range [-π/2, π/2], with a positive value
+ * corresponding to tilt toward the respective positive axis. In order for those values
+ * to be reported, both tilt and orientation have to be populated on the StrokeInput.
+ */
+ @JvmField public val TILT_Y_IN_RADIANS: Source = Source(4)
+ /** Stylus orientation with values reported in the range [0, 2π). */
+ @JvmField public val ORIENTATION_IN_RADIANS: Source = Source(5)
+ /** Stylus orientation with values reported in the range (-π, π]. */
+ @JvmField public val ORIENTATION_ABOUT_ZERO_IN_RADIANS: Source = Source(6)
+ /**
+ * Pointer speed with values >= 0 in distance units per second, where one distance unit
+ * is equal to the brush size.
+ */
+ @JvmField public val SPEED_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND: Source = Source(7)
+ /**
+ * Signed x component of pointer velocity in distance units per second, where one
+ * distance unit is equal to the brush size.
+ */
+ @JvmField
+ public val VELOCITY_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND: Source = Source(8)
+ /**
+ * Signed y component of pointer velocity in distance units per second, where one
+ * distance unit is equal to the brush size.
+ */
+ @JvmField
+ public val VELOCITY_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND: Source = Source(9)
+ /**
+ * The angle of the stroke's current direction of travel in stroke space, normalized to
+ * the range [0, 2π). A value of 0 indicates the direction of the positive X-axis in
+ * stroke space; a value of π/2 indicates the direction of the positive Y-axis in stroke
+ * space.
+ */
+ @JvmField public val DIRECTION_IN_RADIANS: Source = Source(10)
+ /**
+ * The angle of the stroke's current direction of travel in stroke space, normalized to
+ * the range (-π, π]. A value of 0 indicates the direction of the positive X-axis in
+ * stroke space; a value of π/2 indicates the direction of the positive Y-axis in stroke
+ * space.
+ */
+ @JvmField public val DIRECTION_ABOUT_ZERO_IN_RADIANS: Source = Source(11)
+ /**
+ * Signed x component of the normalized travel direction, with values in the range
+ * [-1, 1].
+ */
+ @JvmField public val NORMALIZED_DIRECTION_X: Source = Source(12)
+ /**
+ * Signed y component of the normalized travel direction, with values in the range
+ * [-1, 1].
+ */
+ @JvmField public val NORMALIZED_DIRECTION_Y: Source = Source(13)
+ /**
+ * Distance traveled by the inputs of the current stroke, starting at 0 at the first
+ * input, where one distance unit is equal to the brush size.
+ */
+ @JvmField public val DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE: Source = Source(14)
+ /**
+ * The time elapsed, in seconds, from when the stroke started to when this part of the
+ * stroke was drawn. The value remains fixed for any given part of the stroke once
+ * drawn.
+ */
+ @JvmField public val TIME_OF_INPUT_IN_SECONDS: Source = Source(15)
+ /**
+ * The time elapsed, in millis, from when the stroke started to when this part of the
+ * stroke was drawn. The value remains fixed for any given part of the stroke once
+ * drawn.
+ */
+ @JvmField public val TIME_OF_INPUT_IN_MILLIS: Source = Source(16)
+ /**
+ * Distance traveled by the inputs of the current prediction, starting at 0 at the last
+ * non-predicted input, where one distance unit is equal to the brush size. For cases
+ * where prediction hasn't started yet, we don't return a negative value, but clamp to a
+ * min of 0.
+ */
+ @JvmField
+ public val PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE: Source = Source(17)
+ /**
+ * Elapsed time of the prediction, starting at 0 at the last non-predicted input. For
+ * cases where prediction hasn't started yet, we don't return a negative value, but
+ * clamp to a min of 0.
+ */
+ @JvmField public val PREDICTED_TIME_ELAPSED_IN_SECONDS: Source = Source(18)
+ /**
+ * Elapsed time of the prediction, starting at 0 at the last non-predicted input. For
+ * cases where prediction hasn't started yet, we don't return a negative value, but
+ * clamp to a min of 0.
+ */
+ @JvmField public val PREDICTED_TIME_ELAPSED_IN_MILLIS: Source = Source(19)
+ /**
+ * The distance left to be traveled from a given input to the current last input of the
+ * stroke, where one distance unit is equal to the brush size. This value changes for
+ * each input as the stroke is drawn.
+ */
+ @JvmField public val DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE: Source = Source(20)
+ /**
+ * The amount of time that has elapsed, in seconds, since this part of the stroke was
+ * drawn. This continues to increase even after all stroke inputs have completed, and
+ * can be used to drive stroke animations. This enumerators are only compatible with a
+ * [sourceOutOfRangeBehavior] of [OutOfRange.CLAMP], to ensure that the animation will
+ * eventually end.
+ */
+ @JvmField public val TIME_SINCE_INPUT_IN_SECONDS: Source = Source(21)
+ /**
+ * The amount of time that has elapsed, in millis, since this part of the stroke was
+ * drawn. This continues to increase even after all stroke inputs have completed, and
+ * can be used to drive stroke animations. This enumerators are only compatible with a
+ * [sourceOutOfRangeBehavior] of [OutOfRange.CLAMP], to ensure that the animation will
+ * eventually end.
+ */
+ @JvmField public val TIME_SINCE_INPUT_IN_MILLIS: Source = Source(22)
+ /**
+ * Directionless pointer acceleration with values >= 0 in distance units per second
+ * squared, where one distance unit is equal to the brush size.
+ */
+ @JvmField
+ public val ACCELERATION_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED: Source =
+ Source(23)
+ /**
+ * Signed x component of pointer acceleration in distance units per second squared,
+ * where one distance unit is equal to the brush size.
+ */
+ @JvmField
+ public val ACCELERATION_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED: Source =
+ Source(24)
+ /**
+ * Signed y component of pointer acceleration in distance units per second squared,
+ * where one distance unit is equal to the brush size.
+ */
+ @JvmField
+ public val ACCELERATION_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED: Source =
+ Source(25)
+ /**
+ * Pointer acceleration along the current direction of travel in distance units per
+ * second squared, where one distance unit is equal to the brush size. A positive value
+ * indicates that the pointer is accelerating along the current direction of travel,
+ * while a negative value indicates that the pointer is decelerating.
+ */
+ @JvmField
+ public val ACCELERATION_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED: Source =
+ Source(26)
+ /**
+ * Pointer acceleration perpendicular to the current direction of travel in distance
+ * units per second squared, where one distance unit is equal to the brush size. If the
+ * X- and Y-axes of stroke space were rotated so that the positive X-axis points in the
+ * direction of stroke travel, then a positive value for this source indicates
+ * acceleration along the positive Y-axis (and a negative value indicates acceleration
+ * along the negative Y-axis).
+ */
+ @JvmField
+ public val ACCELERATION_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED: Source =
+ Source(27)
+ /**
+ * The physical speed of the input pointer at the point in question, in centimeters per
+ * second.
+ */
+ @JvmField public val INPUT_SPEED_IN_CENTIMETERS_PER_SECOND: Source = Source(28)
+ /**
+ * Signed x component of the physical velocity of the input pointer at the point in
+ * question, in centimeters per second.
+ */
+ @JvmField public val INPUT_VELOCITY_X_IN_CENTIMETERS_PER_SECOND: Source = Source(29)
+ /**
+ * Signed y component of the physical velocity of the input pointer at the point in
+ * question, in centimeters per second.
+ */
+ @JvmField public val INPUT_VELOCITY_Y_IN_CENTIMETERS_PER_SECOND: Source = Source(30)
+ /**
+ * The physical distance traveled by the input pointer from the start of the stroke
+ * along the input path to the point in question, in centimeters.
+ */
+ @JvmField public val INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS: Source = Source(31)
+ /**
+ * The physical distance that the input pointer would have to travel from its actual
+ * last real position along its predicted path to reach the predicted point in question,
+ * in centimeters. For points on the stroke before the predicted portion, this has a
+ * value of zero.
+ */
+ @JvmField
+ public val PREDICTED_INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS: Source = Source(32)
+ /**
+ * The directionless physical acceleration of the input pointer at the point in
+ * question, with values >= 0, in centimeters per second squared.
+ */
+ @JvmField
+ public val INPUT_ACCELERATION_IN_CENTIMETERS_PER_SECOND_SQUARED: Source = Source(33)
+ /**
+ * Signed x component of the physical acceleration of the input pointer, in centimeters
+ * per second squared.
+ */
+ @JvmField
+ public val INPUT_ACCELERATION_X_IN_CENTIMETERS_PER_SECOND_SQUARED: Source = Source(34)
+ /**
+ * Signed y component of the physical acceleration of the input pointer, in centimeters
+ * per second squared.
+ */
+ @JvmField
+ public val INPUT_ACCELERATION_Y_IN_CENTIMETERS_PER_SECOND_SQUARED: Source = Source(35)
+ /**
+ * The physical acceleration of the input pointer along its current direction of travel
+ * at the point in question, in centimeters per second squared. A positive value
+ * indicates that the pointer is accelerating along the current direction of travel,
+ * while a negative value indicates that the pointer is decelerating.
+ */
+ @JvmField
+ public val INPUT_ACCELERATION_FORWARD_IN_CENTIMETERS_PER_SECOND_SQUARED: Source =
+ Source(36)
+ /**
+ * The physical acceleration of the input pointer perpendicular to its current direction
+ * of travel at the point in question, in centimeters per second squared. If the X- and
+ * Y-axes of stroke space were rotated so that the positive X-axis points in the
+ * direction of stroke travel, then a positive value for this source indicates
+ * acceleration along the positive Y-axis (and a negative value indicates acceleration
+ * along the negative Y-axis).
+ */
+ @JvmField
+ public val INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED: Source =
+ Source(37)
+ private const val PREFIX = "BrushBehavior.Source."
+ }
+ }
+
+ /** List of tip properties that can be modified by a [BrushBehavior]. */
+ public class Target private constructor(@JvmField internal val value: Int) {
+
+ public fun toSimpleString(): String =
+ when (this) {
+ WIDTH_MULTIPLIER -> "WIDTH_MULTIPLIER"
+ HEIGHT_MULTIPLIER -> "HEIGHT_MULTIPLIER"
+ SIZE_MULTIPLIER -> "SIZE_MULTIPLIER"
+ SLANT_OFFSET_IN_RADIANS -> "SLANT_OFFSET_IN_RADIANS"
+ PINCH_OFFSET -> "PINCH_OFFSET"
+ ROTATION_OFFSET_IN_RADIANS -> "ROTATION_OFFSET_IN_RADIANS"
+ CORNER_ROUNDING_OFFSET -> "CORNER_ROUNDING_OFFSET"
+ POSITION_OFFSET_X_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "POSITION_OFFSET_X_IN_MULTIPLES_OF_BRUSH_SIZE"
+ POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE"
+ POSITION_OFFSET_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "POSITION_OFFSET_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE"
+ POSITION_OFFSET_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE ->
+ "POSITION_OFFSET_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE"
+ HUE_OFFSET_IN_RADIANS -> "HUE_OFFSET_IN_RADIANS"
+ SATURATION_MULTIPLIER -> "SATURATION_MULTIPLIER"
+ LUMINOSITY -> "LUMINOSITY"
+ OPACITY_MULTIPLIER -> "OPACITY_MULTIPLIER"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is Target) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+
+ /**
+ * Scales the brush-tip width, starting from the value calculated using
+ * [BrushTip.scaleX] and [BrushTip.scaleY]. The final brush width is clamped to a
+ * maximum of twice the base width. If multiple behaviors have one of these targets,
+ * they stack multiplicatively.
+ */
+ @JvmField public val WIDTH_MULTIPLIER: Target = Target(0)
+ /**
+ * Scales the brush-tip height, starting from the value calculated using
+ * [BrushTip.scaleX] and [BrushTip.scaleY]. The final brush height is clamped to a
+ * maximum of twice the base height. If multiple behaviors have one of these targets,
+ * they stack multiplicatively.
+ */
+ @JvmField public val HEIGHT_MULTIPLIER: Target = Target(1)
+ /** Convenience enumerator to target both [WIDTH_MULTIPLIER] and [HEIGHT_MULTIPLIER]. */
+ @JvmField public val SIZE_MULTIPLIER: Target = Target(2)
+ /**
+ * Adds the target modifier to [BrushTip.slant]. The final brush slant value is clamped
+ * to [-π/2, π/2]. If multiple behaviors have this target, they stack additively.
+ */
+ @JvmField public val SLANT_OFFSET_IN_RADIANS: Target = Target(3)
+ /**
+ * Adds the target modifier to [BrushTip.pinch]. The final brush pinch value is clamped
+ * to [0, 1]. If multiple behaviors have this target, they stack additively.
+ */
+ @JvmField public val PINCH_OFFSET: Target = Target(4)
+ /**
+ * Adds the target modifier to [BrushTip.rotation]. The final brush rotation angle is
+ * effectively normalized (mod 2π). If multiple behaviors have this target, they stack
+ * additively.
+ */
+ @JvmField public val ROTATION_OFFSET_IN_RADIANS: Target = Target(5)
+ /**
+ * Adds the target modifier to [BrushTip.cornerRounding]. The final brush corner
+ * rounding value is clamped to [0, 1]. If multiple behaviors have this target, they
+ * stack additively.
+ */
+ @JvmField public val CORNER_ROUNDING_OFFSET: Target = Target(6)
+ /**
+ * Adds the target modifier to the brush tip x position, where one distance unit is
+ * equal to the brush size.
+ */
+ @JvmField public val POSITION_OFFSET_X_IN_MULTIPLES_OF_BRUSH_SIZE: Target = Target(7)
+ /**
+ * Adds the target modifier to the brush tip y position, where one distance unit is
+ * equal to the brush size.
+ */
+ @JvmField public val POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE: Target = Target(8)
+ /**
+ * Moves the brush tip center forward (or backward, for negative values) from the input
+ * position, in the current direction of stroke travel, where one distance unit is equal
+ * to the brush size.
+ */
+ @JvmField
+ public val POSITION_OFFSET_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE: Target = Target(9)
+ /**
+ * Moves the brush tip center sideways from the input position, relative to the
+ * direction of stroke travel, where one distance unit is equal to the brush size. If
+ * the X- and Y-axes of stroke space were rotated so that the positive X-axis points in
+ * the direction of stroke travel, then a positive value for this offset moves the brush
+ * tip center towards the positive Y-axis (and a negative value moves the brush tip
+ * center towards the negative Y-axis).
+ */
+ @JvmField
+ public val POSITION_OFFSET_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE: Target = Target(10)
+
+ // The following are targets for tip color adjustments, including opacity. Renderers can
+ // apply
+ // them to the brush color when a stroke is drawn to contribute to the local color of
+ // each
+ // part of the stroke.
+ /**
+ * Shifts the hue of the base brush color. A positive offset shifts around the hue wheel
+ * from red towards orange, while a negative offset shifts the other way, from red
+ * towards violet. The final hue offset is not clamped, but is effectively normalized
+ * (mod 2π). If multiple behaviors have this target, they stack additively.
+ */
+ @JvmField public val HUE_OFFSET_IN_RADIANS: Target = Target(11)
+ /**
+ * Scales the saturation of the base brush color. If multiple behaviors have one of
+ * these targets, they stack multiplicatively. The final saturation multiplier is
+ * clamped to [0, 2].
+ */
+ @JvmField public val SATURATION_MULTIPLIER: Target = Target(12)
+ /**
+ * Target the luminosity of the color. An offset of +/-100% corresponds to changing the
+ * luminosity by up to +/-100%.
+ */
+ @JvmField public val LUMINOSITY: Target = Target(13)
+ /**
+ * Scales the opacity of the base brush color. If multiple behaviors have one of these
+ * targets, they stack multiplicatively. The final opacity multiplier is clamped to
+ * [0, 2].
+ */
+ @JvmField public val OPACITY_MULTIPLIER: Target = Target(14)
+
+ private const val PREFIX = "BrushBehavior.Target."
+ }
+ }
+
+ /**
+ * The desired behavior when an input value is outside the range defined by
+ * [sourceValueRangeLowerBound, sourceValueRangeUpperBound].
+ */
+ public class OutOfRange private constructor(@JvmField internal val value: Int) {
+ public fun toSimpleString(): String =
+ when (this) {
+ CLAMP -> "CLAMP"
+ REPEAT -> "REPEAT"
+ MIRROR -> "MIRROR"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is OutOfRange) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+
+ // Values outside the range will be clamped to not exceed the bounds.
+ @JvmField public val CLAMP: OutOfRange = OutOfRange(0)
+ // Values will be shifted by an integer multiple of the range size so that they fall
+ // within
+ // the bounds.
+ //
+ // In this case, the range will be treated as a half-open interval, with a value exactly
+ // at
+ // [sourceValueRangeUpperBound] being treated as though it was
+ // [sourceValueRangeLowerBound].
+ @JvmField public val REPEAT: OutOfRange = OutOfRange(1)
+ // Similar to [Repeat], but every other repetition of the bounds will be mirrored, as
+ // though
+ // the
+ // two elements [sourceValueRangeLowerBound] and [sourceValueRangeUpperBound] were
+ // swapped.
+ // This means the range does not need to be treated as a half-open interval like in the
+ // case
+ // of [Repeat].
+ @JvmField public val MIRROR: OutOfRange = OutOfRange(2)
+ private const val PREFIX = "BrushBehavior.OutOfRange."
+ }
+ }
+
+ /** List of input properties that might not be reported by inputs. */
+ public class OptionalInputProperty private constructor(@JvmField internal val value: Int) {
+
+ public fun toSimpleString(): String =
+ when (this) {
+ PRESSURE -> "PRESSURE"
+ TILT -> "TILT"
+ ORIENTATION -> "ORIENTATION"
+ TILT_X_AND_Y -> "TILT_X_AND_Y"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is OptionalInputProperty) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+
+ @JvmField public val PRESSURE: OptionalInputProperty = OptionalInputProperty(0)
+ @JvmField public val TILT: OptionalInputProperty = OptionalInputProperty(1)
+ @JvmField public val ORIENTATION: OptionalInputProperty = OptionalInputProperty(2)
+ /** Tilt-x and tilt-y require both tilt and orientation to be reported. */
+ @JvmField public val TILT_X_AND_Y: OptionalInputProperty = OptionalInputProperty(3)
+ private const val PREFIX = "BrushBehavior.OptionalInputProperty."
+ }
+ }
+
+ /** A binary operation for combining two values in a [BinaryOpNode]. */
+ public class BinaryOp private constructor(@JvmField internal val value: Int) {
+
+ internal fun toSimpleString(): String =
+ when (this) {
+ PRODUCT -> "PRODUCT"
+ SUM -> "SUM"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is BinaryOp) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /** Evaluates to the product of the two input values, or null if either is null. */
+ @JvmField public val PRODUCT: BinaryOp = BinaryOp(0)
+ /** Evaluates to the sum of the two input values, or null if either is null. */
+ @JvmField public val SUM: BinaryOp = BinaryOp(1)
+
+ private const val PREFIX = "BrushBehavior.BinaryOp."
+ }
+ }
+
+ /** Dimensions/units for measuring the [dampingGap] field of a [DampingNode] */
+ public class DampingSource private constructor(@JvmField internal val value: Int) {
+
+ internal fun toSimpleString(): String =
+ when (this) {
+ TIME_IN_SECONDS -> "TIME_IN_SECONDS"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is DampingSource) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /** Value damping occurs over time, and the [dampingGap] is measured in seconds. */
+ @JvmField public val TIME_IN_SECONDS: DampingSource = DampingSource(0)
+
+ private const val PREFIX = "BrushBehavior.DampingSource."
+ }
+ }
+
+ /**
+ * Represents one node in a [BrushBehavior]'s expression graph. [Node] objects are immutable and
+ * their inputs must be chosen at construction time; therefore, they can only ever be assembled
+ * into an acyclic graph.
+ */
+ public abstract class Node internal constructor() {
+ /** Returns the ordered list of inputs that this node directly depends on. */
+ public open fun inputs(): List<ValueNode> = emptyList()
+
+ /** Appends a native version of this [Node] to a native [BrushBehavior]. */
+ internal abstract fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long)
+ }
+
+ /**
+ * A [ValueNode] is a non-terminal node in the graph; it produces a value to be consumed as an
+ * input by other [Node]s, and may itself depend on zero or more inputs.
+ */
+ public abstract class ValueNode internal constructor() : Node() {}
+
+ /** A [ValueNode] that gets data from the stroke input batch. */
+ public class SourceNode
+ @JvmOverloads
+ constructor(
+ public val source: Source,
+ public val sourceValueRangeLowerBound: Float,
+ public val sourceValueRangeUpperBound: Float,
+ public val sourceOutOfRangeBehavior: OutOfRange = OutOfRange.CLAMP,
+ ) : ValueNode() {
+ init {
+ require(sourceValueRangeLowerBound.isFinite()) {
+ "sourceValueRangeLowerBound must be finite, was $sourceValueRangeLowerBound"
+ }
+ require(sourceValueRangeUpperBound.isFinite()) {
+ "sourceValueRangeUpperBound must be finite, was $sourceValueRangeUpperBound"
+ }
+ require(sourceValueRangeLowerBound != sourceValueRangeUpperBound) {
+ "sourceValueRangeLowerBound and sourceValueRangeUpperBound must be distinct, both were $sourceValueRangeLowerBound"
+ }
+ }
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendSourceNode(
+ nativeBehaviorPointer,
+ source.value,
+ sourceValueRangeLowerBound,
+ sourceValueRangeUpperBound,
+ sourceOutOfRangeBehavior.value,
+ )
+ }
+
+ override fun toString(): String =
+ "SourceNode(${source.toSimpleString()}, $sourceValueRangeLowerBound, $sourceValueRangeUpperBound, ${sourceOutOfRangeBehavior.toSimpleString()})"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is SourceNode) return false
+ return source == other.source &&
+ sourceValueRangeLowerBound == other.sourceValueRangeLowerBound &&
+ sourceValueRangeUpperBound == other.sourceValueRangeUpperBound &&
+ sourceOutOfRangeBehavior == other.sourceOutOfRangeBehavior
+ }
+
+ override fun hashCode(): Int {
+ var result = source.hashCode()
+ result = 31 * result + sourceValueRangeLowerBound.hashCode()
+ result = 31 * result + sourceValueRangeUpperBound.hashCode()
+ result = 31 * result + sourceOutOfRangeBehavior.hashCode()
+ return result
+ }
+
+ /** Appends a native `BrushBehavior::SourceNode` to a native brush behavior struct. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendSourceNode(
+ nativeBehaviorPointer: Long,
+ source: Int,
+ sourceValueRangeLowerBound: Float,
+ sourceValueRangeUpperBound: Float,
+ sourceOutOfRangeBehavior: Int,
+ )
+ }
+
+ /** A [ValueNode] that produces a constant output value. */
+ public class ConstantNode constructor(public val value: Float) : ValueNode() {
+ init {
+ require(value.isFinite()) { "value must be finite, was $value" }
+ }
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendConstantNode(nativeBehaviorPointer, value)
+ }
+
+ override fun toString(): String = "ConstantNode($value)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is ConstantNode) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ /** Appends a native `BrushBehavior::ConstantNode` to a native brush behavior struct. */
+ private external fun nativeAppendConstantNode(
+ nativeBehaviorPointer: Long,
+ value: Float
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ }
+
+ /**
+ * A [ValueNode] for filtering out a branch of a behavior graph unless a particular stroke input
+ * property is missing.
+ */
+ public class FallbackFilterNode
+ constructor(public val isFallbackFor: OptionalInputProperty, public val input: ValueNode) :
+ ValueNode() {
+ override fun inputs(): List<ValueNode> = listOf(input)
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendFallbackFilterNode(nativeBehaviorPointer, isFallbackFor.value)
+ }
+
+ override fun toString(): String =
+ "FallbackFilterNode(${isFallbackFor.toSimpleString()}, $input)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is FallbackFilterNode) return false
+ return isFallbackFor == other.isFallbackFor && input == other.input
+ }
+
+ override fun hashCode(): Int {
+ var result = isFallbackFor.hashCode()
+ result = 31 * result + input.hashCode()
+ return result
+ }
+
+ /**
+ * Appends a native `BrushBehavior::FallbackFilterNode` to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendFallbackFilterNode(
+ nativeBehaviorPointer: Long,
+ isFallbackFor: Int,
+ )
+ }
+
+ /**
+ * A [ValueNode] for filtering out a branch of a behavior graph unless this stroke's tool type
+ * is in the specified set.
+ */
+ public class ToolTypeFilterNode
+ constructor(
+ // The [enabledToolTypes] val below is a defensive copy of this parameter.
+ enabledToolTypes: Set<InputToolType>,
+ public val input: ValueNode,
+ ) : ValueNode() {
+ public val enabledToolTypes: Set<InputToolType> = unmodifiableSet(enabledToolTypes.toSet())
+
+ init {
+ require(!enabledToolTypes.isEmpty()) { "enabledToolTypes must be non-empty" }
+ }
+
+ override fun inputs(): List<ValueNode> = listOf(input)
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendToolTypeFilterNode(
+ nativeBehaviorPointer = nativeBehaviorPointer,
+ mouseEnabled = enabledToolTypes.contains(InputToolType.MOUSE),
+ touchEnabled = enabledToolTypes.contains(InputToolType.TOUCH),
+ stylusEnabled = enabledToolTypes.contains(InputToolType.STYLUS),
+ unknownEnabled = enabledToolTypes.contains(InputToolType.UNKNOWN),
+ )
+ }
+
+ override fun toString(): String = "ToolTypeFilterNode($enabledToolTypes, $input)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is ToolTypeFilterNode) return false
+ return enabledToolTypes == other.enabledToolTypes && input == other.input
+ }
+
+ override fun hashCode(): Int {
+ var result = enabledToolTypes.hashCode()
+ result = 31 * result + input.hashCode()
+ return result
+ }
+
+ /**
+ * Appends a native `BrushBehavior::ToolTypeFilterNode` to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendToolTypeFilterNode(
+ nativeBehaviorPointer: Long,
+ mouseEnabled: Boolean,
+ touchEnabled: Boolean,
+ stylusEnabled: Boolean,
+ unknownEnabled: Boolean,
+ )
+ }
+
+ /**
+ * A [ValueNode] that damps changes in an input value, causing the output value to slowly follow
+ * changes in the input value over a specified time or distance.
+ */
+ public class DampingNode
+ constructor(
+ public val dampingSource: DampingSource,
+ public val dampingGap: Float,
+ public val input: ValueNode,
+ ) : ValueNode() {
+ init {
+ require(dampingGap.isFinite() && dampingGap >= 0.0f) {
+ "dampingGap must be finite and non-negative, was $dampingGap"
+ }
+ }
+
+ override fun inputs(): List<ValueNode> = listOf(input)
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendDampingNode(nativeBehaviorPointer, dampingSource.value, dampingGap)
+ }
+
+ override fun toString(): String =
+ "DampingNode(${dampingSource.toSimpleString()}, $dampingGap, $input)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is DampingNode) return false
+ return dampingSource == other.dampingSource &&
+ dampingGap == other.dampingGap &&
+ input == other.input
+ }
+
+ override fun hashCode(): Int {
+ var result = dampingSource.hashCode()
+ result = 31 * result + dampingGap.hashCode()
+ result = 31 * result + input.hashCode()
+ return result
+ }
+
+ /** Appends a native `BrushBehavior::DampingNode` to a native brush behavior struct. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendDampingNode(
+ nativeBehaviorPointer: Long,
+ dampingSource: Int,
+ dampingGap: Float,
+ )
+ }
+
+ /** A [ValueNode] that maps an input value through a response curve. */
+ public class ResponseNode
+ constructor(public val responseCurve: EasingFunction, public val input: ValueNode) :
+ ValueNode() {
+ override fun inputs(): List<ValueNode> = listOf(input)
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ when (responseCurve) {
+ is EasingFunction.Predefined ->
+ nativeAppendResponseNodePredefined(nativeBehaviorPointer, responseCurve.value)
+ is EasingFunction.CubicBezier ->
+ nativeAppendResponseNodeCubicBezier(
+ nativeBehaviorPointer,
+ responseCurve.x1,
+ responseCurve.y1,
+ responseCurve.x2,
+ responseCurve.y2,
+ )
+ is EasingFunction.Steps ->
+ nativeAppendResponseNodeSteps(
+ nativeBehaviorPointer,
+ responseCurve.stepCount,
+ responseCurve.stepPosition.value,
+ )
+ is EasingFunction.Linear ->
+ nativeAppendResponseNodeLinear(
+ nativeBehaviorPointer,
+ FloatArray(responseCurve.points.size * 2).apply {
+ var index = 0
+ for (point in responseCurve.points) {
+ set(index, point.x)
+ ++index
+ set(index, point.y)
+ ++index
+ }
+ },
+ )
+ }
+ }
+
+ override fun toString(): String = "ResponseNode($responseCurve, $input)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is ResponseNode) return false
+ return responseCurve == other.responseCurve && input == other.input
+ }
+
+ override fun hashCode(): Int {
+ var result = responseCurve.hashCode()
+ result = 31 * result + input.hashCode()
+ return result
+ }
+
+ /**
+ * Appends a native `BrushBehavior::ResponseNode` with response curve of type
+ * [EasingFunction.Predefined] to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendResponseNodePredefined(
+ nativeBehaviorPointer: Long,
+ predefinedResponseCurve: Int,
+ )
+
+ /**
+ * Appends a native `BrushBehavior::ResponseNode` with response curve of type
+ * [EasingFunction.CubicBezier] to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendResponseNodeCubicBezier(
+ nativeBehaviorPointer: Long,
+ cubicBezierX1: Float,
+ cubicBezierX2: Float,
+ cubicBezierY1: Float,
+ cubicBezierY2: Float,
+ )
+
+ /**
+ * Appends a native `BrushBehavior::ResponseNode` with response curve of type
+ * [EasingFunction.Steps] to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendResponseNodeSteps(
+ nativeBehaviorPointer: Long,
+ stepsCount: Int,
+ stepsPosition: Int,
+ )
+
+ /**
+ * Appends a native `BrushBehavior::ResponseNode` with response curve of type
+ * [EasingFunction.Linear] to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendResponseNodeLinear(
+ nativeBehaviorPointer: Long,
+ points: FloatArray,
+ )
+ }
+
+ /** A [ValueNode] that combines two other values with a binary operation. */
+ public class BinaryOpNode
+ constructor(
+ public val operation: BinaryOp,
+ public val firstInput: ValueNode,
+ public val secondInput: ValueNode,
+ ) : ValueNode() {
+ override fun inputs(): List<ValueNode> = listOf(firstInput, secondInput)
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendBinaryOpNode(nativeBehaviorPointer, operation.value)
+ }
+
+ override fun toString(): String =
+ "BinaryOpNode(${operation.toSimpleString()}, $firstInput, $secondInput)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is BinaryOpNode) return false
+ return operation == other.operation &&
+ firstInput == other.firstInput &&
+ secondInput == other.secondInput
+ }
+
+ override fun hashCode(): Int {
+ var result = operation.hashCode()
+ result = 31 * result + firstInput.hashCode()
+ result = 31 * result + secondInput.hashCode()
+ return result
+ }
+
+ /** Appends a native `BrushBehavior::BinaryOpNode` to a native brush behavior struct. */
+ private external fun nativeAppendBinaryOpNode(
+ nativeBehaviorPointer: Long,
+ operation: Int
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ }
+
+ /**
+ * A [TargetNode] is a terminal node in the graph; it does not produce a value and cannot be
+ * used as an input to other [Node]s, but instead applies a modification to the brush tip state.
+ * A [BrushBehavior] consists of a list of [TargetNode]s and the various [ValueNode]s that they
+ * transitively depend on.
+ */
+ public class TargetNode
+ constructor(
+ public val target: Target,
+ public val targetModifierRangeLowerBound: Float,
+ public val targetModifierRangeUpperBound: Float,
+ public val input: ValueNode,
+ ) : Node() {
+ init {
+ require(targetModifierRangeLowerBound.isFinite()) {
+ "targetModifierRangeLowerBound must be finite, was $targetModifierRangeLowerBound"
+ }
+ require(targetModifierRangeUpperBound.isFinite()) {
+ "targetModifierRangeUpperBound must be finite, was $targetModifierRangeUpperBound"
+ }
+ require(targetModifierRangeLowerBound != targetModifierRangeUpperBound) {
+ "targetModifierRangeLowerBound and targetModifierRangeUpperBound must be distinct, both were $targetModifierRangeLowerBound"
+ }
+ }
+
+ override fun inputs(): List<ValueNode> = listOf(input)
+
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendTargetNode(
+ nativeBehaviorPointer,
+ target.value,
+ targetModifierRangeLowerBound,
+ targetModifierRangeUpperBound,
+ )
+ }
+
+ override fun toString(): String =
+ "TargetNode(${target.toSimpleString()}, $targetModifierRangeLowerBound, $targetModifierRangeUpperBound, $input)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is TargetNode) return false
+ return target == other.target &&
+ targetModifierRangeLowerBound == other.targetModifierRangeLowerBound &&
+ targetModifierRangeUpperBound == other.targetModifierRangeUpperBound &&
+ input == other.input
+ }
+
+ override fun hashCode(): Int {
+ var result = target.hashCode()
+ result = 31 * result + targetModifierRangeLowerBound.hashCode()
+ result = 31 * result + targetModifierRangeUpperBound.hashCode()
+ result = 31 * result + input.hashCode()
+ return result
+ }
+
+ /** Appends a native `BrushBehavior::TargetNode` to a native brush behavior struct. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendTargetNode(
+ nativeBehaviorPointer: Long,
+ target: Int,
+ targetModifierRangeLowerBound: Float,
+ targetModifierRangeUpperBound: Float,
+ )
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt
new file mode 100644
index 0000000..1582b57
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushCoat.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.RestrictTo
+import androidx.ink.nativeloader.NativeLoader
+import java.util.Collections.unmodifiableList
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
+
+/**
+ * A [BrushCoat] represents one coat of paint applied by a brush. It includes a single [BrushPaint],
+ * as well as one or more [BrushTip]s used to apply that paint. Multiple [BrushCoat] can be combined
+ * within a single brush; when a stroke drawn by a multi-coat brush is rendered, each coat of paint
+ * will be drawn entirely atop the previous coat, even if the stroke crosses over itself, as though
+ * each coat were painted in its entirety one at a time.
+ */
+@ExperimentalInkCustomBrushApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@Suppress("NotCloseable") // Finalize is only used to free the native peer.
+public class BrushCoat
+@JvmOverloads
+constructor(
+ // The [tips] val below is a defensive copy of this parameter.
+ tips: List<BrushTip>,
+ /** The paint to be applied in this coat. */
+ public val paint: BrushPaint = BrushPaint(),
+) {
+
+ /**
+ * The tip(s) used to apply the paint.
+ *
+ * For now, there must be exactly one tip. This restriction is expected to be lifted in a future
+ * release.
+ */
+ // TODO: b/285594469 - More than one tip.
+ public val tips: List<BrushTip> = unmodifiableList(tips.toList())
+
+ @JvmOverloads
+ public constructor(
+ tip: BrushTip = BrushTip(),
+ paint: BrushPaint = BrushPaint(),
+ ) : this(listOf(tip), paint)
+
+ /** A handle to the underlying native [BrushCoat] object. */
+ internal val nativePointer: Long =
+ nativeCreateBrushCoat(tips.map { it.nativePointer }.toLongArray(), paint.nativePointer)
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @JvmSynthetic
+ public fun copy(tips: List<BrushTip> = this.tips, paint: BrushPaint = this.paint): BrushCoat {
+ return if (tips == this.tips && paint == this.paint) {
+ this
+ } else {
+ BrushCoat(tips, paint)
+ }
+ }
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @JvmSynthetic
+ public fun copy(tip: BrushTip, paint: BrushPaint = this.paint): BrushCoat {
+ return if (this.tips.size == 1 && tip == this.tips[0] && paint == this.paint) {
+ this
+ } else {
+ BrushCoat(tip, paint)
+ }
+ }
+
+ /**
+ * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned
+ * builder to build a copy of a BrushCoat.
+ */
+ public fun toBuilder(): Builder = Builder().setTips(tips).setPaint(paint)
+
+ /**
+ * Builder for [BrushCoat].
+ *
+ * For Java developers, use BrushCoat.Builder to construct [BrushCoat] with default values,
+ * overriding only as needed. For example: `BrushCoat family = new
+ * BrushCoat.Builder().tip(presetBrushTip).build();`
+ */
+ public class Builder {
+ private var tips: List<BrushTip> = listOf(BrushTip())
+ private var paint: BrushPaint = BrushPaint()
+
+ public fun setTip(tip: BrushTip): Builder {
+ this.tips = listOf(tip)
+ return this
+ }
+
+ public fun setTips(tips: List<BrushTip>): Builder {
+ this.tips = tips.toList()
+ return this
+ }
+
+ public fun setPaint(paint: BrushPaint): Builder {
+ this.paint = paint
+ return this
+ }
+
+ public fun build(): BrushCoat = BrushCoat(tips, paint)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is BrushCoat) return false
+ return tips == other.tips && paint == other.paint
+ }
+
+ override fun hashCode(): Int {
+ var result = tips.hashCode()
+ result = 31 * result + paint.hashCode()
+ return result
+ }
+
+ override fun toString(): String = "BrushCoat(tips=$tips, paint=$paint)"
+
+ /** Deletes native BrushCoat memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeBrushCoat(nativePointer)
+ }
+
+ /** Create underlying native object and return reference for all subsequent native calls. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeCreateBrushCoat(
+ tipNativePointers: LongArray,
+ paintNativePointer: Long,
+ ): Long
+
+ /** Release the underlying memory allocated in [nativeCreateBrushCoat]. */
+ private external fun nativeFreeBrushCoat(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ // Companion object gets initialized before anything else.
+ public companion object {
+ init {
+ NativeLoader.load()
+ }
+
+ /** Returns a new [BrushCoat.Builder]. */
+ @JvmStatic public fun builder(): Builder = Builder()
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt
new file mode 100644
index 0000000..a0ef9e6
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushFamily.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.RestrictTo
+import androidx.ink.nativeloader.NativeLoader
+import java.util.Collections.unmodifiableList
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
+
+/**
+ * A [BrushFamily] describes a family of brushes (e.g. “highlighter” or “pressure pen”),
+ * irrespective of their size or color.
+ *
+ * For now, [BrushFamily] is an opaque type that can only be instantiated via [StockBrushes]. A
+ * future version of this module will allow creating fully custom [BrushFamily] objects.
+ *
+ * [BrushFamily] objects are immutable.
+ */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@Suppress("NotCloseable") // Finalize is only used to free the native peer.
+public class BrushFamily
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@ExperimentalInkCustomBrushApi
+@JvmOverloads
+constructor(
+ // The [coats] val below is a defensive copy of this parameter.
+ coats: List<BrushCoat>,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public val uri: String? = null,
+) {
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public val coats: List<BrushCoat> = unmodifiableList(coats.toList())
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ @JvmOverloads
+ public constructor(
+ tip: BrushTip = BrushTip(),
+ paint: BrushPaint = BrushPaint(),
+ uri: String? = null,
+ ) : this(listOf(BrushCoat(tip, paint)), uri)
+
+ /** A handle to the underlying native [BrushFamily] object. */
+ internal val nativePointer: Long =
+ nativeCreateBrushFamily(coats.map { it.nativePointer }.toLongArray(), uri)
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ @JvmSynthetic
+ public fun copy(coats: List<BrushCoat> = this.coats, uri: String? = this.uri): BrushFamily {
+ return if (coats == this.coats && uri == this.uri) {
+ this
+ } else {
+ BrushFamily(coats, uri)
+ }
+ }
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ @JvmSynthetic
+ public fun copy(coat: BrushCoat, uri: String? = this.uri): BrushFamily {
+ return copy(coats = listOf(coat), uri = uri)
+ }
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ @JvmSynthetic
+ public fun copy(tip: BrushTip, paint: BrushPaint, uri: String? = this.uri): BrushFamily {
+ return copy(coat = BrushCoat(tip, paint), uri = uri)
+ }
+
+ /**
+ * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned
+ * builder to build a copy of a BrushFamily.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ public fun toBuilder(): Builder = Builder().setCoats(coats).setUri(uri)
+
+ /**
+ * Builder for [BrushFamily].
+ *
+ * For Java developers, use BrushFamily.Builder to construct [BrushFamily] with default values,
+ * overriding only as needed. For example: `BrushFamily family = new
+ * BrushFamily.Builder().coat(presetBrushCoat).build();`
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ public class Builder {
+ private var coats: List<BrushCoat> = listOf(BrushCoat(BrushTip(), BrushPaint()))
+ private var uri: String? = null
+
+ public fun setCoat(tip: BrushTip, paint: BrushPaint): Builder =
+ setCoat(BrushCoat(tip, paint))
+
+ public fun setCoat(coat: BrushCoat): Builder = setCoats(listOf(coat))
+
+ public fun setCoats(coats: List<BrushCoat>): Builder {
+ this.coats = coats.toList()
+ return this
+ }
+
+ public fun setUri(uri: String?): Builder {
+ this.uri = uri
+ return this
+ }
+
+ public fun build(): BrushFamily = BrushFamily(coats, uri)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is BrushFamily) return false
+ return coats == other.coats && uri == other.uri
+ }
+
+ override fun hashCode(): Int {
+ var result = coats.hashCode()
+ result = 31 * result + uri.hashCode()
+ return result
+ }
+
+ override fun toString(): String = "BrushFamily(coats=$coats, uri=$uri)"
+
+ /** Deletes native BrushFamily memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeBrushFamily(nativePointer)
+ }
+
+ /** Create underlying native object and return reference for all subsequent native calls. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeCreateBrushFamily(coatNativePointers: LongArray, uri: String?): Long
+
+ /** Release the underlying memory allocated in [nativeCreateBrushFamily]. */
+ private external fun nativeFreeBrushFamily(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ // Companion object gets initialized before anything else.
+ public companion object {
+ init {
+ NativeLoader.load()
+ }
+
+ /** Returns a new [BrushFamily.Builder]. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @ExperimentalInkCustomBrushApi
+ @JvmStatic
+ public fun builder(): Builder = Builder()
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt
new file mode 100644
index 0000000..002412f
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt
@@ -0,0 +1,666 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
+import androidx.ink.geometry.AngleRadiansFloat
+import androidx.ink.nativeloader.NativeLoader
+import java.util.Collections.unmodifiableList
+import kotlin.Suppress
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmSynthetic
+
+/**
+ * Parameters that control stroke mesh rendering. Note: This contains only a subset of the
+ * parameters as support is added for them.
+ *
+ * The core of each paint consists of one or more texture layers. The output of each layer is
+ * blended together in sequence, then the combined texture is blended with the output from the brush
+ * color.
+ * - Starting with the first [TextureLayer], the combined texture for layers 0 to i (source) is
+ * blended with layer i+1 (destination) using the blend mode for layer i.
+ * - The final combined texture (source) is blended with the (possibly adjusted per-vertex) brush
+ * color (destination) according to the blend mode of the last texture layer.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@ExperimentalInkCustomBrushApi
+@Suppress("NotCloseable") // Finalize is only used to free the native peer.
+public class BrushPaint(
+ // The [textureLayers] val below is a defensive copy of this parameter.
+ textureLayers: List<TextureLayer> = emptyList()
+) {
+ /** The textures to apply to the stroke. */
+ public val textureLayers: List<TextureLayer> = unmodifiableList(textureLayers.toList())
+
+ /** A handle to the underlying native [BrushPaint] object. */
+ internal val nativePointer: Long = nativeCreateBrushPaint(textureLayers.size)
+
+ init {
+ for (layer in textureLayers) {
+ nativeAppendTextureLayer(nativePointer, layer.nativePointer)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is BrushPaint) return false
+ return textureLayers == other.textureLayers
+ }
+
+ override fun toString(): String = "BrushPaint(textureLayers=$textureLayers)"
+
+ override fun hashCode(): Int {
+ return textureLayers.hashCode()
+ }
+
+ /** Delete native BrushPaint memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeBrushPaint(nativePointer)
+ }
+
+ /** Create underlying native object and return reference for all subsequent native calls. */
+ private external fun nativeCreateBrushPaint(
+ textureLayersCount: Int
+ ): Long // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /**
+ * Appends a texture layer to a *mutable* C++ BrushPaint object as referenced by
+ * [nativePointer]. Only call during `init{}` so to keep this BrushPaint object immutable after
+ * construction and equivalent across Kotlin and C++.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendTextureLayer(nativePointer: Long, textureLayerPointer: Long)
+
+ /** Release the underlying memory allocated in [nativeCreateBrushPaint]. */
+ private external fun nativeFreeBrushPaint(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /** Specification of how the texture should apply to the stroke. */
+ public class TextureMapping private constructor(@JvmField internal val value: Int) {
+ override fun toString(): String =
+ when (this) {
+ TILING -> "BrushPaint.TextureMapping.TILING"
+ WINDING -> "BrushPaint.TextureMapping.WINDING"
+ else -> "BrushPaint.TextureMapping.INVALID($value)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other !is TextureMapping) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /**
+ * The texture will repeat according to a 2D affine transformation of vertex positions.
+ * Each copy of the texture will have the same size and shape modulo reflections.
+ */
+ @JvmField public val TILING: TextureMapping = TextureMapping(0)
+ /**
+ * The texture will morph to "wind along the path of the stroke." The horizontal axis of
+ * texture space will lie along the width of the stroke and the vertical axis will lie
+ * along the direction of travel of the stroke at each point.
+ */
+ @JvmField public val WINDING: TextureMapping = TextureMapping(1)
+ }
+ }
+
+ /** Specification of the origin point to use for the texture. */
+ public class TextureOrigin private constructor(@JvmField internal val value: Int) {
+ override fun toString(): String =
+ when (this) {
+ STROKE_SPACE_ORIGIN -> "BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN"
+ FIRST_STROKE_INPUT -> "BrushPaint.TextureOrigin.FIRST_STROKE_INPUT"
+ LAST_STROKE_INPUT -> "BrushPaint.TextureOrigin.LAST_STROKE_INPUT"
+ else -> "BrushPaint.TextureOrigin.INVALID($value)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other !is TextureOrigin) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /**
+ * The texture origin is the origin of stroke space, however that happens to be defined
+ * for a given stroke.
+ */
+ @JvmField public val STROKE_SPACE_ORIGIN: TextureOrigin = TextureOrigin(0)
+ /** The texture origin is the first input position for the stroke. */
+ @JvmField public val FIRST_STROKE_INPUT: TextureOrigin = TextureOrigin(1)
+ /**
+ * The texture origin is the last input position (including predicted inputs) for the
+ * stroke. Note that this means that the texture origin for an in-progress stroke will
+ * move as more inputs are added.
+ */
+ @JvmField public val LAST_STROKE_INPUT: TextureOrigin = TextureOrigin(2)
+ }
+ }
+
+ /** Units for specifying [TextureLayer.sizeX] and [TextureLayer.sizeY]. */
+ public class TextureSizeUnit private constructor(@JvmField internal val value: Int) {
+ override fun toString(): String =
+ when (this) {
+ BRUSH_SIZE -> "BrushPaint.TextureSizeUnit.BRUSH_SIZE"
+ STROKE_SIZE -> "BrushPaint.TextureSizeUnit.STROKE_SIZE"
+ STROKE_COORDINATES -> "BrushPaint.TextureSizeUnit.STROKE_COORDINATES"
+ else -> "BrushPaint.TextureSizeUnit.INVALID($value)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other !is TextureSizeUnit) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /** As multiples of brush size. */
+ @JvmField public val BRUSH_SIZE: TextureSizeUnit = TextureSizeUnit(0)
+ /**
+ * As multiples of the stroke "size". This has different meanings depending on the value
+ * of [TextureMapping] for the given texture. For [TextureMapping.TILING] textures, the
+ * stroke size is equal to the dimensions of the XY bounding rectangle of the mesh. For
+ * [TextureMapping.WINDING] textures, the stroke size components are given by x: stroke
+ * width, which may change over the course of the stroke if behaviors affect the tip
+ * geometry. y: the total distance traveled by the stroke.
+ */
+ @JvmField public val STROKE_SIZE: TextureSizeUnit = TextureSizeUnit(1)
+ /** In the same units as the stroke's input positions and stored geometry. */
+ @JvmField public val STROKE_COORDINATES: TextureSizeUnit = TextureSizeUnit(2)
+ }
+ }
+
+ /**
+ * The method by which the combined texture layers (index <= i) are blended with the next layer.
+ * The blend mode on the final layer controls how the combined texture is blended with the brush
+ * color, and should typically be a mode whose output alpha is proportional to the destination
+ * alpha, so that it can be adjusted by anti-aliasing.
+ */
+ public class BlendMode private constructor(@JvmField internal val value: Int) {
+ override fun toString(): String =
+ when (this) {
+ MODULATE -> "BrushPaint.BlendMode.MODULATE"
+ DST_IN -> "BrushPaint.BlendMode.DST_IN"
+ DST_OUT -> "BrushPaint.BlendMode.DST_OUT"
+ SRC_ATOP -> "BrushPaint.BlendMode.SRC_ATOP"
+ SRC_IN -> "BrushPaint.BlendMode.SRC_IN"
+ SRC_OVER -> "BrushPaint.BlendMode.SRC_OVER"
+ else -> "BrushPaint.BlendMode.INVALID($value)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ return other is BlendMode && this.value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+
+ /**
+ * Source and destination are component-wise multiplied, including opacity.
+ *
+ * ```
+ * Alpha = Alpha_src * Alpha_dst
+ * Color = Color_src * Color_dst
+ * ```
+ */
+ @JvmField public val MODULATE: BlendMode = BlendMode(0)
+ /**
+ * Keeps destination pixels that cover source pixels. Discards remaining source and
+ * destination pixels.
+ *
+ * ```
+ * Alpha = Alpha_src * Alpha_dst
+ * Color = Alpha_src * Color_dst
+ * ```
+ */
+ @JvmField public val DST_IN: BlendMode = BlendMode(1)
+ /**
+ * Keeps the destination pixels not covered by source pixels. Discards destination
+ * pixels that are covered by source pixels and all source pixels.
+ *
+ * ```
+ * Alpha = (1 - Alpha_src) * Alpha_dst
+ * Color = (1 - Alpha_src) * Color_dst
+ * ```
+ */
+ @JvmField public val DST_OUT: BlendMode = BlendMode(2)
+ /**
+ * Discards source pixels that do not cover destination pixels. Draws remaining pixels
+ * over destination pixels.
+ *
+ * ```
+ * Alpha = Alpha_dst
+ * Color = Alpha_dst * Color_src + (1 - Alpha_src) * Color_dst
+ * ```
+ */
+ @JvmField public val SRC_ATOP: BlendMode = BlendMode(3)
+ /**
+ * Keeps the source pixels that cover destination pixels. Discards remaining source and
+ * destination pixels.
+ *
+ * ```
+ * Alpha = Alpha_src * Alpha_dst
+ * Color = Color_src * Alpha_dst
+ * ```
+ */
+ @JvmField public val SRC_IN: BlendMode = BlendMode(4)
+
+ /*
+ * The following modes can't be used for the last TextureLayer, which defines the mode for
+ * blending the combined texture with the (possibly adjusted per-vertex) brush color. That blend
+ * mode needs the output Alpha to be a multiple of Alpha_dst so that per-vertex adjustment for
+ * anti-aliasing is preserved correctly.
+ */
+
+ /**
+ * The source pixels are drawn over the destination pixels.
+ *
+ * ```
+ * Alpha = Alpha_src + (1 - Alpha_src) * Alpha_dst
+ * Color = Color_src + (1 - Alpha_src) * Color_dst
+ * ```
+ *
+ * This mode shouldn't normally be used for the final [TextureLayer], since its output
+ * alpha is not proportional to the destination alpha (so it wouldn't preserve alpha
+ * adjustments from anti-aliasing).
+ */
+ @JvmField public val SRC_OVER: BlendMode = BlendMode(5)
+ /**
+ * The source pixels are drawn behind the destination pixels.
+ *
+ * ```
+ * Alpha = Alpha_dst + (1 - Alpha_dst) * Alpha_src
+ * Color = Color_dst + (1 - Alpha_dst) * Color_src
+ * ```
+ *
+ * This mode shouldn't normally be used for the final [TextureLayer], since its output
+ * alpha is not proportional to the destination alpha (so it wouldn't preserve alpha
+ * adjustments from anti-aliasing).
+ */
+ @JvmField public val DST_OVER: BlendMode = BlendMode(6)
+ /**
+ * Keeps the source pixels and discards the destination pixels.
+ *
+ * ```
+ * Alpha = Alpha_src
+ * Color = Color_src
+ * ```
+ *
+ * This mode shouldn't normally be used for the final [TextureLayer], since its output
+ * alpha is not proportional to the destination alpha (so it wouldn't preserve alpha
+ * adjustments from anti-aliasing).
+ */
+ @JvmField public val SRC: BlendMode = BlendMode(7)
+ /**
+ * Keeps the destination pixels and discards the source pixels.
+ *
+ * ```
+ * Alpha = Alpha_dst
+ * Color = Color_dst
+ * ```
+ *
+ * This mode is unlikely to be useful, since it effectively causes the renderer to just
+ * ignore this [TextureLayer] and all layers before it, but it is included for
+ * completeness.
+ */
+ @JvmField public val DST: BlendMode = BlendMode(8)
+ /**
+ * Keeps the source pixels that do not cover destination pixels. Discards destination
+ * pixels and all source pixels that cover destination pixels.
+ *
+ * ```
+ * Alpha = (1 - Alpha_dst) * Alpha_src
+ * Color = (1 - Alpha_dst) * Color_src
+ * ```
+ *
+ * This mode shouldn't normally be used for the final [TextureLayer], since its output
+ * alpha is not proportional to the destination alpha (so it wouldn't preserve alpha
+ * adjustments from anti-aliasing).
+ */
+ @JvmField public val SRC_OUT: BlendMode = BlendMode(9)
+ /**
+ * Discards destination pixels that aren't covered by source pixels. Remaining
+ * destination pixels are drawn over source pixels.
+ *
+ * ```
+ * Alpha = Alpha_src
+ * Color = Alpha_src * Color_dst + (1 - Alpha_dst) * Color_src
+ * ```
+ *
+ * This mode shouldn't normally be used for the final [TextureLayer], since its output
+ * alpha is not proportional to the destination alpha (so it wouldn't preserve alpha
+ * adjustments from anti-aliasing).
+ */
+ @JvmField public val DST_ATOP: BlendMode = BlendMode(10)
+ /**
+ * Discards source and destination pixels that intersect; keeps source and destination
+ * pixels that do not intersect.
+ *
+ * ```
+ * Alpha = (1 - Alpha_dst) * Alpha_src + (1 - Alpha_src) * Alpha_dst
+ * Color = (1 - Alpha_dst) * Color_src + (1 - Alpha_src) * Color_dst
+ * ```
+ *
+ * This mode shouldn't normally be used for the final [TextureLayer], since its output
+ * alpha is not proportional to the destination alpha (so it wouldn't preserve alpha
+ * adjustments from anti-aliasing).
+ */
+ @JvmField public val XOR: BlendMode = BlendMode(11)
+ }
+ }
+
+ /**
+ * An explicit layer defined by an image.
+ *
+ * @param colorTextureUri The URI of an image that provides the color for a particular pixel for
+ * this layer. The coordinates within this image that will be used are determined by the other
+ * parameters.
+ * @param sizeX The X size in [TextureSizeUnit] of the image specified by [colorTextureUri].
+ * @param sizeY The Y size in [TextureSizeUnit] of the image specified by [colorTextureUri].
+ * @param offsetX An offset into the texture, specified as fractions of the texture [sizeX] in
+ * the range [0,1].
+ * @param offsetY An offset into the texture, specified as fractions of the texture [sizeY] in
+ * the range [0,1].
+ * @param rotation Angle in radians specifying the rotation of the texture. The rotation is
+ * carried out about the center of the texture's first repetition along both axes.
+ * @param opacity Overall layer opacity in the range [0,1], where 0 is transparent and 1 is
+ * opaque.
+ * @param sizeUnit The units used to specify [sizeX] and [sizeY].
+ * @param mapping The method by which the coordinates of the [colorTextureUri] image will apply
+ * to the stroke.
+ * @param blendMode The method by which the texture layers up to this one (index <= i) are
+ * combined with the subsequent texture layer (index == i+1). For the last texture layer, this
+ * defines the method by which the texture layer is combined with the brush color (possibly
+ * after that color gets per-vertex adjustments).
+ */
+ @Suppress("NotCloseable") // Finalize is only used to free the native peer.
+ public class TextureLayer(
+ public val colorTextureUri: String,
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ public val sizeX: Float,
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ public val sizeY: Float,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ public val offsetX: Float = 0f,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ public val offsetY: Float = 0f,
+ @AngleRadiansFloat public val rotation: Float = 0F,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ public val opacity: Float = 1f,
+ public val sizeUnit: TextureSizeUnit = TextureSizeUnit.STROKE_COORDINATES,
+ public val origin: TextureOrigin = TextureOrigin.STROKE_SPACE_ORIGIN,
+ public val mapping: TextureMapping = TextureMapping.TILING,
+ public val blendMode: BlendMode = BlendMode.MODULATE,
+ ) {
+ internal val nativePointer: Long =
+ nativeCreateTextureLayer(
+ colorTextureUri,
+ sizeX,
+ sizeY,
+ offsetX,
+ offsetY,
+ rotation,
+ opacity,
+ sizeUnit.value,
+ origin.value,
+ mapping.value,
+ blendMode.value,
+ )
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @JvmSynthetic
+ public fun copy(
+ colorTextureUri: String = this.colorTextureUri,
+ sizeX: Float = this.sizeX,
+ sizeY: Float = this.sizeY,
+ offsetX: Float = this.offsetX,
+ offsetY: Float = this.offsetY,
+ @AngleRadiansFloat rotation: Float = this.rotation,
+ opacity: Float = this.opacity,
+ sizeUnit: TextureSizeUnit = this.sizeUnit,
+ origin: TextureOrigin = this.origin,
+ mapping: TextureMapping = this.mapping,
+ blendMode: BlendMode = this.blendMode,
+ ): TextureLayer {
+ if (
+ colorTextureUri == this.colorTextureUri &&
+ sizeX == this.sizeX &&
+ sizeY == this.sizeY &&
+ offsetX == this.offsetX &&
+ offsetY == this.offsetY &&
+ rotation == this.rotation &&
+ opacity == this.opacity &&
+ sizeUnit == this.sizeUnit &&
+ origin == this.origin &&
+ mapping == this.mapping &&
+ blendMode == this.blendMode
+ ) {
+ return this
+ }
+ return TextureLayer(
+ colorTextureUri,
+ sizeX,
+ sizeY,
+ offsetX,
+ offsetY,
+ rotation,
+ opacity,
+ sizeUnit,
+ origin,
+ mapping,
+ blendMode,
+ )
+ }
+
+ /**
+ * Returns a [Builder] with values set equivalent to `this`. Java developers, use the
+ * returned builder to build a copy of a TextureLayer. Kotlin developers, see [copy] method.
+ */
+ public fun toBuilder(): Builder =
+ Builder(
+ colorTextureUri = this.colorTextureUri,
+ sizeX = this.sizeX,
+ sizeY = this.sizeY,
+ offsetX = this.offsetX,
+ offsetY = this.offsetY,
+ rotation = this.rotation,
+ opacity = this.opacity,
+ sizeUnit = this.sizeUnit,
+ origin = this.origin,
+ mapping = this.mapping,
+ blendMode = this.blendMode,
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is TextureLayer) return false
+ return colorTextureUri == other.colorTextureUri &&
+ sizeX == other.sizeX &&
+ sizeY == other.sizeY &&
+ offsetX == other.offsetX &&
+ offsetY == other.offsetY &&
+ rotation == other.rotation &&
+ opacity == other.opacity &&
+ sizeUnit == other.sizeUnit &&
+ origin == other.origin &&
+ mapping == other.mapping &&
+ blendMode == other.blendMode
+ }
+
+ override fun toString(): String =
+ "BrushPaint.TextureLayer(colorTextureUri=$colorTextureUri, sizeX=$sizeX, sizeY=$sizeY, " +
+ "offset=[$offsetX, $offsetY], rotation=$rotation, opacity=$opacity sizeUnit=$sizeUnit, origin=$origin, mapping=$mapping, " +
+ "blendMode=$blendMode)"
+
+ override fun hashCode(): Int {
+ var result = colorTextureUri.hashCode()
+ result = 31 * result + sizeX.hashCode()
+ result = 31 * result + sizeY.hashCode()
+ result = 31 * result + offsetX.hashCode()
+ result = 31 * result + offsetY.hashCode()
+ result = 31 * result + rotation.hashCode()
+ result = 31 * result + opacity.hashCode()
+ result = 31 * result + sizeUnit.hashCode()
+ result = 31 * result + origin.hashCode()
+ result = 31 * result + mapping.hashCode()
+ result = 31 * result + blendMode.hashCode()
+ return result
+ }
+
+ /** Delete native TextureLayer memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeTextureLayer(nativePointer)
+ }
+
+ /**
+ * Builder for [TextureLayer].
+ *
+ * Construct from TextureLayer.toBuilder().
+ */
+ @Suppress(
+ "ScopeReceiverThis"
+ ) // Builder pattern supported for Java clients, despite being an anti-pattern in Kotlin.
+ public class Builder
+ internal constructor(
+ private var colorTextureUri: String,
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ private var sizeX: Float,
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = false,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false,
+ )
+ private var sizeY: Float,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ private var offsetX: Float = 0f,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ private var offsetY: Float = 0f,
+ @AngleRadiansFloat private var rotation: Float = 0F,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ private var opacity: Float = 1f,
+ private var sizeUnit: TextureSizeUnit = TextureSizeUnit.STROKE_COORDINATES,
+ private var origin: TextureOrigin = TextureOrigin.STROKE_SPACE_ORIGIN,
+ private var mapping: TextureMapping = TextureMapping.TILING,
+ private var blendMode: BlendMode = BlendMode.MODULATE,
+ ) {
+ public fun setColorTextureUri(colorTextureUri: String): Builder = apply {
+ this.colorTextureUri = colorTextureUri
+ }
+
+ public fun setSizeX(sizeX: Float): Builder = apply { this.sizeX = sizeX }
+
+ public fun setSizeY(sizeY: Float): Builder = apply { this.sizeY = sizeY }
+
+ public fun setOffsetX(offsetX: Float): Builder = apply { this.offsetX = offsetX }
+
+ public fun setOffsetY(offsetY: Float): Builder = apply { this.offsetY = offsetY }
+
+ public fun setRotation(rotation: Float): Builder = apply { this.rotation = rotation }
+
+ public fun setOpacity(opacity: Float): Builder = apply { this.opacity = opacity }
+
+ public fun setSizeUnit(sizeUnit: TextureSizeUnit): Builder = apply {
+ this.sizeUnit = sizeUnit
+ }
+
+ public fun setOrigin(origin: TextureOrigin): Builder = apply { this.origin = origin }
+
+ public fun setMapping(mapping: TextureMapping): Builder = apply {
+ this.mapping = mapping
+ }
+
+ public fun setBlendMode(blendMode: BlendMode): Builder = apply {
+ this.blendMode = blendMode
+ }
+
+ public fun build(): TextureLayer =
+ TextureLayer(
+ colorTextureUri,
+ sizeX,
+ sizeY,
+ offsetX,
+ offsetY,
+ rotation,
+ opacity,
+ sizeUnit,
+ origin,
+ mapping,
+ blendMode,
+ )
+ }
+
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeCreateTextureLayer(
+ colorTextureUri: String,
+ sizeX: Float,
+ sizeY: Float,
+ offsetX: Float,
+ offsetY: Float,
+ rotation: Float,
+ opacity: Float,
+ sizeUnit: Int,
+ origin: Int,
+ mapping: Int,
+ blendMode: Int,
+ ): Long
+
+ /** Release the underlying memory allocated in [nativeCreateTextureLayer]. */
+ private external fun nativeFreeTextureLayer(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ // To be extended by extension methods.
+ public companion object
+ }
+
+ // To be extended by extension methods.
+ public companion object {
+ init {
+ NativeLoader.load()
+ }
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt
new file mode 100644
index 0000000..e226874
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushTip.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.annotation.RestrictTo
+import androidx.ink.geometry.Angle
+import androidx.ink.geometry.AngleRadiansFloat
+import androidx.ink.nativeloader.NativeLoader
+import java.util.Collections.unmodifiableList
+import kotlin.jvm.JvmStatic
+import kotlin.jvm.JvmSynthetic
+import kotlin.math.PI
+
+/**
+ * A [BrushTip] consists of parameters that control how stroke inputs are used to model the tip
+ * shape and color, and create vertices for the stroke mesh.
+ *
+ * The specification can be considered in two parts:
+ * 1. Parameters for the base shape of the tip as a function of [Brush] size.
+ * 2. An array of [BrushBehavior]s that allow dynamic properties of each input to augment the tip
+ * shape and color.
+ *
+ * Depending on the combination of values, the tip can be shaped as a rounded parallelogram, circle,
+ * or stadium. Through [BrushBehavior]s, the tip can produce a per-vertex HSLA color shift that can
+ * be used to augment the [Brush] color when drawing. The default values below produce a static
+ * circular tip shape with diameter equal to the [Brush] size and no color shift.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@ExperimentalInkCustomBrushApi
+@Suppress("NotCloseable") // Finalize is only used to free the native peer.
+public class BrushTip(
+ /**
+ * 2D scale used to calculate the initial width and height of the tip shape relative to the
+ * brush size prior to applying [slant] and [rotation].
+ *
+ * The base width and height of the tip will be equal to the brush size multiplied by [scaleX]
+ * and [scaleY] respectively. Valid values must be finite and non-negative, with at least one
+ * value greater than zero.
+ */
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = true,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false
+ )
+ public val scaleX: Float = 1f,
+
+ /**
+ * 2D scale used to calculate the initial width and height of the tip shape relative to the
+ * brush size prior to applying [slant] and [rotation].
+ *
+ * The base width and height of the tip will be equal to the brush size multiplied by [scaleX]
+ * and [scaleY] respectively. Valid values must be finite and non-negative, with at least one
+ * value greater than zero.
+ */
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = true,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false
+ )
+ public val scaleY: Float = 1f,
+
+ /**
+ * A normalized value in the range [0, 1] that is used to calculate the initial radius of
+ * curvature for the tip's corners. A value of 0 results in sharp corners and a value of 1
+ * results in the maximum radius of curvature given the current tip dimensions.
+ */
+ @FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true)
+ public val cornerRounding: Float = 1f,
+
+ /**
+ * Angle in readians used to calculate the initial slant of the tip shape prior to applying
+ * [rotation].
+ *
+ * The value should be in the range [-π/2, π/2] radians, and represents the angle by which
+ * "vertical" lines of the tip shape will appear rotated about their intersection with the
+ * x-axis.
+ *
+ * More info: This property is similar to the single-arg CSS skew() transformation. Unlike skew,
+ * slant tries to preserve the perimeter of the tip shape as opposed to its area. This is akin
+ * to "pressing" a rectangle into a parallelogram with non-right angles while preserving the
+ * side lengths.
+ */
+ @FloatRange(from = -PI / 2, fromInclusive = true, to = PI / 2, toInclusive = true)
+ @AngleRadiansFloat
+ public val slant: Float = Angle.ZERO,
+
+ /**
+ * A unitless parameter in the range [0, 1] that controls the separation between two of the
+ * shape's corners prior to applying [rotation].
+ *
+ * The two corners affected lie toward the negative y-axis relative to the center of the tip
+ * shape. I.e. the "upper edge" of the shape if positive y is chosen to point "down" in stroke
+ * coordinates.
+ *
+ * If [scaleX] is not 0, different values of [pinch] produce the following shapes: A value of 0
+ * will leave the corners unaffected as a rectangle or parallelogram. Values between 0 and 1
+ * will bring the corners closer together to result in a (possibly slanted) trapezoidal shape. A
+ * value of 1 will make the two corners coincide and result in a triangular shape.
+ */
+ @FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true)
+ public val pinch: Float = 0f,
+
+ /**
+ * Angle in radians specifying the initial rotation of the tip shape after applying [scaleX],
+ * [scaleY], [pinch], and [slant].
+ */
+ @AngleRadiansFloat public val rotation: Float = Angle.ZERO,
+
+ /**
+ * Scales the opacity of the base brush color for this tip, independent of `brush_behavior`s. A
+ * possible example application is a highlighter brush.
+ *
+ * The multiplier must be in the range [0, 2] and the value ultimately applied can be modified
+ * by applicable `brush_behavior`s.
+ */
+ @FloatRange(from = 0.0, fromInclusive = true, to = 2.0, toInclusive = true)
+ public val opacityMultiplier: Float = 1f,
+
+ /**
+ * Parameter controlling emission of particles as a function of distance traveled by the stroke
+ * inputs.
+ *
+ * When this and [particleGapDurationMillis] are both zero, the stroke will be continuous,
+ * unless gaps are introduced dynamically by [BrushBehavior]s. Otherwise, the stroke will be
+ * made up of particles. A new particle will be emitted after at least
+ * [particleGapDistanceScale] * [Brush.size] distance has been traveled by the stoke inputs.
+ */
+ @FloatRange(
+ from = 0.0,
+ fromInclusive = true,
+ to = Double.POSITIVE_INFINITY,
+ toInclusive = false
+ )
+ public val particleGapDistanceScale: Float = 0f,
+
+ /**
+ * Parameter controlling emission of particles as a function of time elapsed along the stroke.
+ *
+ * When this and [particleGapDistanceScale] are both zero, the stroke will be continuous, unless
+ * gaps are introduced dynamically by `BrushBehavior`s. Otherwise, the stroke will be made up of
+ * particles. Particles will be emitted at most once every [particleGapDurationMillis].
+ */
+ @IntRange(from = 0L) public val particleGapDurationMillis: Long = 0L,
+
+ // The [behaviors] val below is a defensive copy of this parameter.
+ behaviors: List<BrushBehavior> = emptyList(),
+) {
+ /**
+ * A list of [BrushBehavior]s that allow dynamic properties of each input to augment the tip
+ * shape and color.
+ */
+ public val behaviors: List<BrushBehavior> = unmodifiableList(behaviors.toList())
+
+ /** A handle to the underlying native [BrushTip] object. */
+ internal val nativePointer: Long =
+ nativeCreateBrushTip(
+ scaleX,
+ scaleY,
+ cornerRounding,
+ slant,
+ pinch,
+ rotation,
+ opacityMultiplier,
+ particleGapDistanceScale,
+ particleGapDurationMillis,
+ behaviors.size,
+ )
+
+ init {
+ for (behavior in behaviors) {
+ nativeAppendBehavior(nativePointer, behavior.nativePointer)
+ }
+ }
+
+ /**
+ * Creates a copy of `this` and allows named properties to be altered while keeping the rest
+ * unchanged.
+ */
+ @JvmSynthetic
+ public fun copy(
+ scaleX: Float = this.scaleX,
+ scaleY: Float = this.scaleY,
+ cornerRounding: Float = this.cornerRounding,
+ @AngleRadiansFloat slant: Float = this.slant,
+ pinch: Float = this.pinch,
+ @AngleRadiansFloat rotation: Float = this.rotation,
+ opacityMultiplier: Float = this.opacityMultiplier,
+ particleGapDistanceScale: Float = this.particleGapDistanceScale,
+ particleGapDurationMillis: Long = this.particleGapDurationMillis,
+ behaviors: List<BrushBehavior> = this.behaviors,
+ ): BrushTip =
+ BrushTip(
+ scaleX,
+ scaleY,
+ cornerRounding,
+ slant,
+ pinch,
+ rotation,
+ opacityMultiplier,
+ particleGapDistanceScale,
+ particleGapDurationMillis,
+ behaviors,
+ )
+
+ /**
+ * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned
+ * builder to build a copy of a BrushTip. Kotlin developers, see [copy] method.
+ */
+ public fun toBuilder(): Builder =
+ Builder()
+ .setScaleX(scaleX)
+ .setScaleY(scaleY)
+ .setCornerRounding(cornerRounding)
+ .setSlant(slant)
+ .setPinch(pinch)
+ .setRotation(rotation)
+ .setOpacityMultiplier(opacityMultiplier)
+ .setParticleGapDistanceScale(particleGapDistanceScale)
+ .setParticleGapDurationMillis(particleGapDurationMillis)
+ .setBehaviors(behaviors)
+
+ /**
+ * Builder for [BrushTip].
+ *
+ * Use BrushTip.Builder to construct a [BrushTip] with default values, overriding only as
+ * needed.
+ */
+ @Suppress("ScopeReceiverThis")
+ public class Builder {
+ private var scaleX: Float = 1f
+ private var scaleY: Float = 1f
+ private var cornerRounding: Float = 1f
+ private var slant: Float = Angle.ZERO
+ private var pinch: Float = 0f
+ private var rotation: Float = Angle.ZERO
+ private var opacityMultiplier: Float = 1f
+ private var particleGapDistanceScale: Float = 0F
+ private var particleGapDurationMillis: Long = 0L
+ private var behaviors: List<BrushBehavior> = emptyList()
+
+ public fun setScaleX(scaleX: Float): Builder = apply { this.scaleX = scaleX }
+
+ public fun setScaleY(scaleY: Float): Builder = apply { this.scaleY = scaleY }
+
+ public fun setCornerRounding(cornerRounding: Float): Builder = apply {
+ this.cornerRounding = cornerRounding
+ }
+
+ public fun setSlant(slant: Float): Builder = apply { this.slant = slant }
+
+ public fun setPinch(pinch: Float): Builder = apply { this.pinch = pinch }
+
+ public fun setRotation(rotation: Float): Builder = apply { this.rotation = rotation }
+
+ public fun setOpacityMultiplier(opacityMultiplier: Float): Builder = apply {
+ this.opacityMultiplier = opacityMultiplier
+ }
+
+ public fun setParticleGapDistanceScale(particleGapDistanceScale: Float): Builder = apply {
+ this.particleGapDistanceScale = particleGapDistanceScale
+ }
+
+ public fun setParticleGapDurationMillis(particleGapDurationMillis: Long): Builder = apply {
+ this.particleGapDurationMillis = particleGapDurationMillis
+ }
+
+ public fun setBehaviors(behaviors: List<BrushBehavior>): Builder = apply {
+ this.behaviors = behaviors.toList()
+ }
+
+ public fun build(): BrushTip =
+ BrushTip(
+ scaleX,
+ scaleY,
+ cornerRounding,
+ slant,
+ pinch,
+ rotation,
+ opacityMultiplier,
+ particleGapDistanceScale,
+ particleGapDurationMillis,
+ behaviors,
+ )
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is BrushTip) return false
+ return scaleY == other.scaleY &&
+ scaleX == other.scaleX &&
+ pinch == other.pinch &&
+ cornerRounding == other.cornerRounding &&
+ slant == other.slant &&
+ rotation == other.rotation &&
+ particleGapDistanceScale == other.particleGapDistanceScale &&
+ particleGapDurationMillis == other.particleGapDurationMillis &&
+ opacityMultiplier == other.opacityMultiplier &&
+ behaviors == other.behaviors
+ }
+
+ override fun hashCode(): Int {
+ var result = scaleX.hashCode()
+ result = 31 * result + scaleY.hashCode()
+ result = 31 * result + pinch.hashCode()
+ result = 31 * result + cornerRounding.hashCode()
+ result = 31 * result + slant.hashCode()
+ result = 31 * result + rotation.hashCode()
+ result = 31 * result + opacityMultiplier.hashCode()
+ result = 31 * result + particleGapDistanceScale.hashCode()
+ result = 31 * result + particleGapDurationMillis.hashCode()
+ result = 31 * result + behaviors.hashCode()
+ return result
+ }
+
+ override fun toString(): String =
+ "BrushTip(scale=($scaleX, $scaleY), cornerRounding=$cornerRounding," +
+ " slant=$slant, pinch=$pinch, rotation=$rotation, opacityMultiplier=$opacityMultiplier," +
+ " particleGapDistanceScale=$particleGapDistanceScale," +
+ " particleGapDurationMillis=$particleGapDurationMillis, behaviors=$behaviors)"
+
+ /** Delete native BrushTip memory. */
+ protected fun finalize() {
+ // NOMUTANTS -- Not tested post garbage collection.
+ nativeFreeBrushTip(nativePointer)
+ }
+
+ /** Create underlying native object and return reference for all subsequent native calls. */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeCreateBrushTip(
+ scaleX: Float,
+ scaleY: Float,
+ cornerRounding: Float,
+ slant: Float,
+ pinch: Float,
+ rotation: Float,
+ opacityMultiplier: Float,
+ particleGapDistanceScale: Float,
+ particleGapDurationMillis: Long,
+ behaviorsCount: Int,
+ ): Long
+
+ /**
+ * Appends a texture layer to a *mutable* C++ BrushTip object as referenced by [nativePointer].
+ * Only call during init{} so to keep this BrushTip object immutable after construction and
+ * equivalent across Kotlin and C++.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendBehavior(tipNativePointer: Long, behaviorNativePointer: Long)
+
+ /** Release the underlying memory allocated in [nativeCreateBrushTip]. */
+ private external fun nativeFreeBrushTip(
+ nativePointer: Long
+ ) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ // Companion object gets initialized before anything else.
+ public companion object {
+ init {
+ NativeLoader.load()
+ }
+
+ /** Returns a new [BrushTip.Builder]. */
+ @JvmStatic public fun builder(): Builder = Builder()
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt
new file mode 100644
index 0000000..889fab9
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/ColorExtensions.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.RestrictTo
+import androidx.ink.brush.color.Color as ComposeColor
+import androidx.ink.brush.color.colorspace.ColorSpace as ComposeColorSpace
+import androidx.ink.brush.color.colorspace.ColorSpaces as ComposeColorSpaces
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun ComposeColor.toColorInInkSupportedColorSpace(): ComposeColor {
+ return if (this.colorSpace.isSupportedInInk()) {
+ this
+ } else {
+ this.convert(ComposeColorSpaces.DisplayP3)
+ }
+}
+
+internal fun ComposeColorSpace.toInkColorSpaceId() =
+ when (this) {
+ ComposeColorSpaces.Srgb -> 0
+ ComposeColorSpaces.DisplayP3 -> 1
+ else -> throw IllegalArgumentException("Unsupported Compose color space")
+ }
+
+internal fun ComposeColorSpace.isSupportedInInk() =
+ (this == ComposeColorSpaces.Srgb || this == ComposeColorSpaces.DisplayP3)
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt
new file mode 100644
index 0000000..b39a502
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/EasingFunction.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.FloatRange
+import androidx.annotation.RestrictTo
+import androidx.ink.geometry.ImmutableVec
+import java.util.Collections.unmodifiableList
+import kotlin.jvm.JvmField
+
+/**
+ * An easing function always passes through the (x, y) points (0, 0) and (1, 1). It typically acts
+ * to map x values in the [0, 1] interval to y values in [0, 1] by either one of the predefined or
+ * one of the parameterized curve types below. Depending on the type of curve, input and output
+ * values outside [0, 1] are possible.
+ */
+@ExperimentalInkCustomBrushApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+public abstract class EasingFunction private constructor() {
+
+ public class Predefined private constructor(@JvmField internal val value: Int) :
+ EasingFunction() {
+
+ public fun toSimpleString(): String =
+ when (value) {
+ 0 -> "LINEAR"
+ 1 -> "EASE"
+ 2 -> "EASE_IN"
+ 3 -> "EASE_OUT"
+ 4 -> "EASE_IN_OUT"
+ 5 -> "STEP_START"
+ 6 -> "STEP_END"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is Predefined) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /** The linear identity function: accepts and returns values outside [0, 1]. */
+ @JvmField public val LINEAR: Predefined = Predefined(0)
+
+ /**
+ * Predefined cubic Bezier function. See
+ * [ease](https://www.w3.org/TR/css-easing-1/#cubic-bezier-easing-functions)
+ * and @see [CubicBezier] about input values outside [0, 1])
+ */
+ @JvmField public val EASE: Predefined = Predefined(1)
+
+ /**
+ * Predefined cubic Bezier function. See
+ * [ease-in](https://www.w3.org/TR/css-easing-1/#cubic-bezier-easing-functions)
+ * and @see [CubicBezier] about input values outside [0, 1])
+ */
+ @JvmField public val EASE_IN: Predefined = Predefined(2)
+
+ /**
+ * Predefined cubic Bezier function. See
+ * [ease-out](https://www.w3.org/TR/css-easing-1/#cubic-bezier-easing-functions)
+ * and @see [CubicBezier] about input values outside [0, 1])
+ */
+ @JvmField public val EASE_OUT: Predefined = Predefined(3)
+
+ /**
+ * Predefined cubic Bezier function. See
+ * [ease-in-out](https://www.w3.org/TR/css-easing-1/#cubic-bezier-easing-functions)
+ * and @see [CubicBezier] about input values outside [0, 1])
+ */
+ @JvmField public val EASE_IN_OUT: Predefined = Predefined(4)
+
+ /**
+ * Predefined step function with a jump-start at input progress value of 0. See
+ * [step start](https://www.w3.org/TR/css-easing-1/#step-easing-functions)
+ */
+ @JvmField public val STEP_START: Predefined = Predefined(5)
+
+ /**
+ * Predefined step function with a jump-end at input progress value of 1. See
+ * [step end](https://www.w3.org/TR/css-easing-1/#step-easing-functions)
+ */
+ @JvmField public val STEP_END: Predefined = Predefined(6)
+
+ private const val PREFIX = "EasingFunction.Predefined."
+ }
+ }
+
+ /**
+ * Parameters for a custom cubic Bezier easing function.
+ *
+ * A cubic Bezier is generally defined by four points, P0 - P3. In the case of the easing
+ * function, P0 is defined to be the point (0, 0), and P3 is defined to be the point (1, 1). The
+ * values of [x1] and [x2] are required to be in the range [0, 1]. This guarantees that the
+ * resulting curve is a function with respect to x and follows the CSS cubic Bezier
+ * specification:
+ * [https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function#cubic_b%C3%A9zier_easing_function](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function#cubic_b%C3%A9zier_easing_function)
+ *
+ * Valid parameters must have all finite values, and [x1] and [x2] must be in the interval
+ * [0, 1].
+ *
+ * Input x values that are outside the interval [0, 1] will be clamped, but output values will
+ * not. This is somewhat different from the w3c defined cubic Bezier that allows extrapolated
+ * values outside x in [0, 1] by following end-point tangents.
+ */
+ public class CubicBezier(
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ public val x1: Float,
+ public val y1: Float,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
+ public val x2: Float,
+ public val y2: Float,
+ ) : EasingFunction() {
+ init {
+ require(x1.isFinite() && x2.isFinite() && y1.isFinite() && y2.isFinite()) {
+ "All parameters must be finite. x1 = $x1, x2 = $x2, y1 = $y1, y2 = $y2"
+ }
+ require(x1 in 0.0..1.0) { "x1 = $x1 is required to be in the range [0, 1]" }
+ require(x2 in 0.0..1.0) { "x2 = $x2 is required to be in the range [0, 1]" }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is CubicBezier) {
+ return false
+ }
+ return x1 == other.x1 && x2 == other.x2 && y1 == other.y1 && y2 == other.y2
+ }
+
+ override fun hashCode(): Int {
+ var result = x1.hashCode()
+ result = 31 * result + x2.hashCode()
+ result = 31 * result + y1.hashCode()
+ result = 31 * result + y2.hashCode()
+ return result
+ }
+
+ override fun toString(): String =
+ "EasingFunction.CubicBezier(x1=$x1, y1=$y1, x2=$x2, y2=$y2)"
+
+ // Declared to make extension functions available.
+ public companion object
+ }
+
+ /**
+ * Parameters for a custom piecewise-linear easing function.
+ *
+ * A piecewise-linear function is defined by a sequence of points; the value of the function at
+ * an x-position equal to one of those points is equal to the y-position of that point, and the
+ * value of the function at an x-position between two points is equal to the linear
+ * interpolation between those points' y-positions. This easing function implicitly includes the
+ * points (0, 0) and (1, 1), so the `points` field below need only include any points between
+ * those. If [points] is empty, then this function is equivalent to the [Predefined.LINEAR]
+ * identity function.
+ *
+ * To be valid, all y-positions must be finite, and all x-positions must be in the range [0, 1]
+ * and must be monotonically non-decreasing. It is valid for multiple points to have the same
+ * x-position, in order to create a discontinuity in the function; in that case, the value of
+ * the function at exactly that x-position is equal to the y-position of the last of these
+ * points.
+ *
+ * If the input x-value is outside the interval [0, 1], the output will be extrapolated from the
+ * first/last line segment.
+ */
+ public class Linear(
+ // The [points] val below is a defensive copy of this parameter.
+ points: List<ImmutableVec>
+ ) : EasingFunction() {
+ public val points: List<ImmutableVec> = unmodifiableList(points.toList())
+
+ init {
+ for (point: ImmutableVec in points) {
+ require(point.x.isFinite() && point.y.isFinite()) {
+ "All points must be finite. Got $point"
+ }
+ require(point.x in 0.0..1.0) {
+ "point.x is required to be in the range [0, 1]. Got $point"
+ }
+ }
+ for ((a, b) in points.zipWithNext()) {
+ require(a.x <= b.x) { "Points must be sorted by x-value. Got $a before $b" }
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is Linear) {
+ return false
+ }
+ return points == other.points
+ }
+
+ override fun hashCode(): Int {
+ return points.hashCode()
+ }
+
+ override fun toString(): String = "EasingFunction.Linear(${points})"
+
+ // Declared to make extension functions available.
+ public companion object
+ }
+
+ /**
+ * Parameters for a custom step easing function.
+ *
+ * A step function is defined by the number of equal-sized steps into which the
+ * [0, 1) interval of input-x is split and the behavior at the extremes. When x < 0, the output will always be 0. When x >= 1, the output will always be 1. The output of the first and last steps is governed by the [StepPosition].
+ *
+ * @param stepCount The number of steps. Must always be greater than 0, and must be greater than
+ * 1 if [stepPosition] is [StepPosition.JUMP_NONE].
+ *
+ * The behavior and naming follows the CSS steps() specification at
+ * [https://www.w3.org/TR/css-easing-1/#step-easing-functions](https://www.w3.org/TR/css-easing-1/#step-easing-functions)
+ */
+ public class Steps(public val stepCount: Int, public val stepPosition: StepPosition) :
+ EasingFunction() {
+ init {
+ require(stepCount > 0) { "stepCount = $stepCount is required to be greater than 0." }
+ require(stepPosition != StepPosition.JUMP_NONE || stepCount > 1) {
+ "stepCount = $stepCount is required to be greater than 1 if stepPosition = JUMP_NONE."
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is Steps) {
+ return false
+ }
+ return stepCount == other.stepCount && stepPosition == other.stepPosition
+ }
+
+ override fun hashCode(): Int {
+ var result = stepCount.hashCode()
+ result = 31 * result + stepPosition.hashCode()
+ return result
+ }
+
+ override fun toString(): String =
+ "EasingFunction.Steps(stepCount=$stepCount, stepPosition=$stepPosition)"
+
+ // Declared to make extension functions available.
+ public companion object
+ }
+
+ /**
+ * Setting to determine the desired output value of the first and last step of
+ * [0, 1) for [EasingFunction.Steps].
+ */
+ public class StepPosition private constructor(@JvmField internal val value: Int) :
+ EasingFunction() {
+
+ public fun toSimpleString(): String =
+ when (value) {
+ 0 -> "JUMP_END"
+ 1 -> "JUMP_START"
+ 2 -> "JUMP_BOTH"
+ 3 -> "JUMP_NONE"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is StepPosition) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /**
+ * The step function "jumps" at the end of [0, 1): For x in [0, 1/step_count) => y = 0.
+ * For x in [1 - 1/step_count, 1) => y = 1 - 1/step_count.
+ */
+ @JvmField public val JUMP_END: StepPosition = StepPosition(0)
+ /**
+ * The step function "jumps" at the start of [0, 1): For x in [0, 1/step_count) => y =
+ * 1/step_count. For x in [1 - 1/step_count, 1) => y = 1.
+ */
+ @JvmField public val JUMP_START: StepPosition = StepPosition(1)
+ /**
+ * The step function "jumps" at both the start and the end: For x in [0, 1/step_count)
+ * => y = 1/(step_count + 1). For x in [1 - 1/step_count, 1) => y = 1 - 1/(step_count +
+ * 1).
+ */
+ @JvmField public val JUMP_BOTH: StepPosition = StepPosition(2)
+
+ /**
+ * The step function does not "jump" at either boundary: For x in [0, 1/step_count) => y
+ * = 0. For x in [1 - 1/step_count, 1) => y = 1.
+ */
+ @JvmField public val JUMP_NONE: StepPosition = StepPosition(3)
+ private const val PREFIX = "EasingFunction.StepPosition."
+ }
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/ExperimentalInkCustomBrushApi.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/ExperimentalInkCustomBrushApi.kt
new file mode 100644
index 0000000..c422f93
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/ExperimentalInkCustomBrushApi.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+/**
+ * Marks declarations that are are part of the **experimental** Ink brush customization API. These
+ * declarations may (or may not) be changed, deprecated, or removed in the near future, or the
+ * semantics of their behavior may change in some way that may break some code.
+ *
+ * You can opt in to using APIs in your code by marking your declaration with `@OptIn` passing the
+ * opt-in requirement annotation as its argument: `@OptIn(ExperimentalInkCustomBrushApi::class)`.
+ */
+@MustBeDocumented
+@Retention(value = AnnotationRetention.BINARY)
+@Target(
+ AnnotationTarget.CLASS,
+ AnnotationTarget.ANNOTATION_CLASS,
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.FIELD,
+ AnnotationTarget.LOCAL_VARIABLE,
+ AnnotationTarget.VALUE_PARAMETER,
+ AnnotationTarget.CONSTRUCTOR,
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER,
+ AnnotationTarget.TYPEALIAS,
+)
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
+public annotation class ExperimentalInkCustomBrushApi
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
new file mode 100644
index 0000000..90626ce
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.RestrictTo
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmStatic
+
+/**
+ * The type of input tool used in producing [com.google.inputmethod.ink.strokes.StrokeInput], used
+ * by [BrushBehavior] to define when a behavior is applicable.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+
+// TODO: b/355248266 - @UsedByNative("stroke_input_jni_helper.cc") must go in Proguard config file
+// instead.
+public class InputToolType
+private constructor(
+ @JvmField
+ @field:RestrictTo(
+ RestrictTo.Scope.LIBRARY_GROUP
+ ) // NonPublicApi // TODO: b/355248266 - @UsedByNative("stroke_input_jni_helper.cc") must go in
+ // Proguard config file instead.
+ public val value: Int
+) {
+
+ private fun toSimpleString(): String =
+ when (this) {
+ UNKNOWN -> "UNKNOWN"
+ MOUSE -> "MOUSE"
+ TOUCH -> "TOUCH"
+ STYLUS -> "STYLUS"
+ else -> "INVALID"
+ }
+
+ public override fun toString(): String = PREFIX + this.toSimpleString()
+
+ public override fun equals(other: Any?): Boolean {
+ if (other == null || other !is InputToolType) return false
+ return value == other.value
+ }
+
+ public override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /**
+ * Get InputToolType by Int. Accessible internally for conversion to and from C++
+ * representations of ToolType in JNI code and in internal Kotlin code. The `internal`
+ * keyword obfuscates the function signature, hence the need for JvmName annotation.
+ */
+ @JvmStatic
+ @JvmName("from")
+ // TODO: b/355248266 - @UsedByNative("stroke_input_jni_helper.cc") must go in Proguard
+ // config file instead.
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun from(value: Int): InputToolType {
+ return when (value) {
+ UNKNOWN.value -> UNKNOWN
+ MOUSE.value -> MOUSE
+ TOUCH.value -> TOUCH
+ STYLUS.value -> STYLUS
+ else -> throw IllegalArgumentException("Invalid value: $value")
+ }
+ }
+
+ @JvmField public val UNKNOWN: InputToolType = InputToolType(0)
+ @JvmField public val MOUSE: InputToolType = InputToolType(1)
+ @JvmField public val TOUCH: InputToolType = InputToolType(2)
+ @JvmField public val STYLUS: InputToolType = InputToolType(3)
+ private const val PREFIX = "InputToolType."
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt
new file mode 100644
index 0000000..5cb0b21
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/StockBrushes.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.annotation.RestrictTo
+import androidx.ink.geometry.Angle
+import kotlin.jvm.JvmStatic
+
+/**
+ * Provides a fixed set of stock [BrushFamily] objects that any app can use.
+ *
+ * All brush designs are versioned, so apps can safely store input points and brush specs instead of
+ * the pixel result, but be able to regenerate strokes from stored input points that look like the
+ * strokes originally drawn by the user. Brush designs are intended to evolve over time, and are
+ * released as update packs to the stock library.
+ *
+ * Each successive brush version will keep to the spirit of the brush, but the actual effect can
+ * change between versions. For example, a new version of the highlighter may introduce a variation
+ * on how round the tip is, or what sort of curve maps color to pressure.
+ *
+ * We generally recommend that applications use the latest brush version available; but some use
+ * cases, such as art, should be careful to track which version of a brush was used if the document
+ * is regenerated, so that the user gets the same visual result.
+ */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+public object StockBrushes {
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // Needed on both property and on getter for AndroidX build, but the Kotlin compiler doesn't
+ // like it on the getter so suppress its complaint.
+ @ExperimentalInkCustomBrushApi
+ @get:ExperimentalInkCustomBrushApi
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @JvmStatic
+ public val predictionFadeOutBehavior: BrushBehavior =
+ BrushBehavior(
+ targetNodes =
+ listOf(
+ BrushBehavior.TargetNode(
+ target = BrushBehavior.Target.OPACITY_MULTIPLIER,
+ targetModifierRangeLowerBound = 1F,
+ targetModifierRangeUpperBound = 0.3F,
+ BrushBehavior.BinaryOpNode(
+ operation = BrushBehavior.BinaryOp.PRODUCT,
+ firstInput =
+ BrushBehavior.SourceNode(
+ source = BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_MILLIS,
+ sourceValueRangeLowerBound = 0F,
+ sourceValueRangeUpperBound = 24F,
+ ),
+ // The second branch of the binary op node keeps the opacity fade-out
+ // from starting
+ // until the predicted inputs have traveled at least 1.5x brush-size.
+ secondInput =
+ BrushBehavior.ResponseNode(
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ input =
+ BrushBehavior.SourceNode(
+ source =
+ BrushBehavior.Source
+ .PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ sourceValueRangeLowerBound = 1.5F,
+ sourceValueRangeUpperBound = 2F,
+ ),
+ ),
+ ),
+ )
+ )
+ )
+
+ /**
+ * Version 1 of a simple, circular fixed-width brush.
+ *
+ * The behavior of this [BrushFamily] will not meaningfully change in future releases. More
+ * significant updates would be contained in a [BrushFamily] with a different name specifying a
+ * later version number.
+ */
+ @JvmStatic
+ public val markerV1: BrushFamily =
+ BrushFamily(tip = BrushTip(behaviors = listOf(predictionFadeOutBehavior)))
+
+ /**
+ * The latest version of a simple, circular fixed-width brush.
+ *
+ * The behavior of this [BrushFamily] may change in future releases, as it always points to the
+ * latest version of the marker.
+ */
+ @JvmStatic public val markerLatest: BrushFamily = markerV1
+
+ /**
+ * Version 1 of a pressure- and speed-sensitive brush that is optimized for handwriting with a
+ * stylus.
+ *
+ * The behavior of this [BrushFamily] will not meaningfully change in future releases. More
+ * significant updates would be contained in a [BrushFamily] with a different name specifying a
+ * later version number.
+ */
+ @JvmStatic
+ public val pressurePenV1: BrushFamily =
+ BrushFamily(
+ tip =
+ BrushTip(
+ behaviors =
+ listOf(
+ predictionFadeOutBehavior,
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.SIZE_MULTIPLIER,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 1f,
+ targetModifierRangeLowerBound = 0.05f,
+ targetModifierRangeUpperBound = 1f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.LINEAR,
+ responseTimeMillis = 40L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ ),
+ )
+ )
+ )
+
+ /**
+ * The latest version of a pressure- and speed-sensitive brush that is optimized for handwriting
+ * with a stylus.
+ *
+ * The behavior of this [BrushFamily] may change in future releases, as it always points to the
+ * latest version of the pressure pen.
+ */
+ @JvmStatic public val pressurePenLatest: BrushFamily = pressurePenV1
+
+ /**
+ * Version 1 of a chisel-tip brush that is intended for highlighting text in a document (when
+ * used with a translucent brush color).
+ *
+ * The behavior of this [BrushFamily] will not meaningfully change in future releases. More
+ * significant updates would be contained in a [BrushFamily] with a different name specifying a
+ * later version number.
+ */
+ @JvmStatic
+ public val highlighterV1: BrushFamily =
+ BrushFamily(
+ tip =
+ BrushTip(
+ scaleX = 0.05f,
+ scaleY = 1f,
+ cornerRounding = 0.11f,
+ rotation = Angle.degreesToRadians(150f),
+ behaviors = listOf(predictionFadeOutBehavior),
+ )
+ )
+
+ /**
+ * Version 1 of a chisel-tip brush that is intended for highlighting text in a document (when
+ * used with a translucent brush color).
+ *
+ * The behavior of this [BrushFamily] may change in future releases, as it always points to the
+ * latest version of the pressure pen.
+ */
+ @JvmStatic public val highlighterLatest: BrushFamily = highlighterV1
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt
new file mode 100644
index 0000000..9fc1779
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt
@@ -0,0 +1,1461 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.IllegalArgumentException
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
+class BrushBehaviorTest {
+
+ @Test
+ fun sourceConstants_areDistinct() {
+ val list =
+ listOf<BrushBehavior.Source>(
+ BrushBehavior.Source.CONSTANT_ZERO,
+ BrushBehavior.Source.NORMALIZED_PRESSURE,
+ BrushBehavior.Source.TILT_IN_RADIANS,
+ BrushBehavior.Source.TILT_X_IN_RADIANS,
+ BrushBehavior.Source.TILT_Y_IN_RADIANS,
+ BrushBehavior.Source.ORIENTATION_IN_RADIANS,
+ BrushBehavior.Source.ORIENTATION_ABOUT_ZERO_IN_RADIANS,
+ BrushBehavior.Source.SPEED_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND,
+ BrushBehavior.Source.VELOCITY_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND,
+ BrushBehavior.Source.VELOCITY_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND,
+ BrushBehavior.Source.DIRECTION_IN_RADIANS,
+ BrushBehavior.Source.DIRECTION_ABOUT_ZERO_IN_RADIANS,
+ BrushBehavior.Source.NORMALIZED_DIRECTION_X,
+ BrushBehavior.Source.NORMALIZED_DIRECTION_Y,
+ BrushBehavior.Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Source.TIME_OF_INPUT_IN_SECONDS,
+ BrushBehavior.Source.TIME_OF_INPUT_IN_MILLIS,
+ BrushBehavior.Source.PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_SECONDS,
+ BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_MILLIS,
+ BrushBehavior.Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Source.TIME_SINCE_INPUT_IN_SECONDS,
+ BrushBehavior.Source.TIME_SINCE_INPUT_IN_MILLIS,
+ BrushBehavior.Source.ACCELERATION_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED,
+ BrushBehavior.Source.ACCELERATION_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED,
+ BrushBehavior.Source.ACCELERATION_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED,
+ BrushBehavior.Source
+ .ACCELERATION_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED,
+ BrushBehavior.Source
+ .ACCELERATION_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED,
+ BrushBehavior.Source.INPUT_SPEED_IN_CENTIMETERS_PER_SECOND,
+ BrushBehavior.Source.INPUT_VELOCITY_X_IN_CENTIMETERS_PER_SECOND,
+ BrushBehavior.Source.INPUT_VELOCITY_Y_IN_CENTIMETERS_PER_SECOND,
+ BrushBehavior.Source.INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS,
+ BrushBehavior.Source.PREDICTED_INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS,
+ BrushBehavior.Source.INPUT_ACCELERATION_IN_CENTIMETERS_PER_SECOND_SQUARED,
+ BrushBehavior.Source.INPUT_ACCELERATION_X_IN_CENTIMETERS_PER_SECOND_SQUARED,
+ BrushBehavior.Source.INPUT_ACCELERATION_Y_IN_CENTIMETERS_PER_SECOND_SQUARED,
+ BrushBehavior.Source.INPUT_ACCELERATION_FORWARD_IN_CENTIMETERS_PER_SECOND_SQUARED,
+ BrushBehavior.Source.INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED,
+ )
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun sourceHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.Source.NORMALIZED_PRESSURE.hashCode())
+ .isEqualTo(BrushBehavior.Source.NORMALIZED_PRESSURE.hashCode())
+
+ assertThat(BrushBehavior.Source.TILT_IN_RADIANS.hashCode())
+ .isNotEqualTo(BrushBehavior.Source.NORMALIZED_PRESSURE.hashCode())
+ }
+
+ @Test
+ fun sourceEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.Source.NORMALIZED_PRESSURE)
+ .isEqualTo(BrushBehavior.Source.NORMALIZED_PRESSURE)
+
+ assertThat(BrushBehavior.Source.TILT_IN_RADIANS)
+ .isNotEqualTo(BrushBehavior.Source.NORMALIZED_PRESSURE)
+ assertThat(BrushBehavior.Source.TILT_IN_RADIANS).isNotEqualTo(null)
+ }
+
+ @Test
+ fun sourceToString_returnsCorrectString() {
+ assertThat(BrushBehavior.Source.CONSTANT_ZERO.toString())
+ .isEqualTo("BrushBehavior.Source.CONSTANT_ZERO")
+ assertThat(BrushBehavior.Source.NORMALIZED_PRESSURE.toString())
+ .isEqualTo("BrushBehavior.Source.NORMALIZED_PRESSURE")
+ assertThat(BrushBehavior.Source.TILT_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.TILT_IN_RADIANS")
+ assertThat(BrushBehavior.Source.TILT_X_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.TILT_X_IN_RADIANS")
+ assertThat(BrushBehavior.Source.TILT_Y_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.TILT_Y_IN_RADIANS")
+ assertThat(BrushBehavior.Source.ORIENTATION_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.ORIENTATION_IN_RADIANS")
+ assertThat(BrushBehavior.Source.ORIENTATION_ABOUT_ZERO_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.ORIENTATION_ABOUT_ZERO_IN_RADIANS")
+ assertThat(BrushBehavior.Source.SPEED_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND.toString())
+ .isEqualTo("BrushBehavior.Source.SPEED_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND")
+ assertThat(BrushBehavior.Source.VELOCITY_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND.toString())
+ .isEqualTo("BrushBehavior.Source.VELOCITY_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND")
+ assertThat(BrushBehavior.Source.VELOCITY_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND.toString())
+ .isEqualTo("BrushBehavior.Source.VELOCITY_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND")
+ assertThat(BrushBehavior.Source.DIRECTION_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.DIRECTION_IN_RADIANS")
+ assertThat(BrushBehavior.Source.DIRECTION_ABOUT_ZERO_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Source.DIRECTION_ABOUT_ZERO_IN_RADIANS")
+ assertThat(BrushBehavior.Source.NORMALIZED_DIRECTION_X.toString())
+ .isEqualTo("BrushBehavior.Source.NORMALIZED_DIRECTION_X")
+ assertThat(BrushBehavior.Source.NORMALIZED_DIRECTION_Y.toString())
+ .isEqualTo("BrushBehavior.Source.NORMALIZED_DIRECTION_Y")
+ assertThat(BrushBehavior.Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE.toString())
+ .isEqualTo("BrushBehavior.Source.DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE")
+ assertThat(BrushBehavior.Source.TIME_OF_INPUT_IN_SECONDS.toString())
+ .isEqualTo("BrushBehavior.Source.TIME_OF_INPUT_IN_SECONDS")
+ assertThat(BrushBehavior.Source.TIME_OF_INPUT_IN_MILLIS.toString())
+ .isEqualTo("BrushBehavior.Source.TIME_OF_INPUT_IN_MILLIS")
+ assertThat(
+ BrushBehavior.Source.PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.PREDICTED_DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE"
+ )
+ assertThat(BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_SECONDS.toString())
+ .isEqualTo("BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_SECONDS")
+ assertThat(BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_MILLIS.toString())
+ .isEqualTo("BrushBehavior.Source.PREDICTED_TIME_ELAPSED_IN_MILLIS")
+ assertThat(BrushBehavior.Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE.toString())
+ .isEqualTo("BrushBehavior.Source.DISTANCE_REMAINING_IN_MULTIPLES_OF_BRUSH_SIZE")
+ assertThat(BrushBehavior.Source.TIME_SINCE_INPUT_IN_SECONDS.toString())
+ .isEqualTo("BrushBehavior.Source.TIME_SINCE_INPUT_IN_SECONDS")
+ assertThat(BrushBehavior.Source.TIME_SINCE_INPUT_IN_MILLIS.toString())
+ .isEqualTo("BrushBehavior.Source.TIME_SINCE_INPUT_IN_MILLIS")
+ assertThat(
+ BrushBehavior.Source.ACCELERATION_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.ACCELERATION_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source.ACCELERATION_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.ACCELERATION_X_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source.ACCELERATION_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.ACCELERATION_Y_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source
+ .ACCELERATION_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.ACCELERATION_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source
+ .ACCELERATION_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.ACCELERATION_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE_PER_SECOND_SQUARED"
+ )
+ assertThat(BrushBehavior.Source.INPUT_SPEED_IN_CENTIMETERS_PER_SECOND.toString())
+ .isEqualTo("BrushBehavior.Source.INPUT_SPEED_IN_CENTIMETERS_PER_SECOND")
+ assertThat(BrushBehavior.Source.INPUT_VELOCITY_X_IN_CENTIMETERS_PER_SECOND.toString())
+ .isEqualTo("BrushBehavior.Source.INPUT_VELOCITY_X_IN_CENTIMETERS_PER_SECOND")
+ assertThat(BrushBehavior.Source.INPUT_VELOCITY_Y_IN_CENTIMETERS_PER_SECOND.toString())
+ .isEqualTo("BrushBehavior.Source.INPUT_VELOCITY_Y_IN_CENTIMETERS_PER_SECOND")
+ assertThat(BrushBehavior.Source.INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS.toString())
+ .isEqualTo("BrushBehavior.Source.INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS")
+ assertThat(BrushBehavior.Source.PREDICTED_INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS.toString())
+ .isEqualTo("BrushBehavior.Source.PREDICTED_INPUT_DISTANCE_TRAVELED_IN_CENTIMETERS")
+ assertThat(
+ BrushBehavior.Source.INPUT_ACCELERATION_IN_CENTIMETERS_PER_SECOND_SQUARED.toString()
+ )
+ .isEqualTo("BrushBehavior.Source.INPUT_ACCELERATION_IN_CENTIMETERS_PER_SECOND_SQUARED")
+ assertThat(
+ BrushBehavior.Source.INPUT_ACCELERATION_X_IN_CENTIMETERS_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.INPUT_ACCELERATION_X_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source.INPUT_ACCELERATION_Y_IN_CENTIMETERS_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.INPUT_ACCELERATION_Y_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source.INPUT_ACCELERATION_FORWARD_IN_CENTIMETERS_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.INPUT_ACCELERATION_FORWARD_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ )
+ assertThat(
+ BrushBehavior.Source.INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior.Source.INPUT_ACCELERATION_LATERAL_IN_CENTIMETERS_PER_SECOND_SQUARED"
+ )
+ }
+
+ @Test
+ fun targetConstants_areDistinct() {
+ val list =
+ listOf<BrushBehavior.Target>(
+ BrushBehavior.Target.WIDTH_MULTIPLIER,
+ BrushBehavior.Target.HEIGHT_MULTIPLIER,
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ BrushBehavior.Target.SLANT_OFFSET_IN_RADIANS,
+ BrushBehavior.Target.PINCH_OFFSET,
+ BrushBehavior.Target.ROTATION_OFFSET_IN_RADIANS,
+ BrushBehavior.Target.CORNER_ROUNDING_OFFSET,
+ BrushBehavior.Target.POSITION_OFFSET_X_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.POSITION_OFFSET_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.POSITION_OFFSET_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE,
+ BrushBehavior.Target.HUE_OFFSET_IN_RADIANS,
+ BrushBehavior.Target.SATURATION_MULTIPLIER,
+ BrushBehavior.Target.LUMINOSITY,
+ BrushBehavior.Target.OPACITY_MULTIPLIER,
+ )
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun targetHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.Target.WIDTH_MULTIPLIER.hashCode())
+ .isEqualTo(BrushBehavior.Target.WIDTH_MULTIPLIER.hashCode())
+
+ assertThat(BrushBehavior.Target.WIDTH_MULTIPLIER.hashCode())
+ .isNotEqualTo(BrushBehavior.Target.HEIGHT_MULTIPLIER.hashCode())
+ }
+
+ @Test
+ fun targetEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.Target.WIDTH_MULTIPLIER)
+ .isEqualTo(BrushBehavior.Target.WIDTH_MULTIPLIER)
+
+ assertThat(BrushBehavior.Target.WIDTH_MULTIPLIER)
+ .isNotEqualTo(BrushBehavior.Target.HEIGHT_MULTIPLIER)
+ assertThat(BrushBehavior.Target.WIDTH_MULTIPLIER).isNotEqualTo(null)
+ }
+
+ @Test
+ fun targetToString_returnsCorrectString() {
+ assertThat(BrushBehavior.Target.WIDTH_MULTIPLIER.toString())
+ .isEqualTo("BrushBehavior.Target.WIDTH_MULTIPLIER")
+ assertThat(BrushBehavior.Target.HEIGHT_MULTIPLIER.toString())
+ .isEqualTo("BrushBehavior.Target.HEIGHT_MULTIPLIER")
+ assertThat(BrushBehavior.Target.SIZE_MULTIPLIER.toString())
+ .isEqualTo("BrushBehavior.Target.SIZE_MULTIPLIER")
+ assertThat(BrushBehavior.Target.SLANT_OFFSET_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Target.SLANT_OFFSET_IN_RADIANS")
+ assertThat(BrushBehavior.Target.PINCH_OFFSET.toString())
+ .isEqualTo("BrushBehavior.Target.PINCH_OFFSET")
+ assertThat(BrushBehavior.Target.ROTATION_OFFSET_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Target.ROTATION_OFFSET_IN_RADIANS")
+ assertThat(BrushBehavior.Target.CORNER_ROUNDING_OFFSET.toString())
+ .isEqualTo("BrushBehavior.Target.CORNER_ROUNDING_OFFSET")
+ assertThat(BrushBehavior.Target.POSITION_OFFSET_X_IN_MULTIPLES_OF_BRUSH_SIZE.toString())
+ .isEqualTo("BrushBehavior.Target.POSITION_OFFSET_X_IN_MULTIPLES_OF_BRUSH_SIZE")
+ assertThat(BrushBehavior.Target.POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE.toString())
+ .isEqualTo("BrushBehavior.Target.POSITION_OFFSET_Y_IN_MULTIPLES_OF_BRUSH_SIZE")
+ assertThat(
+ BrushBehavior.Target.POSITION_OFFSET_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE.toString()
+ )
+ .isEqualTo("BrushBehavior.Target.POSITION_OFFSET_FORWARD_IN_MULTIPLES_OF_BRUSH_SIZE")
+ assertThat(
+ BrushBehavior.Target.POSITION_OFFSET_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE.toString()
+ )
+ .isEqualTo("BrushBehavior.Target.POSITION_OFFSET_LATERAL_IN_MULTIPLES_OF_BRUSH_SIZE")
+ assertThat(BrushBehavior.Target.HUE_OFFSET_IN_RADIANS.toString())
+ .isEqualTo("BrushBehavior.Target.HUE_OFFSET_IN_RADIANS")
+ assertThat(BrushBehavior.Target.SATURATION_MULTIPLIER.toString())
+ .isEqualTo("BrushBehavior.Target.SATURATION_MULTIPLIER")
+ assertThat(BrushBehavior.Target.LUMINOSITY.toString())
+ .isEqualTo("BrushBehavior.Target.LUMINOSITY")
+ assertThat(BrushBehavior.Target.OPACITY_MULTIPLIER.toString())
+ .isEqualTo("BrushBehavior.Target.OPACITY_MULTIPLIER")
+ }
+
+ @Test
+ fun outOfRangeConstants_areDistinct() {
+ val list =
+ listOf<BrushBehavior.OutOfRange>(
+ BrushBehavior.OutOfRange.CLAMP,
+ BrushBehavior.OutOfRange.REPEAT,
+ BrushBehavior.OutOfRange.MIRROR,
+ )
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun outOfRangeHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.OutOfRange.CLAMP.hashCode())
+ .isEqualTo(BrushBehavior.OutOfRange.CLAMP.hashCode())
+
+ assertThat(BrushBehavior.OutOfRange.CLAMP.hashCode())
+ .isNotEqualTo(BrushBehavior.OutOfRange.REPEAT.hashCode())
+ }
+
+ @Test
+ fun outOfRangeEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.OutOfRange.CLAMP).isEqualTo(BrushBehavior.OutOfRange.CLAMP)
+
+ assertThat(BrushBehavior.OutOfRange.CLAMP).isNotEqualTo(BrushBehavior.OutOfRange.REPEAT)
+ assertThat(BrushBehavior.OutOfRange.CLAMP).isNotEqualTo(null)
+ }
+
+ @Test
+ fun outOfRangeToString_returnsCorrectString() {
+ assertThat(BrushBehavior.OutOfRange.CLAMP.toString())
+ .isEqualTo("BrushBehavior.OutOfRange.CLAMP")
+ assertThat(BrushBehavior.OutOfRange.REPEAT.toString())
+ .isEqualTo("BrushBehavior.OutOfRange.REPEAT")
+ assertThat(BrushBehavior.OutOfRange.MIRROR.toString())
+ .isEqualTo("BrushBehavior.OutOfRange.MIRROR")
+ }
+
+ @Test
+ fun optionalInputPropertyConstants_areDistinct() {
+ val list =
+ listOf<BrushBehavior.OptionalInputProperty>(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.OptionalInputProperty.TILT,
+ BrushBehavior.OptionalInputProperty.ORIENTATION,
+ BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun optionalInputPropertyHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.OptionalInputProperty.PRESSURE.hashCode())
+ .isEqualTo(BrushBehavior.OptionalInputProperty.PRESSURE.hashCode())
+
+ assertThat(BrushBehavior.OptionalInputProperty.PRESSURE.hashCode())
+ .isNotEqualTo(BrushBehavior.OptionalInputProperty.TILT.hashCode())
+ }
+
+ @Test
+ fun optionalInputPropertyEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.OptionalInputProperty.PRESSURE)
+ .isEqualTo(BrushBehavior.OptionalInputProperty.PRESSURE)
+
+ assertThat(BrushBehavior.OptionalInputProperty.PRESSURE)
+ .isNotEqualTo(BrushBehavior.OptionalInputProperty.TILT)
+ assertThat(BrushBehavior.OptionalInputProperty.PRESSURE).isNotEqualTo(null)
+ }
+
+ @Test
+ fun optionalInputPropertyToString_returnsCorrectString() {
+ assertThat(BrushBehavior.OptionalInputProperty.PRESSURE.toString())
+ .isEqualTo("BrushBehavior.OptionalInputProperty.PRESSURE")
+ assertThat(BrushBehavior.OptionalInputProperty.TILT.toString())
+ .isEqualTo("BrushBehavior.OptionalInputProperty.TILT")
+ assertThat(BrushBehavior.OptionalInputProperty.ORIENTATION.toString())
+ .isEqualTo("BrushBehavior.OptionalInputProperty.ORIENTATION")
+ assertThat(BrushBehavior.OptionalInputProperty.TILT_X_AND_Y.toString())
+ .isEqualTo("BrushBehavior.OptionalInputProperty.TILT_X_AND_Y")
+ }
+
+ @Test
+ fun binaryOpConstants_areDistinct() {
+ val list =
+ listOf<BrushBehavior.BinaryOp>(
+ BrushBehavior.BinaryOp.PRODUCT,
+ BrushBehavior.BinaryOp.SUM
+ )
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun binaryOpHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.BinaryOp.PRODUCT.hashCode())
+ .isEqualTo(BrushBehavior.BinaryOp.PRODUCT.hashCode())
+
+ assertThat(BrushBehavior.BinaryOp.PRODUCT.hashCode())
+ .isNotEqualTo(BrushBehavior.BinaryOp.SUM.hashCode())
+ }
+
+ @Test
+ fun binaryOpEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.BinaryOp.PRODUCT).isEqualTo(BrushBehavior.BinaryOp.PRODUCT)
+
+ assertThat(BrushBehavior.BinaryOp.PRODUCT).isNotEqualTo(BrushBehavior.BinaryOp.SUM)
+ assertThat(BrushBehavior.BinaryOp.PRODUCT).isNotEqualTo(null)
+ }
+
+ @Test
+ fun binaryOpToString_returnsCorrectString() {
+ assertThat(BrushBehavior.BinaryOp.PRODUCT.toString())
+ .isEqualTo("BrushBehavior.BinaryOp.PRODUCT")
+ assertThat(BrushBehavior.BinaryOp.SUM.toString()).isEqualTo("BrushBehavior.BinaryOp.SUM")
+ }
+
+ @Test
+ fun dampingSourceConstants_areDistinct() {
+ val list = listOf<BrushBehavior.DampingSource>(BrushBehavior.DampingSource.TIME_IN_SECONDS)
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun dampingSourceHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.DampingSource.TIME_IN_SECONDS.hashCode())
+ .isEqualTo(BrushBehavior.DampingSource.TIME_IN_SECONDS.hashCode())
+ }
+
+ @Test
+ fun dampingSourceEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.DampingSource.TIME_IN_SECONDS)
+ .isEqualTo(BrushBehavior.DampingSource.TIME_IN_SECONDS)
+
+ assertThat(BrushBehavior.DampingSource.TIME_IN_SECONDS).isNotEqualTo(null)
+ }
+
+ @Test
+ fun dampingSourceToString_returnsCorrectString() {
+ assertThat(BrushBehavior.DampingSource.TIME_IN_SECONDS.toString())
+ .isEqualTo("BrushBehavior.DampingSource.TIME_IN_SECONDS")
+ }
+
+ @Test
+ fun sourceNodeConstructor_throwsForNonFiniteSourceValueRange() {
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, Float.NaN, 1f)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.SourceNode(
+ BrushBehavior.Source.NORMALIZED_PRESSURE,
+ 0f,
+ Float.POSITIVE_INFINITY,
+ )
+ }
+ }
+
+ @Test
+ fun sourceNodeConstructor_throwsForEmptySourceValueRange() {
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0.5f, 0.5f)
+ }
+ }
+
+ @Test
+ fun sourceNodeInputs_isEmpty() {
+ val node = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
+ assertThat(node.inputs()).isEmpty()
+ }
+
+ @Test
+ fun sourceNodeToString() {
+ val node = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
+ assertThat(node.toString()).isEqualTo("SourceNode(NORMALIZED_PRESSURE, 0.0, 1.0, CLAMP)")
+ }
+
+ @Test
+ fun sourceNodeEquals_checksEqualityOfValues() {
+ val node1 = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
+ val node2 = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
+ val node3 = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 2f)
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun sourceNodeHashCode_withIdenticalValues_match() {
+ val node1 = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
+ val node2 = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
+ val node3 = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 2f)
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun constantNodeConstructor_throwsForNonFiniteValue() {
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.ConstantNode(Float.POSITIVE_INFINITY)
+ }
+ assertFailsWith<IllegalArgumentException> { BrushBehavior.ConstantNode(Float.NaN) }
+ }
+
+ @Test
+ fun constantNodeInputs_isEmpty() {
+ assertThat(BrushBehavior.ConstantNode(42f).inputs()).isEmpty()
+ }
+
+ @Test
+ fun constantNodeToString() {
+ assertThat(BrushBehavior.ConstantNode(42f).toString()).isEqualTo("ConstantNode(42.0)")
+ }
+
+ @Test
+ fun constantNodeEquals_checksEqualityOfValues() {
+ val node1 = BrushBehavior.ConstantNode(1f)
+ val node2 = BrushBehavior.ConstantNode(1f)
+ val node3 = BrushBehavior.ConstantNode(2f)
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun constantNodeHashCode_withIdenticalValues_match() {
+ val node1 = BrushBehavior.ConstantNode(1f)
+ val node2 = BrushBehavior.ConstantNode(1f)
+ val node3 = BrushBehavior.ConstantNode(2f)
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun fallbackFilterNodeInputs_containsInput() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node =
+ BrushBehavior.FallbackFilterNode(BrushBehavior.OptionalInputProperty.PRESSURE, input)
+ assertThat(node.inputs()).containsExactly(input)
+ }
+
+ @Test
+ fun fallbackFilterNodeToString() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node =
+ BrushBehavior.FallbackFilterNode(BrushBehavior.OptionalInputProperty.PRESSURE, input)
+ assertThat(node.toString()).isEqualTo("FallbackFilterNode(PRESSURE, ConstantNode(0.0))")
+ }
+
+ @Test
+ fun fallbackFilterNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.FallbackFilterNode(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.FallbackFilterNode(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.FallbackFilterNode(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun fallbackFilterNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.FallbackFilterNode(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.FallbackFilterNode(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.FallbackFilterNode(
+ BrushBehavior.OptionalInputProperty.PRESSURE,
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun toolTypeFilterNodeConstructor_throwsForEmptyEnabledToolTypes() {
+ val input = BrushBehavior.ConstantNode(0f)
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.ToolTypeFilterNode(emptySet(), input)
+ }
+ }
+
+ @Test
+ fun toolTypeFilterNodeInputs_containsInput() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.ToolTypeFilterNode(setOf(InputToolType.STYLUS), input)
+ assertThat(node.inputs()).containsExactly(input)
+ }
+
+ @Test
+ fun toolTypeFilterNodeToString() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.ToolTypeFilterNode(setOf(InputToolType.STYLUS), input)
+ assertThat(node.toString())
+ .isEqualTo("ToolTypeFilterNode([InputToolType.STYLUS], ConstantNode(0.0))")
+ }
+
+ @Test
+ fun toolTypeFilterNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.ToolTypeFilterNode(
+ setOf(InputToolType.STYLUS),
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node2 =
+ BrushBehavior.ToolTypeFilterNode(
+ setOf(InputToolType.STYLUS),
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node3 =
+ BrushBehavior.ToolTypeFilterNode(
+ setOf(InputToolType.STYLUS),
+ BrushBehavior.ConstantNode(2f)
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun toolTypeFilterNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.ToolTypeFilterNode(
+ setOf(InputToolType.STYLUS),
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node2 =
+ BrushBehavior.ToolTypeFilterNode(
+ setOf(InputToolType.STYLUS),
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node3 =
+ BrushBehavior.ToolTypeFilterNode(
+ setOf(InputToolType.STYLUS),
+ BrushBehavior.ConstantNode(2f)
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun dampingNodeConstructor_throwsForNonFiniteDampingGap() {
+ val input = BrushBehavior.ConstantNode(0f)
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ Float.POSITIVE_INFINITY,
+ input,
+ )
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, Float.NaN, input)
+ }
+ }
+
+ @Test
+ fun dampingNodeConstructor_throwsForNegativeDampingGap() {
+ val input = BrushBehavior.ConstantNode(0f)
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, -1f, input)
+ }
+ }
+
+ @Test
+ fun dampingNodeInputs_containsInput() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f, input)
+ assertThat(node.inputs()).containsExactly(input)
+ }
+
+ @Test
+ fun dampingNodeToString() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f, input)
+ assertThat(node.toString())
+ .isEqualTo("DampingNode(TIME_IN_SECONDS, 1.0, ConstantNode(0.0))")
+ }
+
+ @Test
+ fun dampingNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ 1f,
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun dampingNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.DampingNode(
+ BrushBehavior.DampingSource.TIME_IN_SECONDS,
+ 1f,
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun responseNodeInputs_containsInput() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.ResponseNode(EasingFunction.Predefined.EASE, input)
+ assertThat(node.inputs()).containsExactly(input)
+ }
+
+ @Test
+ fun responseNodeToString() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.ResponseNode(EasingFunction.Predefined.EASE, input)
+ assertThat(node.toString())
+ .isEqualTo("ResponseNode(EasingFunction.Predefined.EASE, ConstantNode(0.0))")
+ }
+
+ @Test
+ fun responseNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.ResponseNode(
+ EasingFunction.Predefined.EASE,
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node2 =
+ BrushBehavior.ResponseNode(
+ EasingFunction.Predefined.EASE,
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node3 =
+ BrushBehavior.ResponseNode(
+ EasingFunction.Predefined.EASE,
+ BrushBehavior.ConstantNode(2f)
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun responseNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.ResponseNode(
+ EasingFunction.Predefined.EASE,
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node2 =
+ BrushBehavior.ResponseNode(
+ EasingFunction.Predefined.EASE,
+ BrushBehavior.ConstantNode(1f)
+ )
+ val node3 =
+ BrushBehavior.ResponseNode(
+ EasingFunction.Predefined.EASE,
+ BrushBehavior.ConstantNode(2f)
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun binaryOpNodeInputs_containsInputsInOrder() {
+ val firstInput = BrushBehavior.ConstantNode(0f)
+ val secondInput = BrushBehavior.ConstantNode(1f)
+ val node = BrushBehavior.BinaryOpNode(BrushBehavior.BinaryOp.SUM, firstInput, secondInput)
+ assertThat(node.inputs()).containsExactly(firstInput, secondInput).inOrder()
+ }
+
+ @Test
+ fun binaryOpNodeToString() {
+ val firstInput = BrushBehavior.ConstantNode(0f)
+ val secondInput = BrushBehavior.ConstantNode(1f)
+ val node = BrushBehavior.BinaryOpNode(BrushBehavior.BinaryOp.SUM, firstInput, secondInput)
+ assertThat(node.toString())
+ .isEqualTo("BinaryOpNode(SUM, ConstantNode(0.0), ConstantNode(1.0))")
+ }
+
+ @Test
+ fun binaryOpNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.BinaryOpNode(
+ BrushBehavior.BinaryOp.SUM,
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.BinaryOpNode(
+ BrushBehavior.BinaryOp.SUM,
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.BinaryOpNode(
+ BrushBehavior.BinaryOp.SUM,
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun binaryOpNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.BinaryOpNode(
+ BrushBehavior.BinaryOp.SUM,
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.BinaryOpNode(
+ BrushBehavior.BinaryOp.SUM,
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.BinaryOpNode(
+ BrushBehavior.BinaryOp.SUM,
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun targetNodeConstructor_throwsForNonFiniteTargetModifierRange() {
+ val input = BrushBehavior.ConstantNode(0f)
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.TargetNode(BrushBehavior.Target.SIZE_MULTIPLIER, Float.NaN, 1f, input)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ Float.POSITIVE_INFINITY,
+ input,
+ )
+ }
+ }
+
+ @Test
+ fun targetNodeConstructor_throwsForEmptyTargetModifierRange() {
+ val input = BrushBehavior.ConstantNode(0f)
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior.TargetNode(BrushBehavior.Target.SIZE_MULTIPLIER, 0.5f, 0.5f, input)
+ }
+ }
+
+ @Test
+ fun targetNodeInputs_containsInput() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.TargetNode(BrushBehavior.Target.SIZE_MULTIPLIER, 0f, 1f, input)
+ assertThat(node.inputs()).containsExactly(input)
+ }
+
+ @Test
+ fun targetNodeToString() {
+ val input = BrushBehavior.ConstantNode(0f)
+ val node = BrushBehavior.TargetNode(BrushBehavior.Target.SIZE_MULTIPLIER, 0f, 1f, input)
+ assertThat(node.toString())
+ .isEqualTo("TargetNode(SIZE_MULTIPLIER, 0.0, 1.0, ConstantNode(0.0))")
+ }
+
+ @Test
+ fun targetNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ 1f,
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun targetNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ 1f,
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.TargetNode(
+ BrushBehavior.Target.SIZE_MULTIPLIER,
+ 0f,
+ 1f,
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
+ fun brushBehaviorConstructor_withInvalidArguments_throws() {
+ // sourceValueRangeLowerBound not finite
+ val sourceValueRangeLowerBoundError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = Float.NaN, // Not finite.
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(sourceValueRangeLowerBoundError.message).contains("source")
+ assertThat(sourceValueRangeLowerBoundError.message).contains("finite")
+
+ // sourceValueRangeUpperBound not finite
+ val sourceValueRangeUpperBoundError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 1.0f,
+ sourceValueRangeUpperBound = Float.NaN, // Not finite.
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(sourceValueRangeUpperBoundError.message).contains("source")
+ assertThat(sourceValueRangeUpperBoundError.message).contains("finite")
+
+ // sourceValueRangeUpperBound == sourceValueRangeUpperBound
+ val sourceValueRangeError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.5f, // same as upper bound.
+ sourceValueRangeUpperBound = 0.5f, // same as lower bound.
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(sourceValueRangeError.message).contains("source")
+ assertThat(sourceValueRangeError.message).contains("distinct")
+
+ // targetModifierRangeLowerBound not finite
+ val targetModifierRangeLowerBoundError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = Float.NaN, // Not finite.
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(targetModifierRangeLowerBoundError.message).contains("target")
+ assertThat(targetModifierRangeLowerBoundError.message).contains("finite")
+
+ // targetModifierRangeUpperBound not finite
+ val targetModifierRangeUpperBoundError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = Float.NaN, // Not finite.
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(targetModifierRangeUpperBoundError.message).contains("target")
+ assertThat(targetModifierRangeUpperBoundError.message).contains("finite")
+
+ // responseTimeMillis less than 0L
+ val responseTimeMillisError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = -1L, // Less than 0.
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(responseTimeMillisError.message).contains("dampingGap")
+ assertThat(responseTimeMillisError.message).contains("non-negative")
+
+ // enabledToolType contains empty set.
+ val enabledToolTypeError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(),
+ )
+ }
+ assertThat(enabledToolTypeError.message).contains("enabledToolTypes")
+ assertThat(enabledToolTypeError.message).contains("non-empty")
+
+ // source and outOfRangeBehavior combination is invalid (TIME_SINCE_INPUT must use CLAMP)
+ val sourceOutOfRangeBehaviorError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushBehavior(
+ source = BrushBehavior.Source.TIME_SINCE_INPUT_IN_SECONDS,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.REPEAT,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ }
+ assertThat(sourceOutOfRangeBehaviorError.message).contains("TimeSince")
+ assertThat(sourceOutOfRangeBehaviorError.message).contains("kClamp")
+ }
+
+ @Test
+ fun brushBehaviorCopy_withArguments_createsCopyWithChanges() {
+ val behavior1 =
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+ assertThat(behavior1.copy(responseTimeMillis = 3L))
+ .isEqualTo(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 3L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+ )
+ }
+
+ @Test
+ fun brushBehaviorCopy_createsCopy() {
+ val behavior1 =
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+ val behavior2 = behavior1.copy()
+ assertThat(behavior2).isEqualTo(behavior1)
+ assertThat(behavior2).isNotSameInstanceAs(behavior1)
+ }
+
+ @Test
+ fun brushBehaviorToString_returnsReasonableString() {
+ assertThat(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ .toString()
+ )
+ .isEqualTo(
+ "BrushBehavior(source=BrushBehavior.Source.NORMALIZED_PRESSURE, " +
+ "target=BrushBehavior.Target.WIDTH_MULTIPLIER, " +
+ "sourceOutOfRangeBehavior=BrushBehavior.OutOfRange.CLAMP, " +
+ "sourceValueRangeLowerBound=0.0, sourceValueRangeUpperBound=1.0, " +
+ "targetModifierRangeLowerBound=1.0, targetModifierRangeUpperBound=1.75, " +
+ "responseCurve=EasingFunction.Predefined.EASE_IN_OUT, responseTimeMillis=1, " +
+ "enabledToolTypes=[InputToolType.STYLUS], isFallbackFor=null)"
+ )
+ }
+
+ @Test
+ fun brushBehaviorEquals_withIdenticalValues_returnsTrue() {
+ val original =
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+
+ val exact =
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+
+ assertThat(original.equals(exact)).isTrue()
+ }
+
+ @Test
+ fun brushBehaviorEquals_withDifferentValues_returnsFalse() {
+ val original =
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.TILT_IN_RADIANS, // different
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.HEIGHT_MULTIPLIER, // different
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.REPEAT, // different
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.3f, // different
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 0.8f, // different
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.56f, // different
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.99f, // different
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.LINEAR, // different
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 35L, // different
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(
+ original.equals(
+ BrushBehavior(
+ source = BrushBehavior.Source.NORMALIZED_PRESSURE,
+ target = BrushBehavior.Target.WIDTH_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.0f,
+ sourceValueRangeUpperBound = 1.0f,
+ targetModifierRangeLowerBound = 1.0f,
+ targetModifierRangeUpperBound = 1.75f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.CLAMP,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.TOUCH), // different
+ )
+ )
+ )
+ .isFalse()
+ }
+
+ /**
+ * Creates an expected C++ StepFunction BrushBehavior and returns true if every property of the
+ * Kotlin BrushBehavior's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushBehavior.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativeStepBehavior(
+ nativePointerToActualBrushBehavior: Long
+ ): Boolean
+
+ /**
+ * Creates an expected C++ PredefinedFunction BrushBehavior and returns true if every property
+ * of the Kotlin BrushBehavior's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushBehavior.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativePredefinedBehavior(
+ nativePointerToActualBrushBehavior: Long
+ ): Boolean
+
+ /**
+ * Creates an expected C++ CubicBezier BrushBehavior and returns true if every property of the
+ * Kotlin BrushBehavior's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushBehavior.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativeCubicBezierBehavior(
+ nativePointerToActualBrushBehavior: Long
+ ): Boolean
+
+ /**
+ * Creates an expected C++ Linear BrushBehavior and returns true if every property of the Kotlin
+ * BrushBehavior's JNI-created C++ counterpart is equivalent to the expected C++ BrushBehavior.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativeLinearBehavior(
+ nativePointerToActualBrushBehavior: Long
+ ): Boolean
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushCoatTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushCoatTest.kt
new file mode 100644
index 0000000..a3504a3
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushCoatTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
+class BrushCoatTest {
+ @Test
+ fun constructor_withValidArguments_returnsABrushCoat() {
+ assertThat(BrushCoat(customTip, customPaint)).isNotNull()
+ }
+
+ @Test
+ fun constructor_withDefaultArguments_returnsABrushCoat() {
+ assertThat(BrushCoat(BrushTip(), BrushPaint())).isNotNull()
+ }
+
+ @Test
+ fun hashCode_withIdenticalValues_matches() {
+ assertThat(newCustomBrushCoat().hashCode()).isEqualTo(newCustomBrushCoat().hashCode())
+ }
+
+ @Test
+ fun equals_comparesValues() {
+ val brushCoat = BrushCoat(customTip, customPaint)
+ val differentTip = BrushTip()
+ val differentPaint = BrushPaint()
+
+ // same values are equal.
+ assertThat(brushCoat).isEqualTo(BrushCoat(customTip, customPaint))
+
+ // different values are not equal.
+ assertThat(brushCoat).isNotEqualTo(null)
+ assertThat(brushCoat).isNotEqualTo(Any())
+ assertThat(brushCoat).isNotEqualTo(brushCoat.copy(tip = differentTip))
+ assertThat(brushCoat).isNotEqualTo(brushCoat.copy(paint = differentPaint))
+ }
+
+ @Test
+ fun toString_returnsExpectedValues() {
+ assertThat(BrushCoat().toString())
+ .isEqualTo(
+ "BrushCoat(tips=[BrushTip(scale=(1.0, 1.0), " +
+ "cornerRounding=1.0, slant=0.0, pinch=0.0, rotation=0.0, opacityMultiplier=1.0, " +
+ "particleGapDistanceScale=0.0, particleGapDurationMillis=0, behaviors=[])], " +
+ "paint=BrushPaint(textureLayers=[]))"
+ )
+ }
+
+ @Test
+ fun copy_whenSameContents_returnsSameInstance() {
+ val customCoat = BrushCoat(customTip, customPaint)
+
+ // A pure copy returns `this`.
+ val copy = customCoat.copy()
+ assertThat(copy).isSameInstanceAs(customCoat)
+ }
+
+ @Test
+ fun copy_withArguments_createsCopyWithChanges() {
+ val brushCoat = BrushCoat(customTip, customPaint)
+ val differentTip = BrushTip()
+ val differentPaint = BrushPaint()
+
+ assertThat(brushCoat.copy(tip = differentTip))
+ .isEqualTo(BrushCoat(differentTip, customPaint))
+ assertThat(brushCoat.copy(paint = differentPaint))
+ .isEqualTo(BrushCoat(customTip, differentPaint))
+ }
+
+ @Test
+ fun builder_createsExpectedBrushCoat() {
+ val coat = BrushCoat.Builder().setTip(customTip).setPaint(customPaint).build()
+ assertThat(coat).isEqualTo(BrushCoat(customTip, customPaint))
+ }
+
+ /**
+ * Creates an expected C++ BrushCoat with defaults and returns true if every property of the
+ * Kotlin BrushCoat's JNI-created C++ counterpart is equivalent to the expected C++ BrushCoat.
+ */
+ private external fun matchesDefaultCoat(
+ brushCoatNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /**
+ * Creates an expected C++ BrushCoat with custom values and returns true if every property of
+ * the Kotlin BrushCoat's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushCoat.
+ */
+ private external fun matchesMultiBehaviorTipCoat(
+ brushCoatNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /** Brush behavior with every field different from default values. */
+ private val customBehavior =
+ BrushBehavior(
+ source = BrushBehavior.Source.TILT_IN_RADIANS,
+ target = BrushBehavior.Target.HEIGHT_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.MIRROR,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+
+ /** Brush tip with every field different from default values and non-empty behaviors. */
+ private val customTip =
+ BrushTip(
+ scaleX = 0.1f,
+ scaleY = 0.2f,
+ cornerRounding = 0.3f,
+ slant = 0.4f,
+ pinch = 0.5f,
+ rotation = 0.6f,
+ opacityMultiplier = 0.7f,
+ particleGapDistanceScale = 0.8f,
+ particleGapDurationMillis = 9L,
+ listOf<BrushBehavior>(customBehavior),
+ )
+
+ /**
+ * Brush Paint with every field different from default values, including non-empty texture
+ * layers.
+ */
+ private val customPaint =
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(
+ colorTextureUri = "ink://ink/texture:test-one",
+ sizeX = 123.45F,
+ sizeY = 678.90F,
+ offsetX = 0.123f,
+ offsetY = 0.678f,
+ rotation = 0.1f,
+ opacity = 0.123f,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN,
+ BrushPaint.TextureMapping.TILING,
+ ),
+ BrushPaint.TextureLayer(
+ colorTextureUri = "ink://ink/texture:test-two",
+ sizeX = 256F,
+ sizeY = 256F,
+ offsetX = 0.456f,
+ offsetY = 0.567f,
+ rotation = 0.2f,
+ opacity = 0.987f,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN,
+ BrushPaint.TextureMapping.TILING,
+ ),
+ )
+ )
+
+ /** Brush Coat with every field different from default values. */
+ private fun newCustomBrushCoat(): BrushCoat = BrushCoat(customTip, customPaint)
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt
new file mode 100644
index 0000000..a9197d5
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushFamilyTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
+class BrushFamilyTest {
+ @Test
+ fun constructor_withValidArguments_returnsABrushFamily() {
+ assertThat(BrushFamily(customTip, customPaint, customUri)).isNotNull()
+ }
+
+ @Test
+ fun constructor_withDefaultArguments_returnsABrushFamily() {
+ assertThat(BrushFamily(BrushTip(), BrushPaint(), uri = null)).isNotNull()
+ assertThat(BrushFamily(BrushTip(), BrushPaint(), uri = "")).isNotNull()
+ }
+
+ @Test
+ fun constructor_withBadUri_throws() {
+ assertFailsWith<IllegalArgumentException> { BrushFamily(customTip, customPaint, "baduri") }
+ }
+
+ @Test
+ fun hashCode_withIdenticalValues_matches() {
+ assertThat(newCustomBrushFamily().hashCode()).isEqualTo(newCustomBrushFamily().hashCode())
+ }
+
+ @Test
+ fun equals_comparesValues() {
+ val brushFamily = BrushFamily(customTip, customPaint, customUri)
+ val differentCoat = BrushCoat(BrushTip(), BrushPaint())
+ val differentUri = null
+
+ // same values are equal.
+ assertThat(brushFamily).isEqualTo(BrushFamily(customTip, customPaint, customUri))
+
+ // different values are not equal.
+ assertThat(brushFamily).isNotEqualTo(null)
+ assertThat(brushFamily).isNotEqualTo(Any())
+ assertThat(brushFamily).isNotEqualTo(brushFamily.copy(coat = differentCoat))
+ assertThat(brushFamily).isNotEqualTo(brushFamily.copy(uri = differentUri))
+ }
+
+ @Test
+ fun toString_returnsExpectedValues() {
+ assertThat(BrushFamily().toString())
+ .isEqualTo(
+ "BrushFamily(coats=[BrushCoat(tips=[BrushTip(scale=(1.0, 1.0), " +
+ "cornerRounding=1.0, slant=0.0, pinch=0.0, rotation=0.0, opacityMultiplier=1.0, " +
+ "particleGapDistanceScale=0.0, particleGapDurationMillis=0, " +
+ "behaviors=[])], paint=BrushPaint(textureLayers=[]))], uri=null)"
+ )
+ }
+
+ @Test
+ fun copy_whenSameContents_returnsSameInstance() {
+ val customFamily = BrushFamily(customTip, customPaint, customUri)
+
+ // A pure copy returns `this`.
+ val copy = customFamily.copy()
+ assertThat(copy).isSameInstanceAs(customFamily)
+ }
+
+ @Test
+ fun copy_withArguments_createsCopyWithChanges() {
+ val brushFamily = BrushFamily(customTip, customPaint, customUri)
+ val differentCoats = listOf(BrushCoat(BrushTip(), BrushPaint()))
+ val differentUri = null
+
+ assertThat(brushFamily.copy(coats = differentCoats))
+ .isEqualTo(BrushFamily(differentCoats, customUri))
+ assertThat(brushFamily.copy(uri = differentUri))
+ .isEqualTo(BrushFamily(customTip, customPaint, differentUri))
+ }
+
+ @Test
+ fun builder_createsExpectedBrushFamily() {
+ val family = BrushFamily.Builder().setCoat(customTip, customPaint).setUri(customUri).build()
+ assertThat(family).isEqualTo(BrushFamily(customTip, customPaint, customUri))
+ }
+
+ /**
+ * Creates an expected C++ BrushFamily with defaults and returns true if every property of the
+ * Kotlin BrushFamily's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushFamily.
+ */
+ private external fun matchesDefaultFamily(
+ brushFamilyNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /**
+ * Creates an expected C++ BrushFamily with custom values and returns true if every property of
+ * the Kotlin BrushFamily's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushFamily.
+ */
+ private external fun matchesMultiBehaviorTipFamily(
+ brushFamilyNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ private val customUri = "/brush-family:inkpen:1"
+
+ /** Brush behavior with every field different from default values. */
+ private val customBehavior =
+ BrushBehavior(
+ source = BrushBehavior.Source.TILT_IN_RADIANS,
+ target = BrushBehavior.Target.HEIGHT_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.MIRROR,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+
+ /** Brush tip with every field different from default values and non-empty behaviors. */
+ private val customTip =
+ BrushTip(
+ scaleX = 0.1f,
+ scaleY = 0.2f,
+ cornerRounding = 0.3f,
+ slant = 0.4f,
+ pinch = 0.5f,
+ rotation = 0.6f,
+ opacityMultiplier = 0.7f,
+ particleGapDistanceScale = 0.8f,
+ particleGapDurationMillis = 9L,
+ listOf(customBehavior),
+ )
+
+ /**
+ * Brush Paint with every field different from default values, including non-empty texture
+ * layers.
+ */
+ private val customPaint =
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(
+ colorTextureUri = "ink://ink/texture:test-one",
+ sizeX = 123.45F,
+ sizeY = 678.90F,
+ offsetX = 0.123f,
+ offsetY = 0.678f,
+ rotation = 0.1f,
+ opacity = 0.123f,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN,
+ BrushPaint.TextureMapping.TILING,
+ ),
+ BrushPaint.TextureLayer(
+ colorTextureUri = "ink://ink/texture:test-two",
+ sizeX = 256F,
+ sizeY = 256F,
+ offsetX = 0.456f,
+ offsetY = 0.567f,
+ rotation = 0.2f,
+ opacity = 0.987f,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN,
+ BrushPaint.TextureMapping.TILING,
+ ),
+ )
+ )
+
+ /** Brush Family with every field different from default values. */
+ private fun newCustomBrushFamily(): BrushFamily = BrushFamily(customTip, customPaint, customUri)
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushPaintTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushPaintTest.kt
new file mode 100644
index 0000000..1982d5f
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushPaintTest.kt
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.ink.geometry.Angle
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
+class BrushPaintTest {
+
+ // region BrushPaint class tests
+ @Test
+ fun constructor_withValidArguments_returnsABrushPaint() {
+ assertThat(
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(1),
+ sizeX = 123.45F,
+ sizeY = 678.90F,
+ offsetX = 0.1f,
+ offsetY = 0.2f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ opacity = 0.3f,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN,
+ BrushPaint.TextureMapping.TILING,
+ ),
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(2),
+ sizeX = 256F,
+ sizeY = 256F,
+ offsetX = 0.8f,
+ offsetY = 0.9f,
+ rotation = Angle.HALF_TURN_RADIANS,
+ opacity = 0.7f,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ BrushPaint.TextureOrigin.FIRST_STROKE_INPUT,
+ BrushPaint.TextureMapping.TILING,
+ ),
+ )
+ )
+ )
+ .isNotNull()
+ }
+
+ @Test
+ fun constructor_withDefaultArguments_returnsABrushPaint() {
+ assertThat(BrushPaint()).isNotNull()
+ }
+
+ @Test
+ fun hashCode_withIdenticalValues_matches() {
+ assertThat(BrushPaint(listOf(makeTestTextureLayer())).hashCode())
+ .isEqualTo(BrushPaint(listOf(makeTestTextureLayer())).hashCode())
+ }
+
+ @Test
+ fun equals_comparesValues() {
+ val customPaint = makeTestPaint()
+ val defaultPaint = BrushPaint()
+ // same values are equal.
+ assertThat(customPaint).isEqualTo(makeTestPaint())
+
+ // different values are not equal.
+ assertThat(customPaint).isNotEqualTo(null)
+ assertThat(customPaint).isNotEqualTo(Any())
+ assertThat(customPaint).isNotEqualTo(defaultPaint)
+ }
+
+ @Test
+ fun toString_returnsExpectedValues() {
+ val string = makeTestPaint().toString()
+ assertThat(string).contains("BrushPaint")
+ assertThat(string).contains("textureLayers")
+ }
+
+ // endregion
+
+ // region TextureLayer class tests
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun textureLayerConstructor_withInvalidSizes_throwsIllegalArgumentException() {
+ val fakeValidUri = makeTestTextureUri()
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, -32F, 64F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, 32F, -64F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, -32F, -64F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, 0F, 128F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, 128F, 0F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, Float.NaN, 128F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, 128F, Float.NaN)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, Float.POSITIVE_INFINITY, 128F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, 128F, Float.POSITIVE_INFINITY)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, Float.NEGATIVE_INFINITY, 128F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, 128F, Float.NEGATIVE_INFINITY)
+ }
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun textureLayerConstructor_withInvalidOffsetX_throwsIllegalArgumentException() {
+ val fakeValidUri = makeTestTextureUri()
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, offsetX = Float.NaN)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, offsetX = -0.001f)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, offsetX = 1.001f)
+ }
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun textureLayerConstructor_withInvalidOffsetY_throwsIllegalArgumentException() {
+ val fakeValidUri = makeTestTextureUri()
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, offsetY = Float.NaN)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, offsetY = -0.001f)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, offsetY = 1.001f)
+ }
+ }
+
+ @Test
+ fun textureLayerConstructor_withInvalidRotation_throwsIllegalArgumentException() {
+ val fakeValidUri = makeTestTextureUri()
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, rotation = Float.NaN)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(
+ fakeValidUri,
+ sizeX = 1f,
+ sizeY = 1f,
+ rotation = Float.POSITIVE_INFINITY,
+ )
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(
+ fakeValidUri,
+ sizeX = 1f,
+ sizeY = 1f,
+ rotation = Float.NEGATIVE_INFINITY,
+ )
+ }
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun textureLayerConstructor_withInvalidOpacity_throwsIllegalArgumentException() {
+ val fakeValidUri = makeTestTextureUri()
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, opacity = Float.NaN)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, opacity = -0.001f)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ BrushPaint.TextureLayer(fakeValidUri, sizeX = 1f, sizeY = 1f, opacity = 1.001f)
+ }
+ }
+
+ @Test
+ fun textureLayerHashCode_withIdenticalValues_matches() {
+ assertThat(makeTestTextureLayer().hashCode()).isEqualTo(makeTestTextureLayer().hashCode())
+ }
+
+ @Test
+ fun textureLayerEquals_checksEqualityOfValues() {
+ val layer =
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(),
+ sizeX = 128F,
+ sizeY = 128F,
+ offsetX = 0.1f,
+ offsetY = 0.2f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ opacity = 0.3f,
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ BrushPaint.TextureOrigin.LAST_STROKE_INPUT,
+ BrushPaint.TextureMapping.WINDING,
+ BrushPaint.BlendMode.SRC_IN,
+ )
+
+ // same values.
+ assertThat(layer)
+ .isEqualTo(
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(),
+ sizeX = 128F,
+ sizeY = 128F,
+ offsetX = 0.1f,
+ offsetY = 0.2f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ opacity = 0.3f,
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ BrushPaint.TextureOrigin.LAST_STROKE_INPUT,
+ BrushPaint.TextureMapping.WINDING,
+ BrushPaint.BlendMode.SRC_IN,
+ )
+ )
+
+ // different values.
+ assertThat(layer).isNotEqualTo(null)
+ assertThat(layer).isNotEqualTo(Any())
+ assertThat(layer).isNotEqualTo(layer.copy(colorTextureUri = makeTestTextureUri(2)))
+ assertThat(layer).isNotEqualTo(layer.copy(sizeX = 999F))
+ assertThat(layer).isNotEqualTo(layer.copy(sizeY = 999F))
+ assertThat(layer).isNotEqualTo(layer.copy(offsetX = 0.999F))
+ assertThat(layer).isNotEqualTo(layer.copy(offsetY = 0.999F))
+ assertThat(layer).isNotEqualTo(layer.copy(rotation = Angle.HALF_TURN_RADIANS))
+ assertThat(layer).isNotEqualTo(layer.copy(opacity = 0.999f))
+ assertThat(layer)
+ .isNotEqualTo(layer.copy(sizeUnit = BrushPaint.TextureSizeUnit.STROKE_COORDINATES))
+ assertThat(layer)
+ .isNotEqualTo(layer.copy(origin = BrushPaint.TextureOrigin.FIRST_STROKE_INPUT))
+ assertThat(layer).isNotEqualTo(layer.copy(mapping = BrushPaint.TextureMapping.TILING))
+ assertThat(layer).isNotEqualTo(layer.copy(blendMode = BrushPaint.BlendMode.MODULATE))
+ }
+
+ @Test
+ fun textureLayerCopy_createsCopy() {
+ val layer = makeTestTextureLayer()
+ val copy = layer.copy()
+
+ // Pure copy returns `this`.
+ assertThat(copy).isSameInstanceAs(layer)
+ }
+
+ @Test
+ fun textureLayerCopy_withArguments_createsCopyWithChanges() {
+ val originalLayer =
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(),
+ sizeX = 128F,
+ sizeY = 128F,
+ offsetX = 0.1f,
+ offsetY = 0.2f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ opacity = 0.3f,
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ BrushPaint.TextureOrigin.FIRST_STROKE_INPUT,
+ BrushPaint.TextureMapping.WINDING,
+ BrushPaint.BlendMode.SRC_IN,
+ )
+ val changedSizeX = originalLayer.copy(sizeX = 999F)
+
+ // sizeX changed.
+ assertThat(changedSizeX).isNotEqualTo(originalLayer)
+ assertThat(changedSizeX.sizeX).isNotEqualTo(originalLayer.sizeX)
+
+ assertThat(changedSizeX)
+ .isEqualTo(
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(),
+ sizeX = 999F, // Changed
+ sizeY = 128F,
+ offsetX = 0.1f,
+ offsetY = 0.2f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ opacity = 0.3f,
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ BrushPaint.TextureOrigin.FIRST_STROKE_INPUT,
+ BrushPaint.TextureMapping.WINDING,
+ BrushPaint.BlendMode.SRC_IN,
+ )
+ )
+ }
+
+ @Test
+ fun textureLayerToString_returnsExpectedValues() {
+ val string = makeTestTextureLayer().toString()
+ assertThat(string).contains("TextureLayer")
+ assertThat(string).contains("colorTextureUri")
+ assertThat(string).contains("size")
+ assertThat(string).contains("offset")
+ assertThat(string).contains("rotation")
+ assertThat(string).contains("opacity")
+ assertThat(string).contains("sizeUnit")
+ assertThat(string).contains("origin")
+ assertThat(string).contains("mapping")
+ assertThat(string).contains("blendMode")
+ }
+
+ // endregion
+
+ // region SizeUnit class tests
+ @Test
+ fun sizeUnitConstants_areDistinct() {
+ val set =
+ setOf(
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ BrushPaint.TextureSizeUnit.STROKE_SIZE,
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES,
+ )
+ assertThat(set).hasSize(3)
+ }
+
+ @Test
+ fun sizeUnitHashCode_withIdenticalValues_match() {
+ assertThat(BrushPaint.TextureSizeUnit.STROKE_COORDINATES.hashCode())
+ .isEqualTo(BrushPaint.TextureSizeUnit.STROKE_COORDINATES.hashCode())
+ }
+
+ @Test
+ fun sizeUnitEquals_checksEqualityOfValues() {
+ assertThat(BrushPaint.TextureSizeUnit.STROKE_COORDINATES)
+ .isEqualTo(BrushPaint.TextureSizeUnit.STROKE_COORDINATES)
+ assertThat(BrushPaint.TextureSizeUnit.STROKE_COORDINATES)
+ .isNotEqualTo(BrushPaint.TextureSizeUnit.BRUSH_SIZE)
+ }
+
+ @Test
+ fun sizeUnitToString_returnsCorrectString() {
+ assertThat(BrushPaint.TextureSizeUnit.BRUSH_SIZE.toString())
+ .isEqualTo("BrushPaint.TextureSizeUnit.BRUSH_SIZE")
+ assertThat(BrushPaint.TextureSizeUnit.STROKE_SIZE.toString())
+ .isEqualTo("BrushPaint.TextureSizeUnit.STROKE_SIZE")
+ assertThat(BrushPaint.TextureSizeUnit.STROKE_COORDINATES.toString())
+ .isEqualTo("BrushPaint.TextureSizeUnit.STROKE_COORDINATES")
+ }
+
+ // endregion
+
+ // region Origin class tests
+ @Test
+ fun originConstants_areDistint() {
+ val set =
+ setOf(
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN,
+ BrushPaint.TextureOrigin.FIRST_STROKE_INPUT,
+ BrushPaint.TextureOrigin.LAST_STROKE_INPUT,
+ )
+ assertThat(set).hasSize(3)
+ }
+
+ @Test
+ fun originHashCode_withIdenticalValues_match() {
+ assertThat(BrushPaint.TextureOrigin.FIRST_STROKE_INPUT.hashCode())
+ .isEqualTo(BrushPaint.TextureOrigin.FIRST_STROKE_INPUT.hashCode())
+ }
+
+ @Test
+ fun originEquals_checksEqualityOfValues() {
+ assertThat(BrushPaint.TextureOrigin.FIRST_STROKE_INPUT)
+ .isEqualTo(BrushPaint.TextureOrigin.FIRST_STROKE_INPUT)
+ assertThat(BrushPaint.TextureOrigin.FIRST_STROKE_INPUT)
+ .isNotEqualTo(BrushPaint.TextureOrigin.LAST_STROKE_INPUT)
+ }
+
+ @Test
+ fun originToString_returnsCorrectString() {
+ assertThat(BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN.toString())
+ .isEqualTo("BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN")
+ assertThat(BrushPaint.TextureOrigin.FIRST_STROKE_INPUT.toString())
+ .isEqualTo("BrushPaint.TextureOrigin.FIRST_STROKE_INPUT")
+ assertThat(BrushPaint.TextureOrigin.LAST_STROKE_INPUT.toString())
+ .isEqualTo("BrushPaint.TextureOrigin.LAST_STROKE_INPUT")
+ }
+
+ // endregion
+
+ // region Mapping class tests
+ @Test
+ fun mappingConstants_areDistint() {
+ val set = setOf(BrushPaint.TextureMapping.TILING, BrushPaint.TextureMapping.WINDING)
+ assertThat(set).hasSize(2)
+ }
+
+ @Test
+ fun mappingHashCode_withIdenticalValues_match() {
+ assertThat(BrushPaint.TextureMapping.TILING.hashCode())
+ .isEqualTo(BrushPaint.TextureMapping.TILING.hashCode())
+ }
+
+ @Test
+ fun mappingEquals_checksEqualityOfValues() {
+ assertThat(BrushPaint.TextureMapping.TILING).isEqualTo(BrushPaint.TextureMapping.TILING)
+ assertThat(BrushPaint.TextureMapping.TILING).isNotEqualTo(BrushPaint.TextureMapping.WINDING)
+ }
+
+ @Test
+ fun mappingToString_returnsCorrectString() {
+ assertThat(BrushPaint.TextureMapping.TILING.toString())
+ .isEqualTo("BrushPaint.TextureMapping.TILING")
+ assertThat(BrushPaint.TextureMapping.WINDING.toString())
+ .isEqualTo("BrushPaint.TextureMapping.WINDING")
+ }
+
+ // endregion
+
+ // region BlendMode class tests
+ @Test
+ fun textureBlendModeConstants_areDistinct() {
+ val set =
+ setOf(
+ BrushPaint.BlendMode.MODULATE,
+ BrushPaint.BlendMode.DST_IN,
+ BrushPaint.BlendMode.DST_OUT,
+ BrushPaint.BlendMode.SRC_ATOP,
+ BrushPaint.BlendMode.SRC_IN,
+ BrushPaint.BlendMode.SRC_OVER,
+ )
+ assertThat(set).hasSize(6)
+ }
+
+ @Test
+ fun textureBlendModeHashCode_withIdenticalValues_match() {
+ assertThat(BrushPaint.BlendMode.MODULATE.hashCode())
+ .isEqualTo(BrushPaint.BlendMode.MODULATE.hashCode())
+ }
+
+ @Test
+ fun textureBlendModeEquals_checksEqualityOfValues() {
+ assertThat(BrushPaint.BlendMode.MODULATE).isEqualTo(BrushPaint.BlendMode.MODULATE)
+ assertThat(BrushPaint.BlendMode.MODULATE).isNotEqualTo(BrushPaint.BlendMode.SRC_OVER)
+ }
+
+ @Test
+ fun textureBlendModeToString_returnsCorrectString() {
+ assertThat(BrushPaint.BlendMode.MODULATE.toString()).contains("MODULATE")
+ assertThat(BrushPaint.BlendMode.DST_IN.toString()).contains("DST_IN")
+ assertThat(BrushPaint.BlendMode.DST_OUT.toString()).contains("DST_OUT")
+ assertThat(BrushPaint.BlendMode.SRC_ATOP.toString()).contains("SRC_ATOP")
+ assertThat(BrushPaint.BlendMode.SRC_IN.toString()).contains("SRC_IN")
+ assertThat(BrushPaint.BlendMode.SRC_OVER.toString()).contains("SRC_OVER")
+ }
+
+ // endregion
+
+ private external fun matchesNativeCustomPaint(
+ brushPaintNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ private fun makeTestTextureUri(version: Int = 0) =
+ "ink://ink/texture:test-texture" + if (version == 0) "" else ":" + version
+
+ private fun makeTestTextureLayer() =
+ BrushPaint.TextureLayer(
+ colorTextureUri = makeTestTextureUri(),
+ sizeX = 128F,
+ sizeY = 128F,
+ offsetX = 0.1f,
+ offsetY = 0.2f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ opacity = 0.3f,
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ BrushPaint.TextureOrigin.FIRST_STROKE_INPUT,
+ BrushPaint.TextureMapping.WINDING,
+ BrushPaint.BlendMode.SRC_IN,
+ )
+
+ private fun makeTestPaint() = BrushPaint(listOf(makeTestTextureLayer()))
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTest.kt
index 4e1045f..ca8c4d4 100644
--- a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTest.kt
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,20 +17,392 @@
package androidx.ink.brush
import androidx.ink.brush.color.Color
-import kotlin.test.Test
-import kotlin.test.assertEquals
+import androidx.ink.brush.color.colorspace.ColorSpaces
+import androidx.ink.brush.color.toArgb
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
class BrushTest {
+ private val size = 10F
+ private val epsilon = 1F
+ private val color = Color(red = 230, green = 115, blue = 140, alpha = 255)
+ private val family = BrushFamily(uri = "/brush-family:inkpen:1")
+
@Test
- fun testSetAndGetColor() {
- val originalColor = Color.Cyan.value.toLong()
- val brush = Brush(color = originalColor, size = 2.5f)
- assertEquals(brush.color, originalColor)
+ fun constructor_withValidArguments_returnsABrush() {
+ val brush = Brush.createWithColorLong(family, color.value.toLong(), size, epsilon)
+ assertThat(brush).isNotNull()
+ assertThat(brush.family).isEqualTo(family)
+ assertThat(brush.colorLong).isEqualTo(color.value.toLong())
+ assertThat(brush.colorIntArgb).isEqualTo(color.toArgb())
+ assertThat(brush.colorLong).isEqualTo(color.value.toLong())
+ assertThat(brush.size).isEqualTo(size)
+ assertThat(brush.epsilon).isEqualTo(epsilon)
}
@Test
- fun testSetAndGetSize() {
- val brush = Brush(color = Color.DarkGray.value.toLong(), size = 2.5f)
- assertEquals(brush.size, 2.5f)
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withBadSize_willThrow() {
+ assertFailsWith<IllegalArgumentException> {
+ Brush(family, color, -2F, epsilon) // non-positive size.
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ Brush(family, color, Float.POSITIVE_INFINITY, epsilon) // non-finite size.
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ Brush(family, color, Float.NaN, epsilon) // non-finite size.
+ }
}
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withBadEpsilon_willThrow() {
+ assertFailsWith<IllegalArgumentException> {
+ Brush(family, color, size, -2F) // non-positive epsilon.
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ Brush(family, color, size, Float.POSITIVE_INFINITY) // non-finite epsilon.
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ Brush(family, color, size, Float.NaN) // non-finite epsilon.
+ }
+ }
+
+ @Test
+ fun colorAccessors_areAllEquivalent() {
+ val color = Color(red = 230, green = 115, blue = 140, alpha = 196)
+ val brush = Brush.createWithColorLong(family, color.value.toLong(), size, epsilon)
+
+ assertThat(brush.colorIntArgb).isEqualTo(color.toArgb())
+ assertThat(brush.colorLong).isEqualTo(color.value.toLong())
+ }
+
+ @Test
+ fun withColorIntArgb_withLowAlpha_returnsBrushWithCorrectColor() {
+ val brush = Brush.createWithColorIntArgb(family, 0x12345678, size, epsilon)
+ assertThat(brush.colorIntArgb).isEqualTo(0x12345678)
+ }
+
+ @Test
+ fun withColorIntArgb_withHighAlpha_returnsBrushWithCorrectColor() {
+ val brush = Brush.createWithColorIntArgb(family, 0xAA123456.toInt(), size, epsilon)
+ assertThat(brush.colorIntArgb).isEqualTo(0xAA123456.toInt())
+ }
+
+ @Test
+ fun withColorLong_returnsBrushWithCorrectColor() {
+ val colorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.DisplayP3).value.toLong()
+ val brush = Brush.createWithColorLong(family, colorLong, size, epsilon)
+ assertThat(brush.colorLong).isEqualTo(colorLong)
+ }
+
+ @Test
+ fun withColorLong_inUnsupportedColorSpace_returnsBrushWithConvertedColor() {
+ val colorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.AdobeRgb).value.toLong()
+ val brush = Brush.createWithColorLong(family, colorLong, size, epsilon)
+
+ val expectedColor = Color(colorLong.toULong()).convert(ColorSpaces.DisplayP3)
+ assertThat(brush.colorLong).isEqualTo(expectedColor.value.toLong())
+ assertThat(brush.colorIntArgb).isEqualTo(expectedColor.toArgb())
+ }
+
+ @Test
+ fun equals_returnsTrueForIdenticalBrushes() {
+ val brush = Brush(family, color, size, epsilon)
+ val otherBrush = Brush(family, color, size, epsilon)
+ assertThat(brush == brush).isTrue()
+ assertThat(brush == otherBrush).isTrue()
+ assertThat(otherBrush == brush).isTrue()
+ }
+
+ @Test
+ fun hashCode_isEqualForIdenticalBrushes() {
+ val brush = Brush(family, color, size, epsilon)
+ val otherBrush = Brush(family, color, size, epsilon)
+ assertThat(brush == brush).isTrue()
+ assertThat(brush == otherBrush).isTrue()
+ assertThat(otherBrush == brush).isTrue()
+ }
+
+ @Test
+ fun equals_returnsFalseIfAnyFieldsDiffer() {
+ val brush = Brush(family, color, size, epsilon)
+
+ val differentFamilyBrush =
+ Brush(BrushFamily(uri = "/brush-family:pencil:1"), color, size, epsilon)
+ assertThat(brush == differentFamilyBrush).isFalse()
+ assertThat(differentFamilyBrush == brush).isFalse()
+ assertThat(brush != differentFamilyBrush).isTrue()
+ assertThat(differentFamilyBrush != brush).isTrue()
+
+ val otherColor =
+ Color(red = 1F, green = 0F, blue = 0F, alpha = 1F, colorSpace = ColorSpaces.DisplayP3)
+ .value
+ .toLong()
+ val differentcolorBrush = Brush.createWithColorLong(family, otherColor, size, epsilon)
+ assertThat(brush == differentcolorBrush).isFalse()
+ assertThat(differentcolorBrush == brush).isFalse()
+ assertThat(brush != differentcolorBrush).isTrue()
+ assertThat(differentcolorBrush != brush).isTrue()
+
+ val differentSizeBrush = Brush(family, color, 9.0f, epsilon)
+ assertThat(brush == differentSizeBrush).isFalse()
+ assertThat(differentSizeBrush == brush).isFalse()
+ assertThat(brush != differentSizeBrush).isTrue()
+ assertThat(differentSizeBrush != brush).isTrue()
+
+ val differentEpsilonBrush = Brush(family, color, size, 1.1f)
+ assertThat(brush == differentEpsilonBrush).isFalse()
+ assertThat(differentEpsilonBrush == brush).isFalse()
+ assertThat(brush != differentEpsilonBrush).isTrue()
+ assertThat(differentEpsilonBrush != brush).isTrue()
+ }
+
+ @Test
+ fun hashCode_differsIfAnyFieldsDiffer() {
+ val brush = Brush(family, color, size, epsilon)
+
+ val differentFamilyBrush =
+ Brush(BrushFamily(uri = "/brush-family:pencil:1"), color, size, epsilon)
+ assertThat(differentFamilyBrush.hashCode()).isNotEqualTo(brush.hashCode())
+
+ val otherColor =
+ Color(red = 1F, green = 0F, blue = 0F, alpha = 1F, colorSpace = ColorSpaces.DisplayP3)
+ .value
+ .toLong()
+ val differentcolorBrush = Brush.createWithColorLong(family, otherColor, size, epsilon)
+ assertThat(differentcolorBrush.hashCode()).isNotEqualTo(brush.hashCode())
+
+ val differentSizeBrush = Brush(family, color, 9.0f, epsilon)
+ assertThat(differentSizeBrush.hashCode()).isNotEqualTo(brush.hashCode())
+
+ val differentEpsilonBrush = Brush(family, color, size, 1.1f)
+ assertThat(differentEpsilonBrush.hashCode()).isNotEqualTo(brush.hashCode())
+ }
+
+ @Test
+ fun copy_returnsTheSameBrush() {
+ val originalBrush = buildTestBrush()
+
+ val newBrush = originalBrush.copy()
+
+ // A pure copy returns `this`.
+ assertThat(newBrush).isSameInstanceAs(originalBrush)
+ }
+
+ @Test
+ fun copy_withChangedBrushFamily_returnsCopyWithDifferentBrushFamily() {
+ val originalBrush = buildTestBrush()
+
+ val newBrush = originalBrush.copy(family = BrushFamily())
+
+ assertThat(newBrush).isNotEqualTo(originalBrush)
+ assertThat(newBrush.family).isNotEqualTo(originalBrush.family)
+
+ // The new brush has the original color, size and epsilon.
+ assertThat(newBrush.colorLong).isEqualTo(originalBrush.colorLong)
+ assertThat(newBrush.size).isEqualTo(originalBrush.size)
+ assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
+ }
+
+ @Test
+ fun copyWithColorIntArgb_withLowAlpha_returnsCopyWithThatColor() {
+ val originalBrush = buildTestBrush()
+
+ val newBrush = originalBrush.copyWithColorIntArgb(colorIntArgb = 0x12345678)
+
+ assertThat(newBrush).isNotEqualTo(originalBrush)
+ assertThat(newBrush.colorLong).isNotEqualTo(originalBrush.colorLong)
+ assertThat(newBrush.colorIntArgb).isEqualTo(0x12345678)
+
+ // The new brush has the original family, size and epsilon.
+ assertThat(newBrush.family).isSameInstanceAs(originalBrush.family)
+ assertThat(newBrush.size).isEqualTo(originalBrush.size)
+ assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
+ }
+
+ @Test
+ fun copyWithColorIntArgb_withHighAlpha_returnsCopyWithThatColor() {
+ val originalBrush = buildTestBrush()
+
+ val newBrush = originalBrush.copyWithColorIntArgb(colorIntArgb = 0xAA123456.toInt())
+
+ assertThat(newBrush).isNotEqualTo(originalBrush)
+ assertThat(newBrush.colorLong).isNotEqualTo(originalBrush.colorLong)
+ assertThat(newBrush.colorIntArgb).isEqualTo(0xAA123456.toInt())
+
+ // The new brush has the original family, size and epsilon.
+ assertThat(newBrush.family).isSameInstanceAs(originalBrush.family)
+ assertThat(newBrush.size).isEqualTo(originalBrush.size)
+ assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
+ }
+
+ @Test
+ fun copyWithColorLong_withChangedColor_returnsCopyWithThatColor() {
+ val originalBrush = buildTestBrush()
+
+ val newColor = Color(red = 255, green = 230, blue = 115, alpha = 140).value.toLong()
+ val newBrush = originalBrush.copyWithColorLong(colorLong = newColor)
+
+ assertThat(newBrush).isNotEqualTo(originalBrush)
+ assertThat(newBrush.colorLong).isNotEqualTo(originalBrush.colorLong)
+ assertThat(newBrush.colorLong).isEqualTo(newColor)
+
+ // The new brush has the original family, size and epsilon.
+ assertThat(newBrush.family).isSameInstanceAs(originalBrush.family)
+ assertThat(newBrush.size).isEqualTo(originalBrush.size)
+ assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
+ }
+
+ @Test
+ fun copyWithColorLong_inUnsupportedColorSpace_returnsCopyWithConvertedColor() {
+ val originalBrush = buildTestBrush()
+
+ val newColor = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.AdobeRgb).value.toLong()
+ val newBrush = originalBrush.copyWithColorLong(colorLong = newColor)
+
+ val expectedColor = Color(newColor.toULong()).convert(ColorSpaces.DisplayP3)
+ assertThat(newBrush.colorLong).isEqualTo(expectedColor.value.toLong())
+ assertThat(newBrush.colorIntArgb).isEqualTo(expectedColor.toArgb())
+ }
+
+ @Test
+ fun brushBuilderBuild_withColorIntWithLowAlpha_createsExpectedBrush() {
+ val testBrush = buildTestBrush()
+
+ val builtBrush =
+ Brush.builder()
+ .setFamily(testBrush.family)
+ .setColorIntArgb(0x12345678)
+ .setSize(9f)
+ .setEpsilon(0.9f)
+ .build()
+
+ assertThat(builtBrush.family).isEqualTo(testBrush.family)
+ assertThat(builtBrush.colorIntArgb).isEqualTo(0x12345678)
+ assertThat(builtBrush.size).isEqualTo(9f)
+ assertThat(builtBrush.epsilon).isEqualTo(0.9f)
+ }
+
+ @Test
+ fun brushBuilderBuild_withColorIntWithHighAlpha_createsExpectedBrush() {
+ val testBrush = buildTestBrush()
+
+ val builtBrush =
+ Brush.builder()
+ .setFamily(testBrush.family)
+ .setColorIntArgb(0xAA123456.toInt())
+ .setSize(9f)
+ .setEpsilon(0.9f)
+ .build()
+
+ assertThat(builtBrush.family).isEqualTo(testBrush.family)
+ assertThat(builtBrush.colorIntArgb).isEqualTo(0xAA123456.toInt())
+ assertThat(builtBrush.size).isEqualTo(9f)
+ assertThat(builtBrush.epsilon).isEqualTo(0.9f)
+ }
+
+ @Test
+ fun brushBuilderBuild_withColorLong_createsExpectedBrush() {
+ val testBrush = buildTestBrush()
+ val testColorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.DisplayP3).value.toLong()
+
+ val builtBrush =
+ Brush.builder()
+ .setFamily(testBrush.family)
+ .setColorLong(testColorLong)
+ .setSize(9f)
+ .setEpsilon(0.9f)
+ .build()
+
+ assertThat(builtBrush.family).isEqualTo(testBrush.family)
+ assertThat(builtBrush.colorLong).isEqualTo(testColorLong)
+ assertThat(builtBrush.size).isEqualTo(9f)
+ assertThat(builtBrush.epsilon).isEqualTo(0.9f)
+ }
+
+ @Test
+ fun brushBuilderBuild_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ val testBrush = buildTestBrush()
+ val testColorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.AdobeRgb).value.toLong()
+
+ val builtBrush =
+ Brush.builder()
+ .setFamily(testBrush.family)
+ .setColorLong(testColorLong)
+ .setSize(9f)
+ .setEpsilon(0.9f)
+ .build()
+
+ val expectedColor = Color(testColorLong.toULong()).convert(ColorSpaces.DisplayP3)
+ assertThat(builtBrush.family).isEqualTo(testBrush.family)
+ assertThat(builtBrush.colorLong).isEqualTo(expectedColor.value.toLong())
+ assertThat(builtBrush.size).isEqualTo(9f)
+ assertThat(builtBrush.epsilon).isEqualTo(0.9f)
+ }
+
+ /**
+ * Creates an expected C++ Brush with default brush family/color and returns true if every
+ * property of the Kotlin Brush's JNI-created C++ counterpart is equivalent to the expected C++
+ * Brush.
+ */
+ private external fun matchesDefaultBrush(
+ actualBrushNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /**
+ * Creates an expected C++ Brush with custom values and returns true if every property of the
+ * Kotlin Brush's JNI-created C++ counterpart is equivalent to the expected C++ Brush.
+ */
+ private external fun matchesCustomBrush(
+ actualBrushNativePointer: Long
+ ): Boolean // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+
+ /** Brush with every field different from default values. */
+ private fun buildTestBrush(): Brush =
+ Brush(
+ BrushFamily(
+ tip =
+ BrushTip(
+ 0.1f,
+ 0.2f,
+ 0.3f,
+ 0.4f,
+ 0.5f,
+ 0.6f,
+ 0.7f,
+ 0.8f,
+ 9L,
+ listOf(
+ BrushBehavior(
+ source = BrushBehavior.Source.TILT_IN_RADIANS,
+ target = BrushBehavior.Target.HEIGHT_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.MIRROR,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+ ),
+ ),
+ paint = BrushPaint(),
+ uri = "/brush-family:marker:1",
+ ),
+ color,
+ 13F,
+ 0.1234F,
+ )
}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTipTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTipTest.kt
new file mode 100644
index 0000000..2e54338
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushTipTest.kt
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.ink.geometry.Angle
+import com.google.common.truth.Truth.assertThat
+import kotlin.IllegalArgumentException
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
+class BrushTipTest {
+ /** Brush behavior with every field different from default values. */
+ private val customBehavior =
+ BrushBehavior(
+ source = BrushBehavior.Source.TILT_IN_RADIANS,
+ target = BrushBehavior.Target.HEIGHT_MULTIPLIER,
+ sourceValueRangeLowerBound = 0.2f,
+ sourceValueRangeUpperBound = .8f,
+ targetModifierRangeLowerBound = 1.1f,
+ targetModifierRangeUpperBound = 1.7f,
+ sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.MIRROR,
+ responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
+ responseTimeMillis = 1L,
+ enabledToolTypes = setOf(InputToolType.STYLUS),
+ isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
+ )
+
+ @Test
+ fun constructor_returnsExpectedValues() {
+ val brushTip = BrushTip()
+ assertThat(brushTip.scaleX).isEqualTo(1f)
+ assertThat(brushTip.scaleY).isEqualTo(1f)
+ assertThat(brushTip.cornerRounding).isEqualTo(1f)
+ assertThat(brushTip.slant).isEqualTo(Angle.ZERO)
+ assertThat(brushTip.pinch).isEqualTo(0.0f)
+ assertThat(brushTip.rotation).isEqualTo(Angle.ZERO)
+ assertThat(brushTip.opacityMultiplier).isEqualTo(1.0f)
+ assertThat(brushTip.particleGapDistanceScale).isEqualTo(0.0f)
+ assertThat(brushTip.particleGapDurationMillis).isEqualTo(0L)
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidScaleX_throws() {
+ val infinityError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(scaleX = Float.POSITIVE_INFINITY) }
+ assertThat(infinityError).hasMessageThat().contains("scale")
+ assertThat(infinityError).hasMessageThat().contains("finite")
+
+ val nanError = assertFailsWith<IllegalArgumentException> { BrushTip(scaleX = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("scale")
+ assertThat(nanError).hasMessageThat().contains("finite")
+
+ val negativeError = assertFailsWith<IllegalArgumentException> { BrushTip(scaleX = -1.0f) }
+ assertThat(negativeError).hasMessageThat().contains("scale")
+ assertThat(negativeError).hasMessageThat().contains("non-negative")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidScaleY_throws() {
+ val infinityError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(scaleY = Float.POSITIVE_INFINITY) }
+ assertThat(infinityError).hasMessageThat().contains("scale")
+ assertThat(infinityError).hasMessageThat().contains("finite")
+
+ val nanError = assertFailsWith<IllegalArgumentException> { BrushTip(scaleY = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("scale")
+ assertThat(nanError).hasMessageThat().contains("finite")
+
+ val negativeError = assertFailsWith<IllegalArgumentException> { BrushTip(scaleY = -1.0f) }
+ assertThat(negativeError).hasMessageThat().contains("scale")
+ assertThat(negativeError).hasMessageThat().contains("non-negative")
+ }
+
+ @Test
+ fun constructor_withZeroScale_throws() {
+ val zeroError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(scaleX = 0f, scaleY = 0f) }
+ assertThat(zeroError).hasMessageThat().contains("at least one value must be positive.")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidCornerRounding_throws() {
+ val nanError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(cornerRounding = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("corner_rounding")
+ assertThat(nanError).hasMessageThat().contains("in the interval [0, 1]")
+
+ val lowError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(cornerRounding = -0.5f) }
+ assertThat(lowError).hasMessageThat().contains("corner_rounding")
+ assertThat(lowError).hasMessageThat().contains("in the interval [0, 1]")
+
+ val highError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(cornerRounding = 1.1f) }
+ assertThat(highError).hasMessageThat().contains("corner_rounding")
+ assertThat(highError).hasMessageThat().contains("in the interval [0, 1]")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidSlant_throws() {
+ val nanError = assertFailsWith<IllegalArgumentException> { BrushTip(slant = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("slant")
+ assertThat(nanError).hasMessageThat().contains("finite")
+
+ val lowError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(slant = -Angle.HALF_TURN_RADIANS) }
+ assertThat(lowError).hasMessageThat().contains("slant")
+ assertThat(lowError).hasMessageThat().contains("interval [-pi/2, pi/2]")
+
+ val highError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(slant = Angle.HALF_TURN_RADIANS) }
+ assertThat(highError).hasMessageThat().contains("slant")
+ assertThat(highError).hasMessageThat().contains("interval [-pi/2, pi/2]")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidPinch_throws() {
+ val nanError = assertFailsWith<IllegalArgumentException> { BrushTip(pinch = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("pinch")
+ assertThat(nanError).hasMessageThat().contains("interval [0, 1]")
+
+ val lowError = assertFailsWith<IllegalArgumentException> { BrushTip(pinch = -0.1f) }
+ assertThat(lowError).hasMessageThat().contains("pinch")
+ assertThat(lowError).hasMessageThat().contains("interval [0, 1]")
+
+ val highError = assertFailsWith<IllegalArgumentException> { BrushTip(pinch = 1.1f) }
+ assertThat(highError).hasMessageThat().contains("pinch")
+ assertThat(highError).hasMessageThat().contains("interval [0, 1]")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidOpacitiyMultiplier_throws() {
+ val nanError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(opacityMultiplier = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("opacity_multiplier")
+ assertThat(nanError).hasMessageThat().contains("interval [0, 2]")
+
+ val lowError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(opacityMultiplier = -0.1f) }
+ assertThat(lowError).hasMessageThat().contains("opacity_multiplier")
+ assertThat(lowError).hasMessageThat().contains("interval [0, 2]")
+
+ val highError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(opacityMultiplier = 2.1f) }
+ assertThat(highError).hasMessageThat().contains("opacity_multiplier")
+ assertThat(highError).hasMessageThat().contains("interval [0, 2]")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidParticleGapDistanceScale_throws() {
+ val infinityError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushTip(particleGapDistanceScale = Float.POSITIVE_INFINITY)
+ }
+ assertThat(infinityError).hasMessageThat().contains("particle_gap_distance_scale")
+ assertThat(infinityError).hasMessageThat().contains("finite")
+
+ val nanError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushTip(particleGapDistanceScale = Float.NaN)
+ }
+ assertThat(nanError).hasMessageThat().contains("particle_gap_distance_scale")
+ assertThat(nanError).hasMessageThat().contains("finite")
+
+ val negativeError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(particleGapDistanceScale = -1.0f) }
+ assertThat(negativeError).hasMessageThat().contains("particle_gap_distance_scale")
+ assertThat(negativeError).hasMessageThat().contains("non-negative")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidParticleGapDurationMillis_throws() {
+ val negativeError =
+ assertFailsWith<IllegalArgumentException> { BrushTip(particleGapDurationMillis = -1L) }
+ assertThat(negativeError).hasMessageThat().contains("particle_gap_duration")
+ assertThat(negativeError).hasMessageThat().contains("non-negative")
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun constructor_withInvalidRotation_throws() {
+ val nanError = assertFailsWith<IllegalArgumentException> { BrushTip(rotation = Float.NaN) }
+ assertThat(nanError).hasMessageThat().contains("rotation")
+ assertThat(nanError).hasMessageThat().contains("finite")
+
+ val infinityError =
+ assertFailsWith<IllegalArgumentException> {
+ BrushTip(rotation = Float.POSITIVE_INFINITY)
+ }
+ assertThat(infinityError).hasMessageThat().contains("rotation")
+ assertThat(infinityError).hasMessageThat().contains("finite")
+ }
+
+ @Test
+ fun hashCode_withIdenticalValues_matches() {
+ // same values.
+ assertThat(
+ BrushTip(
+ 1f,
+ 2f,
+ 0.3f,
+ Angle.QUARTER_TURN_RADIANS,
+ 0.4f,
+ Angle.ZERO,
+ 0.7f,
+ 0.5f,
+ 100L,
+ emptyList(),
+ )
+ .hashCode()
+ )
+ .isEqualTo(
+ BrushTip(
+ 1f,
+ 2f,
+ 0.3f,
+ Angle.QUARTER_TURN_RADIANS,
+ 0.4f,
+ Angle.ZERO,
+ 0.7f,
+ 0.5f,
+ 100L,
+ emptyList(),
+ )
+ .hashCode()
+ )
+ }
+
+ @Test
+ fun equals_comparesValues() {
+ val brushTip = BrushTip()
+ // same values.
+ assertThat(brushTip).isEqualTo(BrushTip())
+
+ // different values.
+ assertThat(brushTip).isNotEqualTo(null)
+ assertThat(brushTip).isNotEqualTo(Any())
+ assertThat(brushTip).isNotEqualTo(BrushTip(scaleX = 2f))
+ assertThat(brushTip).isNotEqualTo(BrushTip(scaleY = 2f))
+ assertThat(brushTip).isNotEqualTo(BrushTip(cornerRounding = 0.2f))
+ assertThat(brushTip).isNotEqualTo(BrushTip(slant = Angle.QUARTER_TURN_RADIANS))
+ assertThat(brushTip).isNotEqualTo(BrushTip(pinch = 0.2f))
+ assertThat(brushTip).isNotEqualTo(BrushTip(rotation = Angle.HALF_TURN_RADIANS))
+ assertThat(brushTip).isNotEqualTo(BrushTip(opacityMultiplier = 0.7f))
+ assertThat(brushTip).isNotEqualTo(BrushTip(behaviors = listOf(customBehavior)))
+ }
+
+ @Test
+ fun toString_returnsExpectedValues() {
+ assertThat(BrushTip().toString())
+ .isEqualTo(
+ "BrushTip(scale=(1.0, 1.0), cornerRounding=1.0, slant=0.0, " +
+ "pinch=0.0, rotation=0.0, opacityMultiplier=1.0, " +
+ "particleGapDistanceScale=0.0, particleGapDurationMillis=0, " +
+ "behaviors=[])"
+ )
+ }
+
+ @Test
+ fun copy_withArguments_createsCopyWithChanges() {
+ val tip1 =
+ BrushTip(
+ scaleX = 2f,
+ scaleY = 3f,
+ cornerRounding = 0.5f,
+ slant = Angle.ZERO,
+ pinch = 0.5f,
+ rotation = Angle.ZERO,
+ opacityMultiplier = 0.7f,
+ particleGapDistanceScale = 0.8f,
+ particleGapDurationMillis = 9L,
+ behaviors = listOf(customBehavior),
+ )
+
+ assertThat(tip1.copy(scaleX = 3f))
+ .isEqualTo(
+ BrushTip(
+ scaleX = 3f,
+ scaleY = 3f,
+ cornerRounding = 0.5f,
+ slant = Angle.ZERO,
+ pinch = 0.5f,
+ rotation = Angle.ZERO,
+ opacityMultiplier = 0.7f,
+ particleGapDistanceScale = 0.8f,
+ particleGapDurationMillis = 9L,
+ behaviors = listOf(customBehavior),
+ )
+ )
+ }
+
+ @Test
+ fun copy_createsCopy() {
+ val tip1 =
+ BrushTip(
+ scaleX = 3f,
+ scaleY = 3f,
+ cornerRounding = 0.5f,
+ slant = Angle.ZERO,
+ pinch = 0.5f,
+ rotation = Angle.ZERO,
+ opacityMultiplier = 0.7f,
+ particleGapDistanceScale = 0.8f,
+ particleGapDurationMillis = 9L,
+ behaviors = listOf(customBehavior),
+ )
+
+ val tip2 = tip1.copy()
+
+ assertThat(tip2).isEqualTo(tip1)
+ assertThat(tip2.nativePointer).isNotEqualTo(tip1.nativePointer)
+ assertThat(tip2).isNotSameInstanceAs(tip1)
+ }
+
+ @Test
+ fun builder_createsExpectedBrushTip() {
+ val tip =
+ BrushTip.Builder()
+ .setScaleX(0.1f)
+ .setScaleY(0.2f)
+ .setCornerRounding(0.3f)
+ .setSlant(0.4f)
+ .setPinch(0.5f)
+ .setRotation(0.6f)
+ .setOpacityMultiplier(0.7f)
+ .setParticleGapDistanceScale(0.8f)
+ .setParticleGapDurationMillis(9L)
+ .setBehaviors(listOf(customBehavior))
+ .build()
+
+ assertThat(tip)
+ .isEqualTo(
+ BrushTip(0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 9L, listOf(customBehavior))
+ )
+ }
+
+ /**
+ * Creates an expected C++ BrushTip with no behaviors and returns true if every property of the
+ * Kotlin BrushTip's JNI-created C++ counterpart is equivalent to the expected C++ BrushTip.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativeNoBehaviorTip(nativePointerToActualBrushTip: Long): Boolean
+
+ /**
+ * Creates an expected C++ BrushTip with a single behavior and returns true if every property of
+ * the Kotlin BrushTip's JNI-created C++ counterpart is equivalent to the expected C++ BrushTip.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativeSingleBehaviorTip(
+ nativePointerToActualBrushTip: Long
+ ): Boolean
+
+ /**
+ * Creates an expected C++ BrushTip with multiple behaviors and returns true if every property
+ * of the Kotlin BrushTip's JNI-created C++ counterpart is equivalent to the expected C++
+ * BrushTip.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun matchesNativeMultiBehaviorTip(nativePointerToActualBrushTip: Long): Boolean
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/ColorExtensionsTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/ColorExtensionsTest.kt
new file mode 100644
index 0000000..e85b662
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/ColorExtensionsTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.ink.brush.color.Color as ComposeColor
+import androidx.ink.brush.color.colorspace.ColorSpaces as ComposeColorSpaces
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ColorExtensionsTest {
+ @Test
+ fun composeColorToColorInInkSupportedColorSpace_withSupportedColorSpace_returnsSameColor() {
+ val composeColor =
+ ComposeColor(
+ red = 0f,
+ green = 1f,
+ blue = 100f / 255f,
+ alpha = 155f / 255f,
+ colorSpace = ComposeColorSpaces.DisplayP3,
+ )
+
+ val convertedColor = composeColor.toColorInInkSupportedColorSpace()
+
+ // The color space is supported, so the color is the same. It's not the same instance,
+ // though,
+ // since ComposeColor is a value class.
+ assertThat(convertedColor).isEqualTo(composeColor)
+ }
+
+ @Test
+ fun composeColorToColorInInkSupportedColorSpace_withUnsupportedColorSpace_convertsToDisplayP3() {
+ val composeColor =
+ ComposeColor(
+ red = 0f,
+ green = 1f,
+ blue = 100f / 255f,
+ alpha = 155f / 255f,
+ colorSpace = ComposeColorSpaces.AdobeRgb,
+ )
+
+ val convertedColor = composeColor.toColorInInkSupportedColorSpace()
+
+ // The color space got converted to DISPLAY_P3. The color is out of gamut, so it got scaled
+ // into
+ // the Display P3 gamut.
+ assertThat(convertedColor.colorSpace).isEqualTo(ComposeColorSpaces.DisplayP3)
+ assertThat(convertedColor.red).isWithin(0.001f).of(0f)
+ assertThat(convertedColor.green).isWithin(0.001f).of(0.9795f)
+ assertThat(convertedColor.blue).isWithin(0.001f).of(0.4204f)
+ assertThat(convertedColor.alpha).isWithin(0.001f).of(155f / 255f)
+ }
+
+ @Test
+ fun composeColorSpaceToInkColorSpaceId_converts() {
+ assertThat(ComposeColorSpaces.Srgb.toInkColorSpaceId()).isEqualTo(0)
+ assertThat(ComposeColorSpaces.DisplayP3.toInkColorSpaceId()).isEqualTo(1)
+ }
+
+ @Test
+ fun composeColorSpaceToInkColorSpaceId_withUnsupportedColorSpace_throws() {
+ assertFailsWith<IllegalArgumentException> {
+ ComposeColorSpaces.AdobeRgb.toInkColorSpaceId()
+ }
+ }
+}
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/EasingFunctionTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/EasingFunctionTest.kt
new file mode 100644
index 0000000..8bf0023
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/EasingFunctionTest.kt
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import androidx.ink.brush.EasingFunction.Predefined
+import androidx.ink.geometry.ImmutableVec
+import com.google.common.truth.Truth.assertThat
+import kotlin.IllegalArgumentException
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(JUnit4::class)
+class EasingFunctionTest {
+
+ @Test
+ fun predefinedConstants_areDistinct() {
+ val set =
+ setOf<EasingFunction.Predefined>(
+ EasingFunction.Predefined.LINEAR,
+ EasingFunction.Predefined.EASE,
+ EasingFunction.Predefined.EASE_IN,
+ EasingFunction.Predefined.EASE_OUT,
+ EasingFunction.Predefined.EASE_IN_OUT,
+ EasingFunction.Predefined.STEP_START,
+ EasingFunction.Predefined.STEP_END,
+ )
+ assertThat(set.size).isEqualTo(7)
+ }
+
+ @Test
+ fun predefinedToString_returnsCorrectString() {
+ assertThat(Predefined.LINEAR.toString()).isEqualTo("EasingFunction.Predefined.LINEAR")
+ assertThat(Predefined.EASE.toString()).isEqualTo("EasingFunction.Predefined.EASE")
+ assertThat(EasingFunction.Predefined.EASE_IN.toString())
+ .isEqualTo("EasingFunction.Predefined.EASE_IN")
+ assertThat(EasingFunction.Predefined.EASE_OUT.toString())
+ .isEqualTo("EasingFunction.Predefined.EASE_OUT")
+ assertThat(EasingFunction.Predefined.EASE_IN_OUT.toString())
+ .isEqualTo("EasingFunction.Predefined.EASE_IN_OUT")
+ assertThat(EasingFunction.Predefined.STEP_START.toString())
+ .isEqualTo("EasingFunction.Predefined.STEP_START")
+ assertThat(EasingFunction.Predefined.STEP_END.toString())
+ .isEqualTo("EasingFunction.Predefined.STEP_END")
+ }
+
+ @Test
+ fun predefinedHashCode_withIdenticalValues_matches() {
+ assertThat(EasingFunction.Predefined.LINEAR.hashCode())
+ .isEqualTo(EasingFunction.Predefined.LINEAR.hashCode())
+
+ assertThat(EasingFunction.Predefined.LINEAR.hashCode())
+ .isNotEqualTo(EasingFunction.Predefined.STEP_END.hashCode())
+ }
+
+ @Test
+ fun predefinedEquals_checksEqualityOfValues() {
+ assertThat(EasingFunction.Predefined.LINEAR).isEqualTo(EasingFunction.Predefined.LINEAR)
+ assertThat(EasingFunction.Predefined.LINEAR).isNotEqualTo(EasingFunction.Predefined.EASE)
+ assertThat(EasingFunction.Predefined.LINEAR).isNotEqualTo(null)
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun cubicBezierConstructor_requiresValuesInRange() {
+ // arg x1 outside range [0,1]
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(x1 = 1.1F, 1F, 3F, 4F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(x1 = -0.2F, 3F, 1F, 4F)
+ }
+ // arg x2 outside range [0,1]
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, 3F, x2 = 2F, 4F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, 3F, x2 = -0.5F, 4F)
+ }
+ }
+
+ @Test
+ @Suppress("Range") // Testing error cases.
+ fun cubicBezierConstructor_requiresFiniteValues() {
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(x1 = Float.POSITIVE_INFINITY, 1F, 1F, 1F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(x1 = Float.NaN, 1F, 1F, 1F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, 1F, x2 = Float.POSITIVE_INFINITY, 1F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, 1F, x2 = Float.NaN, 1F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, y1 = Float.POSITIVE_INFINITY, 1F, 1F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, y1 = Float.NaN, 1F, 1F)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, 1F, 1F, y2 = Float.POSITIVE_INFINITY)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.CubicBezier(1F, 1F, 1F, y2 = Float.NaN)
+ }
+ }
+
+ @Test
+ fun cubicBezierHashCode_withIdenticalValues_matches() {
+ assertThat(EasingFunction.CubicBezier(1f, 2f, 0.3f, 4f).hashCode())
+ .isEqualTo(EasingFunction.CubicBezier(1f, 2f, 0.3f, 4f).hashCode())
+ }
+
+ @Test
+ fun cubicBezierEquals_checksEqualityOfValues() {
+ val original = EasingFunction.CubicBezier(1f, 2f, 0.3f, 4f)
+
+ // Equal
+ assertThat(original).isEqualTo(original) // Same instance.
+ assertThat(original).isEqualTo(EasingFunction.CubicBezier(1f, 2f, 0.3f, 4f)) // Same values.
+
+ // Not equal
+ assertThat(original).isNotEqualTo(null)
+ assertThat(original).isNotEqualTo(EasingFunction.Predefined.LINEAR) // Different type.
+ assertThat(original)
+ .isNotEqualTo(EasingFunction.CubicBezier(0.9f, 0.8f, 0.7f, 0.6f)) // Values.
+ }
+
+ @Test
+ fun cubicBezierToString_returnsReasonableString() {
+ assertThat(EasingFunction.CubicBezier(1f, 2f, 0.3f, 4f).toString())
+ .isEqualTo("EasingFunction.CubicBezier(x1=1.0, y1=2.0, x2=0.3, y2=4.0)")
+ }
+
+ @Test
+ fun linearConstructor_requiresXValuesInRange() {
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(-0.1F, 0.5F)))
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(1.1F, 0.5F)))
+ }
+ }
+
+ @Test
+ fun linearConstructor_requiresFiniteValues() {
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(Float.POSITIVE_INFINITY, 0.5F)))
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(Float.NaN, 0.5F)))
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(0.5F, Float.POSITIVE_INFINITY)))
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(0.5F, Float.NaN)))
+ }
+ }
+
+ @Test
+ fun linearConstructor_requiresSortedXValues() {
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Linear(listOf(ImmutableVec(0.75F, 0.5F), ImmutableVec(0.25F, 0.5F)))
+ }
+ }
+
+ @Test
+ fun linearHashCode_withIdenticalValues_matches() {
+ assertThat(EasingFunction.Linear(listOf(ImmutableVec(0.25f, 0.1f))).hashCode())
+ .isEqualTo(EasingFunction.Linear(listOf(ImmutableVec(0.25f, 0.1f))).hashCode())
+ }
+
+ @Test
+ fun linearEquals_checksEqualityOfValues() {
+ val original =
+ EasingFunction.Linear(listOf(ImmutableVec(0.25f, 0.1f), ImmutableVec(0.75f, 0.9f)))
+
+ // Equal
+ assertThat(original).isEqualTo(original) // Same instance.
+ assertThat(original)
+ .isEqualTo(
+ EasingFunction.Linear(listOf(ImmutableVec(0.25f, 0.1f), ImmutableVec(0.75f, 0.9f)))
+ ) // Same values.
+
+ // Not equal
+ assertThat(original).isNotEqualTo(null)
+ assertThat(original).isNotEqualTo(EasingFunction.Predefined.LINEAR) // Different type.
+ assertThat(original)
+ .isNotEqualTo(
+ EasingFunction.Linear(listOf(ImmutableVec(0.25f, 0.1f)))
+ ) // Shorter list of points.
+ assertThat(original)
+ .isNotEqualTo(
+ EasingFunction.Linear(listOf(ImmutableVec(0.15f, 0.1f), ImmutableVec(0.75f, 0.9f)))
+ ) // Different point values.
+ assertThat(original)
+ .isNotEqualTo(
+ EasingFunction.Linear(
+ listOf(
+ ImmutableVec(0.25f, 0.1f),
+ ImmutableVec(0.75f, 0.9f),
+ ImmutableVec(0.9f, 0.5f)
+ )
+ )
+ ) // Longer list of points.
+ }
+
+ @Test
+ fun linearToString_returnsReasonableString() {
+ val string =
+ EasingFunction.Linear(listOf(ImmutableVec(0.25f, 0.1f), ImmutableVec(0.75f, 0.9f)))
+ .toString()
+ assertThat(string).contains("EasingFunction.Linear")
+ assertThat(string).contains("Vec")
+ assertThat(string).contains("0.25")
+ assertThat(string).contains("0.1")
+ assertThat(string).contains("0.75")
+ assertThat(string).contains("0.9")
+ }
+
+ @Test
+ fun stepPositionConstants_areDistinct() {
+ val set =
+ setOf<EasingFunction.StepPosition>(
+ EasingFunction.StepPosition.JUMP_START,
+ EasingFunction.StepPosition.JUMP_END,
+ EasingFunction.StepPosition.JUMP_NONE,
+ EasingFunction.StepPosition.JUMP_BOTH,
+ )
+ assertThat(set.size).isEqualTo(4)
+ }
+
+ @Test
+ fun stepPositionToString_returnsReasonableString() {
+ assertThat(EasingFunction.StepPosition.JUMP_START.toString())
+ .isEqualTo("EasingFunction.StepPosition.JUMP_START")
+ assertThat(EasingFunction.StepPosition.JUMP_END.toString())
+ .isEqualTo("EasingFunction.StepPosition.JUMP_END")
+ assertThat(EasingFunction.StepPosition.JUMP_BOTH.toString())
+ .isEqualTo("EasingFunction.StepPosition.JUMP_BOTH")
+ assertThat(EasingFunction.StepPosition.JUMP_NONE.toString())
+ .isEqualTo("EasingFunction.StepPosition.JUMP_NONE")
+ }
+
+ @Test
+ fun stepPositionHashCode_withIdenticalValues_matches() {
+ assertThat(EasingFunction.StepPosition.JUMP_START.hashCode())
+ .isEqualTo(EasingFunction.StepPosition.JUMP_START.hashCode())
+
+ assertThat(EasingFunction.StepPosition.JUMP_START.hashCode())
+ .isNotEqualTo(EasingFunction.StepPosition.JUMP_END.hashCode())
+ }
+
+ @Test
+ fun steps_withInvalidStepCount_throws() {
+ // Step count less than zero throws.
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Steps(0, EasingFunction.StepPosition.JUMP_START)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Steps(-1, EasingFunction.StepPosition.JUMP_START)
+ }
+
+ // Step count not greater than 1 for JUMP_NONE throws.
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Steps(0, EasingFunction.StepPosition.JUMP_NONE)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_NONE)
+ }
+
+ assertThat(EasingFunction.Steps(2, EasingFunction.StepPosition.JUMP_NONE)).isNotNull()
+ assertThat(EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_START)).isNotNull()
+ }
+
+ @Test
+ fun stepsHashCode_withSameValues_match() {
+ assertThat(EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_START).hashCode())
+ .isEqualTo(EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_START).hashCode())
+
+ // Different step count.
+ assertThat(EasingFunction.Steps(2, EasingFunction.StepPosition.JUMP_START).hashCode())
+ .isNotEqualTo(
+ EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_START).hashCode()
+ )
+
+ // Different stepPosition.
+ assertThat(EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_START).hashCode())
+ .isNotEqualTo(EasingFunction.Steps(1, EasingFunction.StepPosition.JUMP_END).hashCode())
+ }
+
+ @Test
+ fun stepsEquals_checksEqualityOfValues() {
+ val original = EasingFunction.Steps(2, EasingFunction.StepPosition.JUMP_START)
+
+ // Equal
+ assertThat(original).isEqualTo(original) // Same instance.
+ assertThat(original)
+ .isEqualTo(
+ EasingFunction.Steps(2, EasingFunction.StepPosition.JUMP_START)
+ ) // Same values.
+
+ // Not equal
+ assertThat(original).isNotEqualTo(null)
+ // Different type.
+ assertThat(original).isNotEqualTo(EasingFunction.Predefined.LINEAR)
+ // Different count.
+ assertThat(original)
+ .isNotEqualTo(EasingFunction.Steps(3, EasingFunction.StepPosition.JUMP_START))
+ // Different position.
+ assertThat(original)
+ .isNotEqualTo(EasingFunction.Steps(2, EasingFunction.StepPosition.JUMP_END))
+ }
+
+ @Test
+ fun stepsToString_returnsReasonableString() {
+ assertThat(EasingFunction.Steps(2, EasingFunction.StepPosition.JUMP_START).toString())
+ .isEqualTo(
+ "EasingFunction.Steps(stepCount=2, stepPosition=EasingFunction.StepPosition.JUMP_START)"
+ )
+ }
+}
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/Placeholder.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/Empty.kt
similarity index 68%
rename from room/room-paging/src/commonMain/kotlin/androidx/room/paging/Placeholder.kt
rename to ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/Empty.kt
index fb1781e..0c09d26 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/Placeholder.kt
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/Empty.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,6 +14,10 @@
* limitations under the License.
*/
-package androidx.room.paging
-// empty file to trigger klib creation
-// see: https://youtrack.jetbrains.com/issue/KT-52344
+package androidx.ink.brush
+
+/**
+ * Exists solely so that brush_test_jni_lib can have non-empty srcs, in order to aggregate its
+ * runtime_deps for convenience to the test targets.
+ */
+private class Empty
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/InputToolTypeTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/InputToolTypeTest.kt
new file mode 100644
index 0000000..209fa49
--- /dev/null
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/InputToolTypeTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.brush
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.IllegalArgumentException
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class InputToolTypeTest {
+
+ @Test
+ fun constants_areDistinct() {
+ val set =
+ setOf(
+ InputToolType.UNKNOWN,
+ InputToolType.MOUSE,
+ InputToolType.STYLUS,
+ InputToolType.TOUCH
+ )
+ assertThat(set).hasSize(4)
+ }
+
+ @Test
+ fun toString_returnsCorrectString() {
+ assertThat(InputToolType.UNKNOWN.toString()).isEqualTo("InputToolType.UNKNOWN")
+ assertThat(InputToolType.MOUSE.toString()).isEqualTo("InputToolType.MOUSE")
+ assertThat(InputToolType.TOUCH.toString()).isEqualTo("InputToolType.TOUCH")
+ assertThat(InputToolType.STYLUS.toString()).isEqualTo("InputToolType.STYLUS")
+ }
+
+ @Test
+ fun hashCode_withIdenticalValues_matches() {
+ assertThat(InputToolType.MOUSE.hashCode()).isEqualTo(InputToolType.MOUSE.hashCode())
+
+ assertThat(InputToolType.MOUSE.hashCode()).isNotEqualTo(InputToolType.TOUCH.hashCode())
+ }
+
+ @Test
+ fun equals_checksEqualityOfValues() {
+ assertThat(InputToolType.MOUSE).isEqualTo(InputToolType.MOUSE)
+ assertThat(InputToolType.MOUSE).isNotEqualTo(InputToolType.TOUCH)
+ assertThat(InputToolType.MOUSE).isNotEqualTo(null)
+ }
+
+ @Test
+ fun from_createsCorrectInputToolType() {
+ assertThat(InputToolType.from(0)).isEqualTo(InputToolType.UNKNOWN)
+ assertThat(InputToolType.from(1)).isEqualTo(InputToolType.MOUSE)
+ assertThat(InputToolType.from(2)).isEqualTo(InputToolType.TOUCH)
+ assertThat(InputToolType.from(3)).isEqualTo(InputToolType.STYLUS)
+ assertFailsWith<IllegalArgumentException> { InputToolType.from(4) }
+ }
+}
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 3ee8cd9..7d352f46 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -1,6 +1,22 @@
// Signature format: 4.0
package androidx.ink.geometry {
+ public abstract class AffineTransform {
+ method public final androidx.ink.geometry.MutableParallelogram applyTransform(androidx.ink.geometry.Box box, androidx.ink.geometry.MutableParallelogram outParallelogram);
+ method public final androidx.ink.geometry.MutableParallelogram applyTransform(androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.MutableParallelogram outParallelogram);
+ method public final androidx.ink.geometry.MutableSegment applyTransform(androidx.ink.geometry.Segment segment, androidx.ink.geometry.MutableSegment outSegment);
+ method public final androidx.ink.geometry.MutableTriangle applyTransform(androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.MutableTriangle outTriangle);
+ method public final androidx.ink.geometry.MutableVec applyTransform(androidx.ink.geometry.Vec vec, androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.MutableAffineTransform computeInverse(androidx.ink.geometry.MutableAffineTransform outAffineTransform);
+ method @Size(min=6L) public final float[] getValues();
+ method @Size(min=6L) public final float[] getValues(optional @Size(min=6L) float[] outArray);
+ field public static final androidx.ink.geometry.AffineTransform.Companion Companion;
+ field public static final androidx.ink.geometry.ImmutableAffineTransform IDENTITY;
+ }
+
+ public static final class AffineTransform.Companion {
+ }
+
public final class Angle {
method @androidx.ink.geometry.AngleRadiansFloat public static float degreesToRadians(@androidx.ink.geometry.AngleDegreesFloat float degrees);
method @FloatRange(from=0.0, to=androidx.ink.geometry.Angle.FULL_TURN_RADIANS_DOUBLE) @androidx.ink.geometry.AngleRadiansFloat public static float normalized(@androidx.ink.geometry.AngleRadiansFloat float radians);
@@ -19,5 +35,368 @@
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface AngleRadiansFloat {
}
+ public abstract class Box {
+ method public final androidx.ink.geometry.MutableVec computeCenter(androidx.ink.geometry.MutableVec outVec);
+ method public final void computeCorners(androidx.ink.geometry.MutableVec outVecXMinYMin, androidx.ink.geometry.MutableVec outVecXMaxYMin, androidx.ink.geometry.MutableVec outVecXMaxYMax, androidx.ink.geometry.MutableVec outVecXMinYMax);
+ method public final operator boolean contains(androidx.ink.geometry.Box otherBox);
+ method public final operator boolean contains(androidx.ink.geometry.Vec point);
+ method @FloatRange(from=0.0) public final float getHeight();
+ method @FloatRange(from=0.0) public final float getWidth();
+ method public abstract float getXMax();
+ method public abstract float getXMin();
+ method public abstract float getYMax();
+ method public abstract float getYMin();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Box other, @FloatRange(from=0.0) float tolerance);
+ property @FloatRange(from=0.0) public final float height;
+ property @FloatRange(from=0.0) public final float width;
+ property public abstract float xMax;
+ property public abstract float xMin;
+ property public abstract float yMax;
+ property public abstract float yMin;
+ field public static final androidx.ink.geometry.Box.Companion Companion;
+ }
+
+ public static final class Box.Companion {
+ }
+
+ public final class BoxAccumulator {
+ ctor public BoxAccumulator();
+ ctor public BoxAccumulator(androidx.ink.geometry.Box box);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
+ method public androidx.ink.geometry.Box? getBox();
+ method public boolean isAlmostEqual(androidx.ink.geometry.BoxAccumulator other, @FloatRange(from=0.0) float tolerance);
+ method public boolean isEmpty();
+ method public androidx.ink.geometry.BoxAccumulator populateFrom(androidx.ink.geometry.BoxAccumulator input);
+ method public androidx.ink.geometry.BoxAccumulator reset();
+ property public final androidx.ink.geometry.Box? box;
+ }
+
+ public final class ImmutableAffineTransform extends androidx.ink.geometry.AffineTransform {
+ ctor public ImmutableAffineTransform(float m00, float m10, float m20, float m01, float m11, float m21);
+ ctor public ImmutableAffineTransform(@Size(min=6L) float[] values);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scale(float scaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scale(float xScaleFactor, float yScaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scaleX(float scaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scaleY(float scaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform translate(androidx.ink.geometry.Vec offset);
+ field public static final androidx.ink.geometry.ImmutableAffineTransform.Companion Companion;
+ }
+
+ public static final class ImmutableAffineTransform.Companion {
+ method public androidx.ink.geometry.ImmutableAffineTransform scale(float scaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform scale(float xScaleFactor, float yScaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform scaleX(float scaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform scaleY(float scaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform translate(androidx.ink.geometry.Vec offset);
+ }
+
+ public final class ImmutableBox extends androidx.ink.geometry.Box {
+ method public static androidx.ink.geometry.ImmutableBox fromCenterAndDimensions(androidx.ink.geometry.Vec center, @FloatRange(from=0.0) float width, @FloatRange(from=0.0) float height);
+ method public static androidx.ink.geometry.ImmutableBox fromTwoPoints(androidx.ink.geometry.Vec point1, androidx.ink.geometry.Vec point2);
+ method public float getXMax();
+ method public float getXMin();
+ method public float getYMax();
+ method public float getYMin();
+ property public float xMax;
+ property public float xMin;
+ property public float yMax;
+ property public float yMin;
+ field public static final androidx.ink.geometry.ImmutableBox.Companion Companion;
+ }
+
+ public static final class ImmutableBox.Companion {
+ method public androidx.ink.geometry.ImmutableBox fromCenterAndDimensions(androidx.ink.geometry.Vec center, @FloatRange(from=0.0) float width, @FloatRange(from=0.0) float height);
+ method public androidx.ink.geometry.ImmutableBox fromTwoPoints(androidx.ink.geometry.Vec point1, androidx.ink.geometry.Vec point2);
+ }
+
+ public final class ImmutableParallelogram extends androidx.ink.geometry.Parallelogram {
+ method public static androidx.ink.geometry.ImmutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public static androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public static androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ method public androidx.ink.geometry.ImmutableVec getCenter();
+ method public float getHeight();
+ method public float getRotation();
+ method public float getShearFactor();
+ method public float getWidth();
+ property public androidx.ink.geometry.ImmutableVec center;
+ property public float height;
+ property public float rotation;
+ property public float shearFactor;
+ property public float width;
+ field public static final androidx.ink.geometry.ImmutableParallelogram.Companion Companion;
+ }
+
+ public static final class ImmutableParallelogram.Companion {
+ method public androidx.ink.geometry.ImmutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ }
+
+ public final class ImmutableSegment extends androidx.ink.geometry.Segment {
+ ctor public ImmutableSegment(androidx.ink.geometry.Vec start, androidx.ink.geometry.Vec end);
+ method public androidx.ink.geometry.Vec getEnd();
+ method public androidx.ink.geometry.Vec getStart();
+ property public androidx.ink.geometry.Vec end;
+ property public androidx.ink.geometry.Vec start;
+ }
+
+ public final class ImmutableTriangle extends androidx.ink.geometry.Triangle {
+ ctor public ImmutableTriangle(androidx.ink.geometry.Vec p0, androidx.ink.geometry.Vec p1, androidx.ink.geometry.Vec p2);
+ method public androidx.ink.geometry.Vec getP0();
+ method public androidx.ink.geometry.Vec getP1();
+ method public androidx.ink.geometry.Vec getP2();
+ property public androidx.ink.geometry.Vec p0;
+ property public androidx.ink.geometry.Vec p1;
+ property public androidx.ink.geometry.Vec p2;
+ }
+
+ public final class ImmutableVec extends androidx.ink.geometry.Vec {
+ ctor public ImmutableVec(float x, float y);
+ method public static androidx.ink.geometry.ImmutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ method public float getX();
+ method public float getY();
+ property public float x;
+ property public float y;
+ field public static final androidx.ink.geometry.ImmutableVec.Companion Companion;
+ }
+
+ public static final class ImmutableVec.Companion {
+ method public androidx.ink.geometry.ImmutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ }
+
+ public final class Intersection {
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
+ field public static final androidx.ink.geometry.Intersection INSTANCE;
+ }
+
+ public final class MutableAffineTransform extends androidx.ink.geometry.AffineTransform {
+ ctor public MutableAffineTransform();
+ method public void setValues(float m00, float m10, float m20, float m01, float m11, float m21);
+ method public void setValues(@Size(min=6L) float[] values);
+ }
+
+ public final class MutableBox extends androidx.ink.geometry.Box {
+ ctor public MutableBox();
+ method public float getXMax();
+ method public float getXMin();
+ method public float getYMax();
+ method public float getYMin();
+ method public androidx.ink.geometry.MutableBox populateFrom(androidx.ink.geometry.Box input);
+ method public androidx.ink.geometry.MutableBox populateFromCenterAndDimensions(androidx.ink.geometry.Vec center, @FloatRange(from=0.0) float width, @FloatRange(from=0.0) float height);
+ method public androidx.ink.geometry.MutableBox populateFromTwoPoints(androidx.ink.geometry.Vec point1, androidx.ink.geometry.Vec point2);
+ method public androidx.ink.geometry.MutableBox setXBounds(float x1, float x2);
+ method public androidx.ink.geometry.MutableBox setYBounds(float y1, float y2);
+ property public float xMax;
+ property public float xMin;
+ property public float yMax;
+ property public float yMin;
+ }
+
+ public final class MutableParallelogram extends androidx.ink.geometry.Parallelogram {
+ ctor public MutableParallelogram();
+ method public static androidx.ink.geometry.MutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public static androidx.ink.geometry.MutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public static androidx.ink.geometry.MutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ method public androidx.ink.geometry.MutableVec getCenter();
+ method public float getHeight();
+ method @androidx.ink.geometry.AngleRadiansFloat public float getRotation();
+ method public float getShearFactor();
+ method @FloatRange(from=0.0) public float getWidth();
+ method public void setCenter(androidx.ink.geometry.MutableVec);
+ method public void setHeight(float);
+ method public void setRotation(@androidx.ink.geometry.AngleRadiansFloat float);
+ method public void setShearFactor(float);
+ method public void setWidth(@FloatRange(from=0.0) float);
+ property public androidx.ink.geometry.MutableVec center;
+ property public float height;
+ property @androidx.ink.geometry.AngleRadiansFloat public float rotation;
+ property public float shearFactor;
+ property @FloatRange(from=0.0) public float width;
+ field public static final androidx.ink.geometry.MutableParallelogram.Companion Companion;
+ }
+
+ public static final class MutableParallelogram.Companion {
+ method public androidx.ink.geometry.MutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public androidx.ink.geometry.MutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public androidx.ink.geometry.MutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ }
+
+ public final class MutableSegment extends androidx.ink.geometry.Segment {
+ ctor public MutableSegment();
+ ctor public MutableSegment(androidx.ink.geometry.MutableVec start, androidx.ink.geometry.MutableVec end);
+ method public androidx.ink.geometry.MutableVec getEnd();
+ method public androidx.ink.geometry.MutableVec getStart();
+ method public androidx.ink.geometry.MutableSegment populateFrom(androidx.ink.geometry.Segment input);
+ method public void setEnd(androidx.ink.geometry.MutableVec);
+ method public void setStart(androidx.ink.geometry.MutableVec);
+ property public androidx.ink.geometry.MutableVec end;
+ property public androidx.ink.geometry.MutableVec start;
+ }
+
+ public final class MutableTriangle extends androidx.ink.geometry.Triangle {
+ ctor public MutableTriangle();
+ ctor public MutableTriangle(androidx.ink.geometry.MutableVec p0, androidx.ink.geometry.MutableVec p1, androidx.ink.geometry.MutableVec p2);
+ method public androidx.ink.geometry.MutableVec getP0();
+ method public androidx.ink.geometry.MutableVec getP1();
+ method public androidx.ink.geometry.MutableVec getP2();
+ method public androidx.ink.geometry.MutableTriangle populateFrom(androidx.ink.geometry.Triangle input);
+ method public void setP0(androidx.ink.geometry.MutableVec);
+ method public void setP1(androidx.ink.geometry.MutableVec);
+ method public void setP2(androidx.ink.geometry.MutableVec);
+ property public androidx.ink.geometry.MutableVec p0;
+ property public androidx.ink.geometry.MutableVec p1;
+ property public androidx.ink.geometry.MutableVec p2;
+ }
+
+ public final class MutableVec extends androidx.ink.geometry.Vec {
+ ctor public MutableVec();
+ ctor public MutableVec(float x, float y);
+ method public static androidx.ink.geometry.MutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ method public float getX();
+ method public float getY();
+ method public androidx.ink.geometry.MutableVec populateFrom(androidx.ink.geometry.Vec input);
+ method public void setX(float);
+ method public void setY(float);
+ property public float x;
+ property public float y;
+ field public static final androidx.ink.geometry.MutableVec.Companion Companion;
+ }
+
+ public static final class MutableVec.Companion {
+ method public androidx.ink.geometry.MutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ }
+
+ public abstract class Parallelogram {
+ method public final float computeSignedArea();
+ method public abstract androidx.ink.geometry.Vec getCenter();
+ method public abstract float getHeight();
+ method @androidx.ink.geometry.AngleRadiansFloat public abstract float getRotation();
+ method public abstract float getShearFactor();
+ method @FloatRange(from=0.0) public abstract float getWidth();
+ property public abstract androidx.ink.geometry.Vec center;
+ property public abstract float height;
+ property @androidx.ink.geometry.AngleRadiansFloat public abstract float rotation;
+ property public abstract float shearFactor;
+ property @FloatRange(from=0.0) public abstract float width;
+ field public static final androidx.ink.geometry.Parallelogram.Companion Companion;
+ }
+
+ public static final class Parallelogram.Companion {
+ }
+
+ public abstract class Segment {
+ method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
+ method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
+ method public final androidx.ink.geometry.ImmutableVec computeDisplacement();
+ method public final androidx.ink.geometry.MutableVec computeDisplacement(androidx.ink.geometry.MutableVec outVec);
+ method @FloatRange(from=0.0) public final float computeLength();
+ method public final androidx.ink.geometry.ImmutableVec computeLerpPoint(float ratio);
+ method public final androidx.ink.geometry.MutableVec computeLerpPoint(float ratio, androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.ImmutableVec computeMidpoint();
+ method public final androidx.ink.geometry.MutableVec computeMidpoint(androidx.ink.geometry.MutableVec outVec);
+ method public abstract androidx.ink.geometry.Vec getEnd();
+ method public abstract androidx.ink.geometry.Vec getStart();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Segment other, @FloatRange(from=0.0) float tolerance);
+ method public final float project(androidx.ink.geometry.Vec pointToProject);
+ property public abstract androidx.ink.geometry.Vec end;
+ property public abstract androidx.ink.geometry.Vec start;
+ field public static final androidx.ink.geometry.Segment.Companion Companion;
+ }
+
+ public static final class Segment.Companion {
+ }
+
+ public abstract class Triangle {
+ method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
+ method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
+ method public final androidx.ink.geometry.ImmutableSegment computeEdge(@IntRange(from=0L, to=2L) int index);
+ method public final androidx.ink.geometry.MutableSegment computeEdge(@IntRange(from=0L, to=2L) int index, androidx.ink.geometry.MutableSegment outSegment);
+ method public final float computeSignedArea();
+ method public final operator boolean contains(androidx.ink.geometry.Vec point);
+ method public abstract androidx.ink.geometry.Vec getP0();
+ method public abstract androidx.ink.geometry.Vec getP1();
+ method public abstract androidx.ink.geometry.Vec getP2();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Triangle other, @FloatRange(from=0.0) float tolerance);
+ property public abstract androidx.ink.geometry.Vec p0;
+ property public abstract androidx.ink.geometry.Vec p1;
+ property public abstract androidx.ink.geometry.Vec p2;
+ field public static final androidx.ink.geometry.Triangle.Companion Companion;
+ }
+
+ public static final class Triangle.Companion {
+ }
+
+ public abstract class Vec {
+ method @FloatRange(from=0.0, to=java.lang.Math.PI) @androidx.ink.geometry.AngleRadiansFloat public static final float absoluteAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public static final void add(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method @FloatRange(from=-3.141592653589793, to=java.lang.Math.PI) @androidx.ink.geometry.AngleRadiansFloat public final float computeDirection();
+ method @FloatRange(from=0.0) public final float computeMagnitude();
+ method @FloatRange(from=0.0) public final float computeMagnitudeSquared();
+ method public final androidx.ink.geometry.ImmutableVec computeNegation();
+ method public final androidx.ink.geometry.MutableVec computeNegation(androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.ImmutableVec computeOrthogonal();
+ method public final androidx.ink.geometry.MutableVec computeOrthogonal(androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.ImmutableVec computeUnitVec();
+ method public final androidx.ink.geometry.MutableVec computeUnitVec(androidx.ink.geometry.MutableVec outVec);
+ method public static final float determinant(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public static final void divide(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public static final float dotProduct(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public abstract float getX();
+ method public abstract float getY();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Vec other);
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Vec other, optional @FloatRange(from=0.0) float tolerance);
+ method public final boolean isParallelTo(androidx.ink.geometry.Vec other, @FloatRange(from=0.0) @androidx.ink.geometry.AngleRadiansFloat float angleTolerance);
+ method public final boolean isPerpendicularTo(androidx.ink.geometry.Vec other, @FloatRange(from=0.0) @androidx.ink.geometry.AngleRadiansFloat float angleTolerance);
+ method public static final void multiply(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public static final void multiply(float lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method @FloatRange(from=-3.141592653589793, to=java.lang.Math.PI, fromInclusive=false) @androidx.ink.geometry.AngleRadiansFloat public static final float signedAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public static final void subtract(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ property public abstract float x;
+ property public abstract float y;
+ field public static final androidx.ink.geometry.Vec.Companion Companion;
+ field public static final androidx.ink.geometry.ImmutableVec ORIGIN;
+ }
+
+ public static final class Vec.Companion {
+ method @FloatRange(from=0.0, to=java.lang.Math.PI) @androidx.ink.geometry.AngleRadiansFloat public float absoluteAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void add(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method public float determinant(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void divide(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public float dotProduct(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void multiply(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public void multiply(float lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method @FloatRange(from=-3.141592653589793, to=java.lang.Math.PI, fromInclusive=false) @androidx.ink.geometry.AngleRadiansFloat public float signedAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void subtract(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ }
+
}
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 3ee8cd9..7d352f46 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -1,6 +1,22 @@
// Signature format: 4.0
package androidx.ink.geometry {
+ public abstract class AffineTransform {
+ method public final androidx.ink.geometry.MutableParallelogram applyTransform(androidx.ink.geometry.Box box, androidx.ink.geometry.MutableParallelogram outParallelogram);
+ method public final androidx.ink.geometry.MutableParallelogram applyTransform(androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.MutableParallelogram outParallelogram);
+ method public final androidx.ink.geometry.MutableSegment applyTransform(androidx.ink.geometry.Segment segment, androidx.ink.geometry.MutableSegment outSegment);
+ method public final androidx.ink.geometry.MutableTriangle applyTransform(androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.MutableTriangle outTriangle);
+ method public final androidx.ink.geometry.MutableVec applyTransform(androidx.ink.geometry.Vec vec, androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.MutableAffineTransform computeInverse(androidx.ink.geometry.MutableAffineTransform outAffineTransform);
+ method @Size(min=6L) public final float[] getValues();
+ method @Size(min=6L) public final float[] getValues(optional @Size(min=6L) float[] outArray);
+ field public static final androidx.ink.geometry.AffineTransform.Companion Companion;
+ field public static final androidx.ink.geometry.ImmutableAffineTransform IDENTITY;
+ }
+
+ public static final class AffineTransform.Companion {
+ }
+
public final class Angle {
method @androidx.ink.geometry.AngleRadiansFloat public static float degreesToRadians(@androidx.ink.geometry.AngleDegreesFloat float degrees);
method @FloatRange(from=0.0, to=androidx.ink.geometry.Angle.FULL_TURN_RADIANS_DOUBLE) @androidx.ink.geometry.AngleRadiansFloat public static float normalized(@androidx.ink.geometry.AngleRadiansFloat float radians);
@@ -19,5 +35,368 @@
@kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface AngleRadiansFloat {
}
+ public abstract class Box {
+ method public final androidx.ink.geometry.MutableVec computeCenter(androidx.ink.geometry.MutableVec outVec);
+ method public final void computeCorners(androidx.ink.geometry.MutableVec outVecXMinYMin, androidx.ink.geometry.MutableVec outVecXMaxYMin, androidx.ink.geometry.MutableVec outVecXMaxYMax, androidx.ink.geometry.MutableVec outVecXMinYMax);
+ method public final operator boolean contains(androidx.ink.geometry.Box otherBox);
+ method public final operator boolean contains(androidx.ink.geometry.Vec point);
+ method @FloatRange(from=0.0) public final float getHeight();
+ method @FloatRange(from=0.0) public final float getWidth();
+ method public abstract float getXMax();
+ method public abstract float getXMin();
+ method public abstract float getYMax();
+ method public abstract float getYMin();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Box other, @FloatRange(from=0.0) float tolerance);
+ property @FloatRange(from=0.0) public final float height;
+ property @FloatRange(from=0.0) public final float width;
+ property public abstract float xMax;
+ property public abstract float xMin;
+ property public abstract float yMax;
+ property public abstract float yMin;
+ field public static final androidx.ink.geometry.Box.Companion Companion;
+ }
+
+ public static final class Box.Companion {
+ }
+
+ public final class BoxAccumulator {
+ ctor public BoxAccumulator();
+ ctor public BoxAccumulator(androidx.ink.geometry.Box box);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
+ method public androidx.ink.geometry.Box? getBox();
+ method public boolean isAlmostEqual(androidx.ink.geometry.BoxAccumulator other, @FloatRange(from=0.0) float tolerance);
+ method public boolean isEmpty();
+ method public androidx.ink.geometry.BoxAccumulator populateFrom(androidx.ink.geometry.BoxAccumulator input);
+ method public androidx.ink.geometry.BoxAccumulator reset();
+ property public final androidx.ink.geometry.Box? box;
+ }
+
+ public final class ImmutableAffineTransform extends androidx.ink.geometry.AffineTransform {
+ ctor public ImmutableAffineTransform(float m00, float m10, float m20, float m01, float m11, float m21);
+ ctor public ImmutableAffineTransform(@Size(min=6L) float[] values);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scale(float scaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scale(float xScaleFactor, float yScaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scaleX(float scaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform scaleY(float scaleFactor);
+ method public static androidx.ink.geometry.ImmutableAffineTransform translate(androidx.ink.geometry.Vec offset);
+ field public static final androidx.ink.geometry.ImmutableAffineTransform.Companion Companion;
+ }
+
+ public static final class ImmutableAffineTransform.Companion {
+ method public androidx.ink.geometry.ImmutableAffineTransform scale(float scaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform scale(float xScaleFactor, float yScaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform scaleX(float scaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform scaleY(float scaleFactor);
+ method public androidx.ink.geometry.ImmutableAffineTransform translate(androidx.ink.geometry.Vec offset);
+ }
+
+ public final class ImmutableBox extends androidx.ink.geometry.Box {
+ method public static androidx.ink.geometry.ImmutableBox fromCenterAndDimensions(androidx.ink.geometry.Vec center, @FloatRange(from=0.0) float width, @FloatRange(from=0.0) float height);
+ method public static androidx.ink.geometry.ImmutableBox fromTwoPoints(androidx.ink.geometry.Vec point1, androidx.ink.geometry.Vec point2);
+ method public float getXMax();
+ method public float getXMin();
+ method public float getYMax();
+ method public float getYMin();
+ property public float xMax;
+ property public float xMin;
+ property public float yMax;
+ property public float yMin;
+ field public static final androidx.ink.geometry.ImmutableBox.Companion Companion;
+ }
+
+ public static final class ImmutableBox.Companion {
+ method public androidx.ink.geometry.ImmutableBox fromCenterAndDimensions(androidx.ink.geometry.Vec center, @FloatRange(from=0.0) float width, @FloatRange(from=0.0) float height);
+ method public androidx.ink.geometry.ImmutableBox fromTwoPoints(androidx.ink.geometry.Vec point1, androidx.ink.geometry.Vec point2);
+ }
+
+ public final class ImmutableParallelogram extends androidx.ink.geometry.Parallelogram {
+ method public static androidx.ink.geometry.ImmutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public static androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public static androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ method public androidx.ink.geometry.ImmutableVec getCenter();
+ method public float getHeight();
+ method public float getRotation();
+ method public float getShearFactor();
+ method public float getWidth();
+ property public androidx.ink.geometry.ImmutableVec center;
+ property public float height;
+ property public float rotation;
+ property public float shearFactor;
+ property public float width;
+ field public static final androidx.ink.geometry.ImmutableParallelogram.Companion Companion;
+ }
+
+ public static final class ImmutableParallelogram.Companion {
+ method public androidx.ink.geometry.ImmutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public androidx.ink.geometry.ImmutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.ImmutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ }
+
+ public final class ImmutableSegment extends androidx.ink.geometry.Segment {
+ ctor public ImmutableSegment(androidx.ink.geometry.Vec start, androidx.ink.geometry.Vec end);
+ method public androidx.ink.geometry.Vec getEnd();
+ method public androidx.ink.geometry.Vec getStart();
+ property public androidx.ink.geometry.Vec end;
+ property public androidx.ink.geometry.Vec start;
+ }
+
+ public final class ImmutableTriangle extends androidx.ink.geometry.Triangle {
+ ctor public ImmutableTriangle(androidx.ink.geometry.Vec p0, androidx.ink.geometry.Vec p1, androidx.ink.geometry.Vec p2);
+ method public androidx.ink.geometry.Vec getP0();
+ method public androidx.ink.geometry.Vec getP1();
+ method public androidx.ink.geometry.Vec getP2();
+ property public androidx.ink.geometry.Vec p0;
+ property public androidx.ink.geometry.Vec p1;
+ property public androidx.ink.geometry.Vec p2;
+ }
+
+ public final class ImmutableVec extends androidx.ink.geometry.Vec {
+ ctor public ImmutableVec(float x, float y);
+ method public static androidx.ink.geometry.ImmutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ method public float getX();
+ method public float getY();
+ property public float x;
+ property public float y;
+ field public static final androidx.ink.geometry.ImmutableVec.Companion Companion;
+ }
+
+ public static final class ImmutableVec.Companion {
+ method public androidx.ink.geometry.ImmutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ }
+
+ public final class Intersection {
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
+ field public static final androidx.ink.geometry.Intersection INSTANCE;
+ }
+
+ public final class MutableAffineTransform extends androidx.ink.geometry.AffineTransform {
+ ctor public MutableAffineTransform();
+ method public void setValues(float m00, float m10, float m20, float m01, float m11, float m21);
+ method public void setValues(@Size(min=6L) float[] values);
+ }
+
+ public final class MutableBox extends androidx.ink.geometry.Box {
+ ctor public MutableBox();
+ method public float getXMax();
+ method public float getXMin();
+ method public float getYMax();
+ method public float getYMin();
+ method public androidx.ink.geometry.MutableBox populateFrom(androidx.ink.geometry.Box input);
+ method public androidx.ink.geometry.MutableBox populateFromCenterAndDimensions(androidx.ink.geometry.Vec center, @FloatRange(from=0.0) float width, @FloatRange(from=0.0) float height);
+ method public androidx.ink.geometry.MutableBox populateFromTwoPoints(androidx.ink.geometry.Vec point1, androidx.ink.geometry.Vec point2);
+ method public androidx.ink.geometry.MutableBox setXBounds(float x1, float x2);
+ method public androidx.ink.geometry.MutableBox setYBounds(float y1, float y2);
+ property public float xMax;
+ property public float xMin;
+ property public float yMax;
+ property public float yMin;
+ }
+
+ public final class MutableParallelogram extends androidx.ink.geometry.Parallelogram {
+ ctor public MutableParallelogram();
+ method public static androidx.ink.geometry.MutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public static androidx.ink.geometry.MutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public static androidx.ink.geometry.MutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ method public androidx.ink.geometry.MutableVec getCenter();
+ method public float getHeight();
+ method @androidx.ink.geometry.AngleRadiansFloat public float getRotation();
+ method public float getShearFactor();
+ method @FloatRange(from=0.0) public float getWidth();
+ method public void setCenter(androidx.ink.geometry.MutableVec);
+ method public void setHeight(float);
+ method public void setRotation(@androidx.ink.geometry.AngleRadiansFloat float);
+ method public void setShearFactor(float);
+ method public void setWidth(@FloatRange(from=0.0) float);
+ property public androidx.ink.geometry.MutableVec center;
+ property public float height;
+ property @androidx.ink.geometry.AngleRadiansFloat public float rotation;
+ property public float shearFactor;
+ property @FloatRange(from=0.0) public float width;
+ field public static final androidx.ink.geometry.MutableParallelogram.Companion Companion;
+ }
+
+ public static final class MutableParallelogram.Companion {
+ method public androidx.ink.geometry.MutableParallelogram fromCenterAndDimensions(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height);
+ method public androidx.ink.geometry.MutableParallelogram fromCenterDimensionsAndRotation(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation);
+ method public androidx.ink.geometry.MutableParallelogram fromCenterDimensionsRotationAndShear(androidx.ink.geometry.MutableVec center, @FloatRange(from=0.0) float width, float height, @androidx.ink.geometry.AngleRadiansFloat float rotation, float shearFactor);
+ }
+
+ public final class MutableSegment extends androidx.ink.geometry.Segment {
+ ctor public MutableSegment();
+ ctor public MutableSegment(androidx.ink.geometry.MutableVec start, androidx.ink.geometry.MutableVec end);
+ method public androidx.ink.geometry.MutableVec getEnd();
+ method public androidx.ink.geometry.MutableVec getStart();
+ method public androidx.ink.geometry.MutableSegment populateFrom(androidx.ink.geometry.Segment input);
+ method public void setEnd(androidx.ink.geometry.MutableVec);
+ method public void setStart(androidx.ink.geometry.MutableVec);
+ property public androidx.ink.geometry.MutableVec end;
+ property public androidx.ink.geometry.MutableVec start;
+ }
+
+ public final class MutableTriangle extends androidx.ink.geometry.Triangle {
+ ctor public MutableTriangle();
+ ctor public MutableTriangle(androidx.ink.geometry.MutableVec p0, androidx.ink.geometry.MutableVec p1, androidx.ink.geometry.MutableVec p2);
+ method public androidx.ink.geometry.MutableVec getP0();
+ method public androidx.ink.geometry.MutableVec getP1();
+ method public androidx.ink.geometry.MutableVec getP2();
+ method public androidx.ink.geometry.MutableTriangle populateFrom(androidx.ink.geometry.Triangle input);
+ method public void setP0(androidx.ink.geometry.MutableVec);
+ method public void setP1(androidx.ink.geometry.MutableVec);
+ method public void setP2(androidx.ink.geometry.MutableVec);
+ property public androidx.ink.geometry.MutableVec p0;
+ property public androidx.ink.geometry.MutableVec p1;
+ property public androidx.ink.geometry.MutableVec p2;
+ }
+
+ public final class MutableVec extends androidx.ink.geometry.Vec {
+ ctor public MutableVec();
+ ctor public MutableVec(float x, float y);
+ method public static androidx.ink.geometry.MutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ method public float getX();
+ method public float getY();
+ method public androidx.ink.geometry.MutableVec populateFrom(androidx.ink.geometry.Vec input);
+ method public void setX(float);
+ method public void setY(float);
+ property public float x;
+ property public float y;
+ field public static final androidx.ink.geometry.MutableVec.Companion Companion;
+ }
+
+ public static final class MutableVec.Companion {
+ method public androidx.ink.geometry.MutableVec fromDirectionAndMagnitude(@androidx.ink.geometry.AngleRadiansFloat float direction, float magnitude);
+ }
+
+ public abstract class Parallelogram {
+ method public final float computeSignedArea();
+ method public abstract androidx.ink.geometry.Vec getCenter();
+ method public abstract float getHeight();
+ method @androidx.ink.geometry.AngleRadiansFloat public abstract float getRotation();
+ method public abstract float getShearFactor();
+ method @FloatRange(from=0.0) public abstract float getWidth();
+ property public abstract androidx.ink.geometry.Vec center;
+ property public abstract float height;
+ property @androidx.ink.geometry.AngleRadiansFloat public abstract float rotation;
+ property public abstract float shearFactor;
+ property @FloatRange(from=0.0) public abstract float width;
+ field public static final androidx.ink.geometry.Parallelogram.Companion Companion;
+ }
+
+ public static final class Parallelogram.Companion {
+ }
+
+ public abstract class Segment {
+ method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
+ method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
+ method public final androidx.ink.geometry.ImmutableVec computeDisplacement();
+ method public final androidx.ink.geometry.MutableVec computeDisplacement(androidx.ink.geometry.MutableVec outVec);
+ method @FloatRange(from=0.0) public final float computeLength();
+ method public final androidx.ink.geometry.ImmutableVec computeLerpPoint(float ratio);
+ method public final androidx.ink.geometry.MutableVec computeLerpPoint(float ratio, androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.ImmutableVec computeMidpoint();
+ method public final androidx.ink.geometry.MutableVec computeMidpoint(androidx.ink.geometry.MutableVec outVec);
+ method public abstract androidx.ink.geometry.Vec getEnd();
+ method public abstract androidx.ink.geometry.Vec getStart();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Segment other, @FloatRange(from=0.0) float tolerance);
+ method public final float project(androidx.ink.geometry.Vec pointToProject);
+ property public abstract androidx.ink.geometry.Vec end;
+ property public abstract androidx.ink.geometry.Vec start;
+ field public static final androidx.ink.geometry.Segment.Companion Companion;
+ }
+
+ public static final class Segment.Companion {
+ }
+
+ public abstract class Triangle {
+ method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
+ method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
+ method public final androidx.ink.geometry.ImmutableSegment computeEdge(@IntRange(from=0L, to=2L) int index);
+ method public final androidx.ink.geometry.MutableSegment computeEdge(@IntRange(from=0L, to=2L) int index, androidx.ink.geometry.MutableSegment outSegment);
+ method public final float computeSignedArea();
+ method public final operator boolean contains(androidx.ink.geometry.Vec point);
+ method public abstract androidx.ink.geometry.Vec getP0();
+ method public abstract androidx.ink.geometry.Vec getP1();
+ method public abstract androidx.ink.geometry.Vec getP2();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Triangle other, @FloatRange(from=0.0) float tolerance);
+ property public abstract androidx.ink.geometry.Vec p0;
+ property public abstract androidx.ink.geometry.Vec p1;
+ property public abstract androidx.ink.geometry.Vec p2;
+ field public static final androidx.ink.geometry.Triangle.Companion Companion;
+ }
+
+ public static final class Triangle.Companion {
+ }
+
+ public abstract class Vec {
+ method @FloatRange(from=0.0, to=java.lang.Math.PI) @androidx.ink.geometry.AngleRadiansFloat public static final float absoluteAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public static final void add(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method @FloatRange(from=-3.141592653589793, to=java.lang.Math.PI) @androidx.ink.geometry.AngleRadiansFloat public final float computeDirection();
+ method @FloatRange(from=0.0) public final float computeMagnitude();
+ method @FloatRange(from=0.0) public final float computeMagnitudeSquared();
+ method public final androidx.ink.geometry.ImmutableVec computeNegation();
+ method public final androidx.ink.geometry.MutableVec computeNegation(androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.ImmutableVec computeOrthogonal();
+ method public final androidx.ink.geometry.MutableVec computeOrthogonal(androidx.ink.geometry.MutableVec outVec);
+ method public final androidx.ink.geometry.ImmutableVec computeUnitVec();
+ method public final androidx.ink.geometry.MutableVec computeUnitVec(androidx.ink.geometry.MutableVec outVec);
+ method public static final float determinant(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public static final void divide(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public static final float dotProduct(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public abstract float getX();
+ method public abstract float getY();
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Vec other);
+ method public final boolean isAlmostEqual(androidx.ink.geometry.Vec other, optional @FloatRange(from=0.0) float tolerance);
+ method public final boolean isParallelTo(androidx.ink.geometry.Vec other, @FloatRange(from=0.0) @androidx.ink.geometry.AngleRadiansFloat float angleTolerance);
+ method public final boolean isPerpendicularTo(androidx.ink.geometry.Vec other, @FloatRange(from=0.0) @androidx.ink.geometry.AngleRadiansFloat float angleTolerance);
+ method public static final void multiply(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public static final void multiply(float lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method @FloatRange(from=-3.141592653589793, to=java.lang.Math.PI, fromInclusive=false) @androidx.ink.geometry.AngleRadiansFloat public static final float signedAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public static final void subtract(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ property public abstract float x;
+ property public abstract float y;
+ field public static final androidx.ink.geometry.Vec.Companion Companion;
+ field public static final androidx.ink.geometry.ImmutableVec ORIGIN;
+ }
+
+ public static final class Vec.Companion {
+ method @FloatRange(from=0.0, to=java.lang.Math.PI) @androidx.ink.geometry.AngleRadiansFloat public float absoluteAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void add(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method public float determinant(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void divide(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public float dotProduct(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void multiply(androidx.ink.geometry.Vec lhs, float rhs, androidx.ink.geometry.MutableVec output);
+ method public void multiply(float lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ method @FloatRange(from=-3.141592653589793, to=java.lang.Math.PI, fromInclusive=false) @androidx.ink.geometry.AngleRadiansFloat public float signedAngleBetween(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs);
+ method public void subtract(androidx.ink.geometry.Vec lhs, androidx.ink.geometry.Vec rhs, androidx.ink.geometry.MutableVec output);
+ }
+
}
diff --git a/ink/ink-geometry/src/androidInstrumentedTest/kotlin/androidx/ink/geometry/EnvelopeExtensionsTest.kt b/ink/ink-geometry/src/androidInstrumentedTest/kotlin/androidx/ink/geometry/EnvelopeExtensionsTest.kt
index 44862ce..f98e981 100644
--- a/ink/ink-geometry/src/androidInstrumentedTest/kotlin/androidx/ink/geometry/EnvelopeExtensionsTest.kt
+++ b/ink/ink-geometry/src/androidInstrumentedTest/kotlin/androidx/ink/geometry/EnvelopeExtensionsTest.kt
@@ -40,7 +40,7 @@
fun getBoundsRectF_whenHasBounds_returnsTrueAndOverwritesOutParameter() {
val envelope =
BoxAccumulator()
- .add(MutableBox().fillFromTwoPoints(ImmutablePoint(1f, 2f), ImmutablePoint(3f, 4f)))
+ .add(MutableBox().populateFromTwoPoints(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f)))
val outRect = RectF(5F, 6F, 7F, 8F)
assertThat(envelope.getBounds(outRect)).isTrue()
diff --git a/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt b/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt
index ae923f3..6efc190 100644
--- a/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt
+++ b/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt
@@ -29,12 +29,12 @@
/** Writes the values from this [AffineTransform] to [matrixOut]. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun AffineTransform.populateMatrix(matrixOut: Matrix) {
- matrixValuesScratchArray[Matrix.MSCALE_X] = a
- matrixValuesScratchArray[Matrix.MSKEW_X] = b
- matrixValuesScratchArray[Matrix.MTRANS_X] = c
- matrixValuesScratchArray[Matrix.MSKEW_Y] = d
- matrixValuesScratchArray[Matrix.MSCALE_Y] = e
- matrixValuesScratchArray[Matrix.MTRANS_Y] = f
+ matrixValuesScratchArray[Matrix.MSCALE_X] = m00
+ matrixValuesScratchArray[Matrix.MSKEW_X] = m10
+ matrixValuesScratchArray[Matrix.MTRANS_X] = m20
+ matrixValuesScratchArray[Matrix.MSKEW_Y] = m01
+ matrixValuesScratchArray[Matrix.MSCALE_Y] = m11
+ matrixValuesScratchArray[Matrix.MTRANS_Y] = m21
matrixValuesScratchArray[Matrix.MPERSP_0] = 0f
matrixValuesScratchArray[Matrix.MPERSP_1] = 0f
matrixValuesScratchArray[Matrix.MPERSP_2] = 1f
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/AffineTransform.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/AffineTransform.kt
index d190a0a..611db89 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/AffineTransform.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/AffineTransform.kt
@@ -17,22 +17,23 @@
package androidx.ink.geometry
import androidx.annotation.RestrictTo
+import androidx.annotation.Size
import kotlin.jvm.JvmField
/**
* An affine transformation in the plane. The transformation can be thought of as a 3x3 matrix:
* ```
- * ⎡a b c⎤
- * ⎢d e f⎥
- * ⎣0 0 1⎦
+ * ⎡m00 m10 m20⎤
+ * ⎢m01 m11 m21⎥
+ * ⎣ 0 0 1 ⎦
* ```
*
* Applying the transformation can be thought of as a matrix multiplication, with the
* to-be-transformed point represented as a column vector with an extra 1:
* ```
- * ⎡a b c⎤ ⎡x⎤ ⎡a*x + b*y + c⎤
- * ⎢d e f⎥ * ⎢y⎥ = ⎢d*x + e*y + f⎥
- * ⎣0 0 1⎦ ⎣1⎦ ⎣ 1 ⎦
+ * ⎡m00 m10 m20⎤ ⎡x⎤ ⎡m00*x + m10*y + m20⎤
+ * ⎢m01 m11 m21⎥ * ⎢y⎥ = ⎢m01*x + m11*y + m21⎥
+ * ⎣ 0 0 1 ⎦ ⎣1⎦ ⎣ 1 ⎦
* ```
*
* Transformations are composed via multiplication. Multiplication is not commutative (i.e. A*B !=
@@ -43,109 +44,90 @@
* val translate = ImmutableAffineTransform.translate(Vec(10, 0))
* ```
*
- * then the `rotate * translate` first translates 10 units in the positive x-direction, then rotates
- * 90° about the origin.
+ * then `rotate * translate` first translates 10 units in the positive x-direction, then rotates 45°
+ * about the origin.
*
- * This class follows AndroidX guidelines ({@link http://go/androidx-api-guidelines#kotlin-data}) to
- * avoid Kotlin data classes.
- *
- * See [MutableAffineTransform] and [ImmutableAffineTransform] for implementations.
+ * [ImmutableAffineTransform] and [MutableAffineTransform] are the two concrete implementations of
+ * this.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface AffineTransform {
- public val a: Float
- public val b: Float
- public val c: Float
- public val d: Float
- public val e: Float
- public val f: Float
+public abstract class AffineTransform internal constructor() {
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ public abstract val m00: Float
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ public abstract val m10: Float
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ public abstract val m20: Float
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ public abstract val m01: Float
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ public abstract val m11: Float
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ public abstract val m21: Float
/**
* Returns an immutable copy of this object. This will return itself if called on an immutable
* instance.
*/
- public fun asImmutable(): ImmutableAffineTransform {
- return ImmutableAffineTransform(
- a = this.a,
- b = this.b,
- c = this.c,
- d = this.d,
- e = this.e,
- f = this.f,
- )
- }
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract fun asImmutable(): ImmutableAffineTransform
/**
- * Populates [output] with the inverse of the [AffineTransform]. The same MutableAffineTransform
- * can be used as the output to avoid additional allocations.
+ * Populates [outAffineTransform] with the inverse of this [AffineTransform]. The same
+ * [MutableAffineTransform] instance can be used as the output to avoid additional allocations.
+ * Returns [outAffineTransform].
*/
- public fun populateInverse(output: MutableAffineTransform) {
- val determinant = a * e - b * d
+ public fun computeInverse(outAffineTransform: MutableAffineTransform): MutableAffineTransform {
+ val determinant = m00 * m11 - m10 * m01
require(determinant != 0F) {
"The inverse of the AffineTransform cannot be found because the determinant is 0."
}
- val newA = e / determinant
- val newB = -b / determinant
- val newC = (b * f - c * e) / determinant
- val newD = -d / determinant
- val newE = a / determinant
- val newF = (c * d - a * f) / determinant
- output.a = newA
- output.b = newB
- output.c = newC
- output.d = newD
- output.e = newE
- output.f = newF
+ val newM00 = m11 / determinant
+ val newM10 = -m10 / determinant
+ val newM20 = (m10 * m21 - m20 * m11) / determinant
+ val newM01 = -m01 / determinant
+ val newM11 = m00 / determinant
+ val newM21 = (m20 * m01 - m00 * m21) / determinant
+ outAffineTransform.setValues(newM00, newM10, newM20, newM01, newM11, newM21)
+ return outAffineTransform
}
- private fun transformX(x: Float, y: Float): Float = a * x + b * y + c
+ private fun applyTransformX(x: Float, y: Float): Float = m00 * x + m10 * y + m20
- private fun transformY(x: Float, y: Float): Float = d * x + e * y + f
+ private fun applyTransformY(x: Float, y: Float): Float = m01 * x + m11 * y + m21
/**
* Apply the [AffineTransform] to the [Vec] and store the result in the [MutableVec]. The same
* [MutableVec] can be used as both the input and output to avoid additional allocations.
+ * Returns [outVec].
*/
- public fun applyTransform(vec: Vec, output: MutableVec) {
- val newX = transformX(vec.x, vec.y)
- output.y = transformY(vec.x, vec.y)
- output.x = newX
+ public fun applyTransform(vec: Vec, outVec: MutableVec): MutableVec {
+ val newX = applyTransformX(vec.x, vec.y)
+ outVec.y = applyTransformY(vec.x, vec.y)
+ outVec.x = newX
+ return outVec
}
/**
* Apply the [AffineTransform] to the [Segment] and store the result in the [MutableSegment].
* The same [MutableSegment] can be used as both the input and output to avoid additional
- * allocations.
+ * allocations. Returns [outSegment].
*/
- public fun applyTransform(segment: Segment, output: MutableSegment) {
- output.start(
- transformX(segment.start.x, segment.start.y),
- transformY(segment.start.x, segment.start.y),
- )
- output.end(
- transformX(segment.end.x, segment.end.y),
- transformY(segment.end.x, segment.end.y)
- )
+ public fun applyTransform(segment: Segment, outSegment: MutableSegment): MutableSegment {
+ applyTransform(segment.start, outSegment.start)
+ applyTransform(segment.end, outSegment.end)
+ return outSegment
}
/**
* Apply the [AffineTransform] to the [Triangle] and store the result in the [MutableTriangle].
* The same [MutableTriangle] can be used as both the input and output to avoid additional
- * allocations.
+ * allocations. Returns [outTriangle].
*/
- public fun applyTransform(triangle: Triangle, output: MutableTriangle) {
- output.p0(
- transformX(triangle.p0.x, triangle.p0.y),
- transformY(triangle.p0.x, triangle.p0.y)
- )
- output.p1(
- transformX(triangle.p1.x, triangle.p1.y),
- transformY(triangle.p1.x, triangle.p1.y)
- )
- output.p2(
- transformX(triangle.p2.x, triangle.p2.y),
- transformY(triangle.p2.x, triangle.p2.y)
- )
+ public fun applyTransform(triangle: Triangle, outTriangle: MutableTriangle): MutableTriangle {
+ applyTransform(triangle.p0, outTriangle.p0)
+ applyTransform(triangle.p1, outTriangle.p1)
+ applyTransform(triangle.p2, outTriangle.p2)
+ return outTriangle
}
/**
@@ -153,22 +135,26 @@
* This is the only Apply function where the input cannot also be the output, as applying an
* Affine Transform to a Box makes a Parallelogram.
*/
- public fun applyTransform(box: Box, outputParallelogram: MutableParallelogram) {
+ public fun applyTransform(
+ box: Box,
+ outParallelogram: MutableParallelogram,
+ ): MutableParallelogram {
AffineTransformHelper.nativeApplyParallelogram(
- affineTransformA = a,
- affineTransformB = b,
- affineTransformC = c,
- affineTransformD = d,
- affineTransformE = e,
- affineTransformF = f,
+ affineTransformA = m00,
+ affineTransformB = m10,
+ affineTransformC = m20,
+ affineTransformD = m01,
+ affineTransformE = m11,
+ affineTransformF = m21,
parallelogramCenterX = (box.xMin + box.xMax) / 2,
parallelogramCenterY = (box.yMin + box.yMax) / 2,
parallelogramWidth = box.width,
parallelogramHeight = box.height,
parallelogramRotation = 0f,
parallelogramShearFactor = 0f,
- out = outputParallelogram,
+ out = outParallelogram,
)
+ return outParallelogram
}
/**
@@ -178,23 +164,52 @@
*/
public fun applyTransform(
parallelogram: Parallelogram,
- outputParallelogram: MutableParallelogram,
- ) {
+ outParallelogram: MutableParallelogram,
+ ): MutableParallelogram {
AffineTransformHelper.nativeApplyParallelogram(
- affineTransformA = a,
- affineTransformB = b,
- affineTransformC = c,
- affineTransformD = d,
- affineTransformE = e,
- affineTransformF = f,
+ affineTransformA = m00,
+ affineTransformB = m10,
+ affineTransformC = m20,
+ affineTransformD = m01,
+ affineTransformE = m11,
+ affineTransformF = m21,
parallelogramCenterX = parallelogram.center.x,
parallelogramCenterY = parallelogram.center.y,
parallelogramWidth = parallelogram.width,
parallelogramHeight = parallelogram.height,
parallelogramRotation = parallelogram.rotation,
parallelogramShearFactor = parallelogram.shearFactor,
- out = outputParallelogram,
+ out = outParallelogram,
)
+ return outParallelogram
+ }
+
+ /**
+ * Populates the first 6 elements of [outArray] with the values of this transform, starting with
+ * the top left corner of the matrix and proceeding in row-major order.
+ *
+ * In performance-sensitive code, prefer to pass in an array that has already been allocated and
+ * is being reused, rather than relying on the default behavior of allocating a new instance for
+ * each call.
+ *
+ * Prefer to apply this transform to an object, such as with [applyTransform], rather than
+ * accessing the actual numeric values of this transform. This function is useful for when the
+ * values are needed in bulk but not to apply a transform, for example for serialization.
+ *
+ * To set these values on a transform in the same order that they are retrieved here, use the
+ * [ImmutableAffineTransform] constructor or use [MutableAffineTransform.setValues].
+ */
+ @JvmOverloads
+ @Size(min = 6)
+ @Suppress("ArrayReturn") // Returning the input value for chaining.
+ public fun getValues(@Size(min = 6) outArray: FloatArray = FloatArray(6)): FloatArray {
+ outArray[0] = m00
+ outArray[1] = m10
+ outArray[2] = m20
+ outArray[3] = m01
+ outArray[4] = m11
+ outArray[5] = m21
+ return outArray
}
public companion object {
@@ -204,29 +219,29 @@
*/
@JvmField
public val IDENTITY: ImmutableAffineTransform =
- ImmutableAffineTransform(a = 1f, b = 0f, c = 0f, d = 0f, e = 1f, f = 0f)
+ ImmutableAffineTransform(1f, 0f, 0f, 0f, 1f, 0f)
/**
* Returns true if [first] and [second] have the same values for all properties of
* [AffineTransform].
*/
internal fun areEquivalent(first: AffineTransform, second: AffineTransform): Boolean =
- first.a == second.a &&
- first.b == second.b &&
- first.c == second.c &&
- first.d == second.d &&
- first.e == second.e &&
- first.f == second.f
+ first.m00 == second.m00 &&
+ first.m10 == second.m10 &&
+ first.m20 == second.m20 &&
+ first.m01 == second.m01 &&
+ first.m11 == second.m11 &&
+ first.m21 == second.m21
/** Returns a hash code for [affineTransform] using its [AffineTransform] properties. */
internal fun hash(affineTransform: AffineTransform): Int =
affineTransform.run {
- var result = a.hashCode()
- result = 31 * result + b.hashCode()
- result = 31 * result + c.hashCode()
- result = 31 * result + d.hashCode()
- result = 31 * result + e.hashCode()
- result = 31 * result + f.hashCode()
+ var result = m00.hashCode()
+ result = 31 * result + m10.hashCode()
+ result = 31 * result + m20.hashCode()
+ result = 31 * result + m01.hashCode()
+ result = 31 * result + m11.hashCode()
+ result = 31 * result + m21.hashCode()
return result
}
@@ -235,6 +250,8 @@
* properties.
*/
internal fun string(affineTransform: AffineTransform): String =
- affineTransform.run { "AffineTransform(a=$a, b=$b, c=$c, d=$d, e=$e, f=$f)" }
+ affineTransform.run {
+ "AffineTransform(m00=$m00, m10=$m10, m20=$m20, m01=$m01, m11=$m11, m21=$m21)"
+ }
}
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Box.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Box.kt
index 314a735..26e596b 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Box.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Box.kt
@@ -17,7 +17,6 @@
package androidx.ink.geometry
import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
import kotlin.math.abs
/**
@@ -26,19 +25,18 @@
*
* The [Box] interface is the read-only view of the underlying data which may or may not be mutable.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface Box {
+public abstract class Box internal constructor() {
/** The lower bound in the `X` direction. */
- public val xMin: Float
+ public abstract val xMin: Float
/** The lower bound in the `Y` direction. */
- public val yMin: Float
+ public abstract val yMin: Float
/** The upper bound in the `X` direction. */
- public val xMax: Float
+ public abstract val xMax: Float
/** The upper bound in the `Y` direction. */
- public val yMax: Float
+ public abstract val yMax: Float
/** The width of the rectangle. This can never be negative. */
public val width: Float
@@ -48,34 +46,37 @@
public val height: Float
@FloatRange(from = 0.0) get() = yMax - yMin
- /** Populates [out] with the center of the [Box]. */
- public fun center(out: MutablePoint)
+ /** Populates [outVec] with the center of the [Box], and returns [outVec]. */
+ public fun computeCenter(outVec: MutableVec): MutableVec {
+ BoxHelper.nativeCenter(xMin, yMin, xMax, yMax, outVec)
+ return outVec
+ }
/**
- * Populates the 4 [output] points with the corners of the [Box]. The order of the corners is:
+ * Populates the 4 output points with the corners of the [Box]. The order of the corners is:
* (x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)
*/
- public fun corners(
- outputXMinYMin: MutablePoint,
- outputXMaxYMin: MutablePoint,
- outputXMaxYMax: MutablePoint,
- outputXMinYMax: MutablePoint,
+ public fun computeCorners(
+ outVecXMinYMin: MutableVec,
+ outVecXMaxYMin: MutableVec,
+ outVecXMaxYMax: MutableVec,
+ outVecXMinYMax: MutableVec,
) {
- outputXMinYMin.x = xMin
- outputXMinYMin.y = yMin
- outputXMaxYMin.x = xMax
- outputXMaxYMin.y = yMin
- outputXMaxYMax.x = xMax
- outputXMaxYMax.y = yMax
- outputXMinYMax.x = xMin
- outputXMinYMax.y = yMax
+ outVecXMinYMin.x = xMin
+ outVecXMinYMin.y = yMin
+ outVecXMaxYMin.x = xMax
+ outVecXMaxYMin.y = yMin
+ outVecXMaxYMax.x = xMax
+ outVecXMaxYMax.y = yMax
+ outVecXMinYMax.x = xMin
+ outVecXMinYMax.y = yMax
}
/**
* Returns whether the given point is contained within the Box. Points that lie exactly on the
* Box's boundary are considered to be contained.
*/
- public operator fun contains(point: Point): Boolean =
+ public operator fun contains(point: Vec): Boolean =
BoxHelper.nativeContainsPoint(xMin, yMin, xMax, yMax, point.x, point.y)
/**
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index acf7fb6..f14b398 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -19,8 +19,6 @@
import androidx.annotation.FloatRange
import androidx.annotation.RestrictTo
import androidx.ink.nativeloader.NativeLoader
-import kotlin.Deprecated
-import kotlin.jvm.JvmSynthetic
/**
* A helper class for accumulating the minimum bounding boxes of zero or more geometry objects. In
@@ -28,7 +26,6 @@
*/
// TODO: b/355248266 - @UsedByNative("envelope_jni_helper.cc") must go in Proguard config file
// instead.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class BoxAccumulator {
/**
* The bounds, which are valid only if [hasBounds] is `true`. When [hasBounds] is `false`, this
@@ -59,9 +56,9 @@
) : this(
true,
MutableBox()
- .fillFromTwoPoints(
- ImmutablePoint(box.xMin, box.yMin),
- ImmutablePoint(box.xMax, box.yMax)
+ .populateFromTwoPoints(
+ ImmutableVec(box.xMin, box.yMin),
+ ImmutableVec(box.xMax, box.yMax)
),
)
@@ -75,16 +72,23 @@
*/
public fun isEmpty(): Boolean = !hasBounds
- /** Populates this [BoxAccumulator] with the same values contained in [input]. */
+ /**
+ * Populates this [BoxAccumulator] with the same values contained in [input].
+ *
+ * @return `this`
+ */
public fun populateFrom(input: BoxAccumulator): BoxAccumulator {
reset().add(input)
return this
}
- /** Reset this object to have no bounds. Returns the same instance to chain function calls. */
+ /**
+ * Reset this object to have no bounds.
+ *
+ * @return `this`
+ */
// TODO: b/355248266 - @UsedByNative("envelope_jni_helper.cc") must go in Proguard config file
// instead.
-
public fun reset(): BoxAccumulator {
hasBounds = false
_bounds.setXBounds(Float.NaN, Float.NaN).setYBounds(Float.NaN, Float.NaN)
@@ -94,6 +98,8 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains [other]. If
* [other] is null, this is a no-op.
+ *
+ * @return `this`
*/
public fun add(other: BoxAccumulator?): BoxAccumulator {
BoxAccumulatorNative.nativeAddOptionalBox(
@@ -115,6 +121,8 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains [point]. If
* [point] is null, this is a no-op.
+ *
+ * @return `this`
*/
public fun add(point: Vec): BoxAccumulator {
BoxAccumulatorNative.nativeAddPoint(
@@ -133,6 +141,8 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains [segment]. If
* [segment] is null, this is a no-op.
+ *
+ * @return `this`
*/
public fun add(segment: Segment): BoxAccumulator {
BoxAccumulatorNative.nativeAddSegment(
@@ -153,6 +163,8 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains [triangle]. If
* [triangle] is null, this is a no-op.
+ *
+ * @return `this`
*/
public fun add(triangle: Triangle): BoxAccumulator {
BoxAccumulatorNative.nativeAddTriangle(
@@ -175,6 +187,8 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains [box]. If
* [box] is null, this is a no-op.
+ *
+ * @return `this`
*/
public fun add(box: Box?): BoxAccumulator {
BoxAccumulatorNative.nativeAddOptionalBox(
@@ -196,6 +210,8 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains
* [parallelogram]. If [parallelogram] is null, this is a no-op.
+ *
+ * @return `this`
*/
public fun add(parallelogram: Parallelogram): BoxAccumulator {
BoxAccumulatorNative.nativeAddParallelogram(
@@ -218,8 +234,11 @@
/**
* Expands the accumulated bounding box (if necessary) such that it also contains [mesh]. If
* [mesh] is null or empty, this is a no-op.
+ *
+ * @return `this`
*/
- public fun add(mesh: ModeledShape): BoxAccumulator = this.add(mesh.bounds)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
/**
* Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
@@ -235,13 +254,13 @@
/**
* Overwrite the entries of this object with new values. This is useful for recycling an
- * instance. Returns the same instance to chain function calls.
+ * instance.
+ *
+ * @return `this`
*/
// TODO: b/355248266 - @UsedByNative("envelope_jni_helper.cc") must go in Proguard config file
// instead.
-
- @JvmSynthetic
- @Deprecated("Prefer to use methods [reset] and [add]")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun overwriteFrom(x1: Float, y1: Float, x2: Float, y2: Float): BoxAccumulator {
hasBounds = true
_bounds.setXBounds(x1, x2).setYBounds(y1, y2)
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxHelper.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxHelper.kt
index 087e544..68f902b 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxHelper.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxHelper.kt
@@ -31,7 +31,7 @@
rectYMin: Float,
rectXMax: Float,
rectYMax: Float,
- out: MutablePoint,
+ out: MutableVec,
)
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableAffineTransform.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableAffineTransform.kt
index aea127e..1b417cb 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableAffineTransform.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableAffineTransform.kt
@@ -17,21 +17,22 @@
package androidx.ink.geometry
import androidx.annotation.RestrictTo
+import androidx.annotation.Size
/**
* An affine transformation in the plane. The transformation can be thought of as a 3x3 matrix:
* ```
- * ⎡a b c⎤
- * ⎢d e f⎥
- * ⎣0 0 1⎦
+ * ⎡m00 m10 m20⎤
+ * ⎢m01 m11 m21⎥
+ * ⎣ 0 0 1 ⎦
* ```
*
* Applying the transformation can be thought of as a matrix multiplication, with the
* to-be-transformed point represented as a column vector with an extra 1:
* ```
- * ⎡a b c⎤ ⎡x⎤ ⎡a*x + b*y + c⎤
- * ⎢d e f⎥ * ⎢y⎥ = ⎢d*x + e*y + f⎥
- * ⎣0 0 1⎦ ⎣1⎦ ⎣ 1 ⎦
+ * ⎡m00 m10 m20⎤ ⎡x⎤ ⎡m00*x + m10*y + m20⎤
+ * ⎢m01 m11 m21⎥ * ⎢y⎥ = ⎢m01*x + m11*y + m21⎥
+ * ⎣ 0 0 1 ⎦ ⎣1⎦ ⎣ 1 ⎦
* ```
*
* Transformations are composed via multiplication. Multiplication is not commutative (i.e. A*B !=
@@ -42,25 +43,45 @@
* val translate = ImmutableAffineTransform.translate(Vec(10, 0))
* ```
*
- * then the `rotate * translate` first translates 10 units in the positive x-direction, then rotates
- * 90° about the origin.
- *
- * This class follows AndroidX guidelines ({@link http://go/androidx-api-guidelines#kotlin-data}) to
- * avoid Kotlin data classes.
+ * then `rotate * translate` first translates 10 units in the positive x-direction, then rotates 45°
+ * about the origin.
*
* See [MutableAffineTransform] for mutable alternative to this class.
+ *
+ * @constructor Constructs this transform with 6 float values, starting with the top left corner of
+ * the matrix and proceeding in row-major order. Prefer to create this object with functions that
+ * apply specific transform operations, such as [scale] or [translate], rather than directly
+ * passing in the actual numeric values of this transform. This constructor is useful for when the
+ * values are needed to be provided all at once, for example for serialization. To access these
+ * values in the same order as they are passed in here, use [AffineTransform.getValues]. To
+ * construct this object using an array as input, there is another public constructor for that.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class ImmutableAffineTransform(
- override val a: Float,
- override val b: Float,
- override val c: Float,
- override val d: Float,
- override val e: Float,
- override val f: Float,
-) : AffineTransform {
+public class ImmutableAffineTransform
+public constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override val m00: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override val m10: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override val m20: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override val m01: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override val m11: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override val m21: Float,
+) : AffineTransform() {
- override public fun asImmutable(): ImmutableAffineTransform = this
+ /**
+ * Like the primary constructor, but accepts a [FloatArray] instead of individual [Float]
+ * values.
+ */
+ public constructor(
+ @Size(min = 6) values: FloatArray
+ ) : this(values[0], values[1], values[2], values[3], values[4], values[5])
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public override fun asImmutable(): ImmutableAffineTransform = this
/**
* Component-wise equality operator for [ImmutableAffineTransform].
@@ -70,18 +91,18 @@
* [equals] in some cases.
*/
override fun equals(other: Any?): Boolean =
- other === this || (other is AffineTransform && AffineTransform.areEquivalent(this, other))
+ other === this || (other is AffineTransform && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = AffineTransform.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Immutable${AffineTransform.hash(this)}"
+ override fun toString(): String = "Immutable${string(this)}"
public companion object {
/** Returns a transformation that translates by the given [offset] vector. */
@JvmStatic
public fun translate(offset: Vec): ImmutableAffineTransform =
- ImmutableAffineTransform(a = 1f, b = 0f, c = offset.x, d = 0f, e = 1f, f = offset.y)
+ ImmutableAffineTransform(1f, 0f, offset.x, 0f, 1f, offset.y)
/**
* Returns a transformation that scales in both the x- and y-direction by the given pair of
@@ -89,14 +110,7 @@
*/
@JvmStatic
public fun scale(xScaleFactor: Float, yScaleFactor: Float): ImmutableAffineTransform =
- ImmutableAffineTransform(
- a = xScaleFactor,
- b = 0f,
- c = 0f,
- d = 0f,
- e = yScaleFactor,
- f = 0f
- )
+ ImmutableAffineTransform(xScaleFactor, 0f, 0f, 0f, yScaleFactor, 0f)
/**
* Returns a transformation that scales in both the x- and y-direction by the given
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableBox.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableBox.kt
index 163c613..916ce00 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableBox.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableBox.kt
@@ -17,7 +17,6 @@
package androidx.ink.geometry
import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
import kotlin.math.max
import kotlin.math.min
@@ -28,8 +27,7 @@
* (e.g. the positive `Y` axis being "down"), because it is intended to be used with any coordinate
* system rather than just Android screen/View space.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class ImmutableBox private constructor(x1: Float, y1: Float, x2: Float, y2: Float) : Box {
+public class ImmutableBox private constructor(x1: Float, y1: Float, x2: Float, y2: Float) : Box() {
/** The lower bound in the `X` direction. */
override val xMin: Float = min(x1, x2)
@@ -43,35 +41,6 @@
/** The upper bound in the `Y` direction. */
override val yMax: Float = max(y1, y2)
- public fun fillMutable(output: MutableBox) {
- output.setXBounds(xMin, xMax).setYBounds(yMin, yMax)
- }
-
- public fun newMutable(): MutableBox {
- return MutableBox().setXBounds(xMin, xMax).setYBounds(yMin, yMax)
- }
-
- /** Populates [out] with the center of the [ImmutableBox]. */
- override fun center(out: MutablePoint): Unit =
- BoxHelper.nativeCenter(xMin, yMin, xMax, yMax, out)
-
- /**
- * Return a copy of this object with modified values as provided, where [x1] and [y1] default to
- * minimum values and [x2] and [y2] default to the maximum values, respectively.
- */
- @JvmSynthetic
- public fun copy(
- x1: Float = this.xMin,
- y1: Float = this.yMin,
- x2: Float = this.xMax,
- y2: Float = this.yMax,
- ): ImmutableBox =
- if (this.xMin == x1 && this.yMin == y1 && this.xMax == x2 && this.yMax == y2) {
- this
- } else {
- ImmutableBox(x1, y1, x2, y2)
- }
-
override fun equals(other: Any?): Boolean =
other === this || (other is Box && Box.areEquivalent(this, other))
@@ -84,7 +53,7 @@
/** Constructs an [ImmutableBox] with a given [center], [width], and [height]. */
@JvmStatic
public fun fromCenterAndDimensions(
- center: Point,
+ center: Vec,
@FloatRange(from = 0.0) width: Float,
@FloatRange(from = 0.0) height: Float,
): ImmutableBox {
@@ -99,7 +68,7 @@
/** Constructs the smallest [ImmutableBox] containing the two given points. */
@JvmStatic
- public fun fromTwoPoints(point1: Point, point2: Point): ImmutableBox {
+ public fun fromTwoPoints(point1: Vec, point2: Vec): ImmutableBox {
return ImmutableBox(point1.x, point1.y, point2.x, point2.y)
}
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableParallelogram.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableParallelogram.kt
index 95ca1f7..750093c 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableParallelogram.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableParallelogram.kt
@@ -17,29 +17,27 @@
package androidx.ink.geometry
import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
/**
* Immutable parallelogram (i.e. a quadrilateral with parallel sides), defined by its [center],
* [width], [height], [rotation], and [shearFactor].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class ImmutableParallelogram
private constructor(
- override val center: ImmutablePoint,
+ override val center: ImmutableVec,
override val width: Float,
override val height: Float,
@AngleRadiansFloat override val rotation: Float,
override val shearFactor: Float,
-) : Parallelogram {
+) : Parallelogram() {
override fun equals(other: Any?): Boolean =
- other === this || (other is Parallelogram && Parallelogram.areEquivalent(this, other))
+ other === this || (other is Parallelogram && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Parallelogram.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Immutable${Parallelogram.string(this)}"
+ override fun toString(): String = "Immutable${string(this)}"
public companion object {
@@ -50,14 +48,11 @@
*/
@JvmStatic
public fun fromCenterAndDimensions(
- center: ImmutablePoint,
+ center: ImmutableVec,
@FloatRange(from = 0.0) width: Float,
height: Float,
): ImmutableParallelogram =
- Parallelogram.normalizeAndRun(width, height, rotation = Angle.ZERO) {
- w: Float,
- h: Float,
- r: Float ->
+ normalizeAndRun(width, height, rotation = Angle.ZERO) { w: Float, h: Float, r: Float ->
ImmutableParallelogram(center, w, h, r, shearFactor = 0f)
}
@@ -69,12 +64,12 @@
*/
@JvmStatic
public fun fromCenterDimensionsAndRotation(
- center: ImmutablePoint,
+ center: ImmutableVec,
@FloatRange(from = 0.0) width: Float,
height: Float,
@AngleRadiansFloat rotation: Float,
): ImmutableParallelogram =
- Parallelogram.normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
+ normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
ImmutableParallelogram(center, w, h, r, shearFactor = 0f)
}
@@ -85,13 +80,13 @@
*/
@JvmStatic
public fun fromCenterDimensionsRotationAndShear(
- center: ImmutablePoint,
+ center: ImmutableVec,
@FloatRange(from = 0.0) width: Float,
height: Float,
@AngleRadiansFloat rotation: Float,
shearFactor: Float,
): ImmutableParallelogram =
- Parallelogram.normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
+ normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
ImmutableParallelogram(center, w, h, r, shearFactor)
}
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutablePoint.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutablePoint.kt
deleted file mode 100644
index 44eb30c..0000000
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutablePoint.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import androidx.annotation.RestrictTo
-import kotlin.jvm.JvmSynthetic
-
-/** Represents a location in 2-dimensional space. See [MutablePoint] for a mutable alternative. */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class ImmutablePoint(override val x: Float, override val y: Float) : Point {
- /** Fills [output] with the x and y coordinates of this [ImmutablePoint] */
- public fun fillMutable(output: MutablePoint) {
- output.x = this.x
- output.y = this.y
- }
-
- /** Returns a [MutablePoint] containing the same x and y coordinates as this [ImmutablePoint] */
- public fun newMutable(): MutablePoint {
- return MutablePoint(x, y)
- }
-
- /** Return a copy of this object with modified x and y as provided. */
- @JvmSynthetic
- public fun copy(x: Float = this.x, y: Float = this.y): ImmutablePoint =
- if (x == this.x && y == this.y) this else ImmutablePoint(x, y)
-
- override fun equals(other: Any?): Boolean =
- other === this || (other is Point && Point.areEquivalent(this, other))
-
- // NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Point.hash(this)
-
- override fun toString(): String = "Immutable${Point.string(this)}"
-}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableSegment.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableSegment.kt
index 4e965ce..a7e20db 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableSegment.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableSegment.kt
@@ -22,54 +22,18 @@
* Represents a directed line segment between two points. See [MutableSegment] for mutable
* alternative.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class ImmutableSegment(start: Vec, end: Vec) : Segment {
+public class ImmutableSegment(start: Vec, end: Vec) : Segment() {
- @Suppress("Immutable") override val start: Vec = start.asImmutable
- @Suppress("Immutable") override val end: Vec = end.asImmutable
+ @Suppress("Immutable") override val start: Vec = start.asImmutable()
+ @Suppress("Immutable") override val end: Vec = end.asImmutable()
- /**
- * Caches the result of [vec] if it is called. This format is used to avoid unnecessary
- * allocations on construction, and avoid extra allocations if [vec] is called multiple times.
- * Although the Immutable lint is being suppressed, this object is still immutable as its
- * visible data cannot be modified.
- */
- @Suppress("Immutable") private var _vec: ImmutableVec? = null
-
- override val vec: ImmutableVec
- get() = _vec ?: ImmutableVec(end.x - start.x, end.y - start.y).also { _vec = it }
-
- override fun asImmutable(): ImmutableSegment = this
-
- @JvmSynthetic
- override fun asImmutable(start: Vec, end: Vec): ImmutableSegment {
- if (this.start === start && this.end === end) {
- return this
- }
-
- return ImmutableSegment(start, end)
- }
-
- /**
- * Caches the result of [midpoint] if it is called. This format is used to avoid unnecessary
- * allocations on construction, and avoid extra allocations if [midpoint] is called multiple
- * times. Although the Immutable lint is being suppressed, this object is still immutable as its
- * visible data cannot be modified.
- */
- @Suppress("Immutable") private var _midpoint: ImmutableVec? = null
-
- override val midpoint: ImmutableVec
- get() =
- _midpoint
- ?: ImmutableVec((start.x + end.x) / 2, (start.y + end.y) / 2).also {
- _midpoint = it
- }
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun asImmutable(): ImmutableSegment = this
override fun equals(other: Any?): Boolean =
- other === this || (other is Segment && Segment.areEquivalent(this, other))
+ other === this || (other is Segment && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Segment.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Immutable${Segment.string(this)}"
+ override fun toString(): String = "Immutable${string(this)}"
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableTriangle.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableTriangle.kt
index c99da80..183bb8e 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableTriangle.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableTriangle.kt
@@ -22,33 +22,23 @@
* An immutable triangle, defined by its three corners [p0], [p1] and [p2] in order. This object is
* immutable, so it is inherently thread-safe. See [MutableTriangle] for the mutable version.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class ImmutableTriangle(p0: Vec, p1: Vec, p2: Vec) : Triangle {
+public class ImmutableTriangle(p0: Vec, p1: Vec, p2: Vec) : Triangle() {
- @Suppress("Immutable") override val p0: Vec = p0.asImmutable
- @Suppress("Immutable") override val p1: Vec = p1.asImmutable
- @Suppress("Immutable") override val p2: Vec = p2.asImmutable
+ @Suppress("Immutable") override val p0: Vec = p0.asImmutable()
+ @Suppress("Immutable") override val p1: Vec = p1.asImmutable()
+ @Suppress("Immutable") override val p2: Vec = p2.asImmutable()
- override fun asImmutable(): ImmutableTriangle = this
-
- @JvmSynthetic
- override fun asImmutable(p0: Vec, p1: Vec, p2: Vec): ImmutableTriangle {
- if (this.p0 === p0 && this.p1 === p1 && this.p2 === p2) {
- return this
- }
-
- return ImmutableTriangle(p0, p1, p2)
- }
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun asImmutable(): ImmutableTriangle = this
/**
* Equality for [ImmutableTriangle] is defined using the order in which [p0], [p1] and [p2] are
* defined. Rotated/flipped triangles with out-of-order vertices are not considered equal.
*/
override fun equals(other: Any?): Boolean =
- other === this || (other is Triangle && Triangle.areEquivalent(this, other))
+ other === this || (other is Triangle && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode.
- override fun hashCode(): Int = Triangle.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Immutable${Triangle.string(this)}"
+ override fun toString(): String = "Immutable${string(this)}"
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableVec.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableVec.kt
index fe0b830..01b29ec 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableVec.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ImmutableVec.kt
@@ -17,48 +17,29 @@
package androidx.ink.geometry
import androidx.annotation.RestrictTo
-import kotlin.jvm.JvmSynthetic
import kotlin.math.cos
-import kotlin.math.hypot
import kotlin.math.sin
/**
- * An immutable 2-dimensional vector, representing an offset in space. See [MutableVec] for a
- * mutable alternative, and see [Point] (and its concrete implementations [ImmutablePoint] and
- * [MutablePoint]) for a location in space.
+ * An immutable two-dimensional vector, i.e. an (x, y) coordinate pair. It can be used to represent
+ * either:
+ * 1) A two-dimensional offset, i.e. the difference between two points
+ * 2) A point in space, i.e. treating the vector as an offset from the origin
+ *
+ * This object is immutable, so it is inherently thread-safe. See [MutableVec] for a mutable
+ * alternative.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class ImmutableVec(override val x: Float, override val y: Float) : Vec {
+public class ImmutableVec(override val x: Float, override val y: Float) : Vec() {
- /** Fills [output] with the x and y coordinates of this [ImmutableVec] */
- public fun fillMutable(output: MutableVec) {
- output.x = this.x
- output.y = this.y
- }
-
- /** Returns a [MutableVec] containing the same x and y coordinates as this [ImmutableVec] */
- public fun newMutable(): MutableVec {
- return MutableVec(x, y)
- }
-
- override val magnitude: Float = hypot(x, y)
-
- override val magnitudeSquared: Float = x * x + y * y
-
- override val asImmutable: ImmutableVec = this
-
- @JvmSynthetic
- override fun asImmutable(x: Float, y: Float): ImmutableVec {
- return if (x == this.x && y == this.y) this else ImmutableVec(x, y)
- }
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun asImmutable(): ImmutableVec = this
override fun equals(other: Any?): Boolean =
- other === this || (other is Vec && Vec.areEquivalent(this, other))
+ other === this || (other is Vec && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Vec.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Immutable${Vec.string(this)}"
+ override fun toString(): String = "Immutable${string(this)}"
public companion object {
@JvmStatic
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index ce3fce9..1ee275d 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -23,7 +23,6 @@
* Contains functions for intersection of ink geometry classes. For Kotlin callers, these are
* available as extension functions on the geometry classes themselves.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public object Intersection {
init {
@@ -117,17 +116,18 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- public fun Vec.intersects(mesh: ModeledShape, meshToPoint: AffineTransform): Boolean {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
return nativeMeshVecIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
vecX = this.x,
vecY = this.y,
- meshToVecA = meshToPoint.a,
- meshToVecB = meshToPoint.b,
- meshToVecC = meshToPoint.c,
- meshToVecD = meshToPoint.d,
- meshToVecE = meshToPoint.e,
- meshToVecF = meshToPoint.f,
+ meshToVecA = meshToPoint.m00,
+ meshToVecB = meshToPoint.m10,
+ meshToVecC = meshToPoint.m20,
+ meshToVecD = meshToPoint.m01,
+ meshToVecE = meshToPoint.m11,
+ meshToVecF = meshToPoint.m21,
)
}
@@ -218,19 +218,20 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun Segment.intersects(mesh: ModeledShape, meshToSegment: AffineTransform): Boolean {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
return nativeMeshSegmentIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
segmentStartX = this.start.x,
segmentStartY = this.start.y,
segmentEndX = this.end.x,
segmentEndY = this.end.y,
- meshToSegmentA = meshToSegment.a,
- meshToSegmentB = meshToSegment.b,
- meshToSegmentC = meshToSegment.c,
- meshToSegmentD = meshToSegment.d,
- meshToSegmentE = meshToSegment.e,
- meshToSegmentF = meshToSegment.f,
+ meshToSegmentA = meshToSegment.m00,
+ meshToSegmentB = meshToSegment.m10,
+ meshToSegmentC = meshToSegment.m20,
+ meshToSegmentD = meshToSegment.m01,
+ meshToSegmentE = meshToSegment.m11,
+ meshToSegmentF = meshToSegment.m21,
)
}
@@ -310,7 +311,11 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun Triangle.intersects(mesh: ModeledShape, meshToTriangle: AffineTransform): Boolean {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun Triangle.intersects(
+ mesh: PartitionedMesh,
+ meshToTriangle: AffineTransform
+ ): Boolean {
return nativeMeshTriangleIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
triangleP0X = this.p0.x,
@@ -319,12 +324,12 @@
triangleP1Y = this.p1.y,
triangleP2X = this.p2.x,
triangleP2Y = this.p2.y,
- meshToTriangleA = meshToTriangle.a,
- meshToTriangleB = meshToTriangle.b,
- meshToTriangleC = meshToTriangle.c,
- meshToTriangleD = meshToTriangle.d,
- meshToTriangleE = meshToTriangle.e,
- meshToTriangleF = meshToTriangle.f,
+ meshToTriangleA = meshToTriangle.m00,
+ meshToTriangleB = meshToTriangle.m10,
+ meshToTriangleC = meshToTriangle.m20,
+ meshToTriangleD = meshToTriangle.m01,
+ meshToTriangleE = meshToTriangle.m11,
+ meshToTriangleF = meshToTriangle.m21,
)
}
@@ -377,19 +382,20 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun Box.intersects(mesh: ModeledShape, meshToBox: AffineTransform): Boolean {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
return nativeMeshBoxIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
boxXMin = this.xMin,
boxYMin = this.yMin,
boxXMax = this.xMax,
boxYMax = this.yMax,
- meshToBoxA = meshToBox.a,
- meshToBoxB = meshToBox.b,
- meshToBoxC = meshToBox.c,
- meshToBoxD = meshToBox.d,
- meshToBoxE = meshToBox.e,
- meshToBoxF = meshToBox.f,
+ meshToBoxA = meshToBox.m00,
+ meshToBoxB = meshToBox.m10,
+ meshToBoxC = meshToBox.m20,
+ meshToBoxD = meshToBox.m01,
+ meshToBoxE = meshToBox.m11,
+ meshToBoxF = meshToBox.m21,
)
}
@@ -430,8 +436,9 @@
* checked in.
*/
@JvmStatic
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Parallelogram.intersects(
- mesh: ModeledShape,
+ mesh: PartitionedMesh,
meshToParallelogram: AffineTransform,
): Boolean {
return nativeMeshParallelogramIntersects(
@@ -442,12 +449,12 @@
parallelogramHeight = this.height,
parallelogramAngleInRadian = this.rotation,
parallelogramShearFactor = this.shearFactor,
- meshToParallelogramA = meshToParallelogram.a,
- meshToParallelogramB = meshToParallelogram.b,
- meshToParallelogramC = meshToParallelogram.c,
- meshToParallelogramD = meshToParallelogram.d,
- meshToParallelogramE = meshToParallelogram.e,
- meshToParallelogramF = meshToParallelogram.f,
+ meshToParallelogramA = meshToParallelogram.m00,
+ meshToParallelogramB = meshToParallelogram.m10,
+ meshToParallelogramC = meshToParallelogram.m20,
+ meshToParallelogramD = meshToParallelogram.m01,
+ meshToParallelogramE = meshToParallelogram.m11,
+ meshToParallelogramF = meshToParallelogram.m21,
)
}
@@ -460,26 +467,27 @@
* coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun ModeledShape.intersects(
- other: ModeledShape,
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun PartitionedMesh.intersects(
+ other: PartitionedMesh,
thisToCommonTransForm: AffineTransform,
otherToCommonTransform: AffineTransform,
): Boolean {
return nativeMeshModeledShapeIntersects(
thisModeledShapeAddress = this.getNativeAddress(),
otherModeledShapeAddress = other.getNativeAddress(),
- thisToCommonTransformA = thisToCommonTransForm.a,
- thisToCommonTransformB = thisToCommonTransForm.b,
- thisToCommonTransformC = thisToCommonTransForm.c,
- thisToCommonTransformD = thisToCommonTransForm.d,
- thisToCommonTransformE = thisToCommonTransForm.e,
- thisToCommonTransformF = thisToCommonTransForm.f,
- otherToCommonTransformA = otherToCommonTransform.a,
- otherToCommonTransformB = otherToCommonTransform.b,
- otherToCommonTransformC = otherToCommonTransform.c,
- otherToCommonTransformD = otherToCommonTransform.d,
- otherToCommonTransformE = otherToCommonTransform.e,
- otherToCommonTransformF = otherToCommonTransform.f,
+ thisToCommonTransformA = thisToCommonTransForm.m00,
+ thisToCommonTransformB = thisToCommonTransForm.m10,
+ thisToCommonTransformC = thisToCommonTransForm.m20,
+ thisToCommonTransformD = thisToCommonTransForm.m01,
+ thisToCommonTransformE = thisToCommonTransForm.m11,
+ thisToCommonTransformF = thisToCommonTransForm.m21,
+ otherToCommonTransformA = otherToCommonTransform.m00,
+ otherToCommonTransformB = otherToCommonTransform.m10,
+ otherToCommonTransformC = otherToCommonTransform.m20,
+ otherToCommonTransformD = otherToCommonTransform.m01,
+ otherToCommonTransformE = otherToCommonTransform.m11,
+ otherToCommonTransformF = otherToCommonTransform.m21,
)
}
@@ -558,7 +566,8 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- public fun ModeledShape.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
point.intersects(this, meshToPoint)
/**
@@ -569,8 +578,11 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun ModeledShape.intersects(segment: Segment, meshToSegment: AffineTransform): Boolean =
- segment.intersects(this, meshToSegment)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun PartitionedMesh.intersects(
+ segment: Segment,
+ meshToSegment: AffineTransform
+ ): Boolean = segment.intersects(this, meshToSegment)
/**
* Returns true when a [PartitionedMesh] intersects with a [Triangle].
@@ -580,9 +592,10 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun ModeledShape.intersects(
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun PartitionedMesh.intersects(
triangle: Triangle,
- meshToTriangle: AffineTransform
+ meshToTriangle: AffineTransform,
): Boolean = triangle.intersects(this, meshToTriangle)
/**
@@ -593,7 +606,8 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- public fun ModeledShape.intersects(box: Box, meshToBox: AffineTransform): Boolean =
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
box.intersects(this, meshToBox)
/**
@@ -605,7 +619,8 @@
* checked in.
*/
@JvmStatic
- public fun ModeledShape.intersects(
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun PartitionedMesh.intersects(
parallelogram: Parallelogram,
meshToParallelogram: AffineTransform,
): Boolean = parallelogram.intersects(this, meshToParallelogram)
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Mesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Mesh.kt
index 5c3e74f..e3959af 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Mesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Mesh.kt
@@ -135,7 +135,7 @@
* [vertexCount]). The resulting x/y position of that vertex will be put into [outPosition],
* which can be pre-allocated and reused to avoid allocations where appropriate.
*/
- public fun fillPosition(@IntRange(from = 0) vertexIndex: Int, outPosition: MutablePoint) {
+ public fun fillPosition(@IntRange(from = 0) vertexIndex: Int, outPosition: MutableVec) {
require(vertexIndex >= 0 && vertexIndex < vertexCount) {
"vertexIndex=$vertexIndex must be between 0 and vertexCount=$vertexCount."
}
@@ -239,7 +239,7 @@
external fun fillPosition(
nativeAddress: Long,
vertexIndex: Int,
- outPosition: MutablePoint
+ outPosition: MutableVec
) // TODO: b/355248266 - @Keep must go in Proguard config file instead.
@VisibleForTesting
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableAffineTransform.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableAffineTransform.kt
index c1c9572..26bce28 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableAffineTransform.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableAffineTransform.kt
@@ -17,22 +17,22 @@
package androidx.ink.geometry
import androidx.annotation.RestrictTo
+import androidx.annotation.Size
/**
- * A mutable affine transformation in the plane. The transformation can be thought of as a 3x3
- * matrix:
+ * An affine transformation in the plane. The transformation can be thought of as a 3x3 matrix:
* ```
- * ⎡a b c⎤
- * ⎢d e f⎥
- * ⎣0 0 1⎦
+ * ⎡m00 m10 m20⎤
+ * ⎢m01 m11 m21⎥
+ * ⎣ 0 0 1 ⎦
* ```
*
* Applying the transformation can be thought of as a matrix multiplication, with the
* to-be-transformed point represented as a column vector with an extra 1:
* ```
- * ⎡a b c⎤ ⎡x⎤ ⎡a*x + b*y + c⎤
- * ⎢d e f⎥ * ⎢y⎥ = ⎢d*x + e*y + f⎥
- * ⎣0 0 1⎦ ⎣1⎦ ⎣ 1 ⎦
+ * ⎡m00 m10 m20⎤ ⎡x⎤ ⎡m00*x + m10*y + m20⎤
+ * ⎢m01 m11 m21⎥ * ⎢y⎥ = ⎢m01*x + m11*y + m21⎥
+ * ⎣ 0 0 1 ⎦ ⎣1⎦ ⎣ 1 ⎦
* ```
*
* Transformations are composed via multiplication. Multiplication is not commutative (i.e. A*B !=
@@ -43,23 +43,42 @@
* val translate = ImmutableAffineTransform.translate(Vec(10, 0))
* ```
*
- * then the `rotate * translate` first translates 10 units in the positive x-direction, then rotates
- * 90° about the origin.
+ * then `rotate * translate` first translates 10 units in the positive x-direction, then rotates 45°
+ * about the origin.
*
- * This class follows AndroidX guidelines ({@link http://go/androidx-api-guidelines#kotlin-data}) to
- * avoid Kotlin data classes.
+ * See [ImmutableAffineTransform] for an immutable alternative to this class.
*
- * See [ImmutableAffineTransform] for immutable alternative to this class.
+ * @constructor Constructs this transform with 6 float values, starting with the top left corner of
+ * the matrix and proceeding in row-major order. Prefer to create this object with functions that
+ * apply specific transform operations, such as [populateFromScale] or [populateFromRotate],
+ * rather than directly passing in the actual numeric values of this transform. This constructor
+ * is useful for when the values are needed to be provided all at once, for example for
+ * serialization. To access these values in the same order as they are passed in here, use
+ * [AffineTransform.getValues]. To construct this object using an array as input, there is another
+ * public constructor for that.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class MutableAffineTransform(
- override var a: Float,
- override var b: Float,
- override var c: Float,
- override var d: Float,
- override var e: Float,
- override var f: Float,
-) : AffineTransform {
+public class MutableAffineTransform
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override var m00: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override var m10: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override var m20: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override var m01: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override var m11: Float,
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ override var m21: Float,
+) : AffineTransform() {
/**
* Constructs an identity [MutableAffineTransform]:
@@ -69,10 +88,44 @@
* ⎣0 0 1⎦
* ```
*
- * This is useful when pre-allocating a scratch instance to be filled later. (e.g. using
- * [ImmutableAffineTransform.fillMutable] method)
+ * This is useful when pre-allocating a scratch instance to be filled later.
*/
- public constructor() : this(a = 1f, b = 0f, c = 0f, d = 0f, e = 1f, f = 0f)
+ public constructor() : this(1f, 0f, 0f, 0f, 1f, 0f)
+
+ /**
+ * Populates this transform with the given values, starting with the top left corner of the
+ * matrix and proceeding in row-major order.
+ *
+ * Prefer to modify this object with functions that apply specific transform operations, such as
+ * [populateFromScale] or [populateFromRotate], rather than directly setting the actual numeric
+ * values of this transform. This function is useful for when the values are needed to be
+ * provided in bulk, for example for serialization.
+ *
+ * To access these values in the same order as they are set here, use
+ * [AffineTransform.getValues].
+ */
+ public fun setValues(m00: Float, m10: Float, m20: Float, m01: Float, m11: Float, m21: Float) {
+ this.m00 = m00
+ this.m10 = m10
+ this.m20 = m20
+ this.m01 = m01
+ this.m11 = m11
+ this.m21 = m21
+ }
+
+ /** Like [setValues], but accepts a [FloatArray] instead of individual float values. */
+ public fun setValues(@Size(min = 6) values: FloatArray) {
+ m00 = values[0]
+ m10 = values[1]
+ m20 = values[2]
+ m01 = values[3]
+ m11 = values[4]
+ m21 = values[5]
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ override fun asImmutable(): ImmutableAffineTransform =
+ ImmutableAffineTransform(m00, m10, m20, m01, m11, m21)
/**
* Component-wise equality operator for [MutableAffineTransform].
@@ -82,10 +135,10 @@
* [equals] in some cases.
*/
override fun equals(other: Any?): Boolean =
- other === this || (other is AffineTransform && AffineTransform.areEquivalent(this, other))
+ other === this || (other is AffineTransform && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = AffineTransform.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Mutable${AffineTransform.hash(this)}"
+ override fun toString(): String = "Mutable${string(this)}"
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableBox.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableBox.kt
index 9a417da..9721bfc 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableBox.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableBox.kt
@@ -17,7 +17,6 @@
package androidx.ink.geometry
import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
import kotlin.math.max
import kotlin.math.min
@@ -28,8 +27,7 @@
* (e.g. the positive Y axis being "down"), because it is intended to be used with any coordinate
* system rather than just Android screen/View space.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class MutableBox private constructor(x1: Float, y1: Float, x2: Float, y2: Float) : Box {
+public class MutableBox private constructor(x1: Float, y1: Float, x2: Float, y2: Float) : Box() {
/** The lower bound in the `X` direction. */
override var xMin: Float = min(x1, x2)
@@ -47,10 +45,6 @@
override var yMax: Float = max(y1, y2)
private set
- /** Populates [out] with the center of the [MutableBox]. */
- override fun center(out: MutablePoint): Unit =
- BoxHelper.nativeCenter(xMin, yMin, xMax, yMax, out)
-
/**
* Sets the lower and upper bounds in the `X` direction to new values. The minimum value becomes
* `xMin`, and the maximum value becomes `xMax`. Returns the same instance to chain function
@@ -80,7 +74,7 @@
public constructor() : this(0f, 0f, 0f, 0f)
/** Constructs the smallest [MutableBox] containing the two given points. */
- public fun fillFromTwoPoints(point1: Point, point2: Point): MutableBox {
+ public fun populateFromTwoPoints(point1: Vec, point2: Vec): MutableBox {
setXBounds(point1.x, point2.x)
setYBounds(point1.y, point2.y)
return this
@@ -90,8 +84,8 @@
* Constructs a [MutableBox] with a given [center], [width], and [height]. [width] and [height]
* must be non-negative numbers.
*/
- public fun fillFromCenterAndDimensions(
- center: Point,
+ public fun populateFromCenterAndDimensions(
+ center: Vec,
@FloatRange(from = 0.0) width: Float,
@FloatRange(from = 0.0) height: Float,
): MutableBox {
@@ -110,16 +104,6 @@
return this
}
- /** Convert this object to a new immutable [Box]. */
- public fun buildBox(): ImmutableBox {
- return ImmutableBox.fromTwoPoints(ImmutablePoint(xMin, yMin), ImmutablePoint(xMax, yMax))
- }
-
- /** Return a copy of this object that can be modified independently. */
- public fun copy(): MutableBox {
- return MutableBox(xMin, yMin, xMax, yMax)
- }
-
override fun equals(other: Any?): Boolean =
other === this || (other is Box && Box.areEquivalent(this, other))
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableParallelogram.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableParallelogram.kt
index e899efa..c700fc1 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableParallelogram.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableParallelogram.kt
@@ -22,34 +22,33 @@
/**
* Mutable parallelogram (i.e. a quadrilateral with parallel sides), defined by its [center],
* [width], [height], [rotation], and [shearFactor].
+ *
+ * @constructor Create the [MutableParallelogram] from an existing [MutableVec] instance and
+ * primitive [Float] parameters. Note that this instances will become the internal state of this
+ * [MutableParallelogram], so modifications made to it directly or through setters on this
+ * [MutableParallelogram] will modify the input [MutableVec] instances too. This is to allow
+ * performance-critical code to avoid any unnecessary allocations. This can be tricky to manage,
+ * especially in multithreaded code, so when calling code is unable to guarantee ownership of the
+ * nested mutable data at a particular time, it may be safest to construct this with a copy of the
+ * data to give this [MutableSegment] exclusive ownership of that copy.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class MutableParallelogram
private constructor(
- center: Point,
+ override var center: MutableVec,
width: Float,
override var height: Float,
@AngleRadiansFloat rotation: Float,
override var shearFactor: Float,
-) : Parallelogram {
+) : Parallelogram() {
- /* [_center] is a private backing field that is internally constructed such that no
- * caller can obtain a direct reference to it. */
- private var _center: MutablePoint = MutablePoint(center.x, center.y)
@AngleRadiansFloat private var _rotation: Float = Angle.normalized(rotation)
+
override var rotation: Float
@AngleRadiansFloat get() = _rotation
set(@AngleRadiansFloat value) {
_rotation = Angle.normalized(value)
}
- override var center: Point
- get() = _center
- set(value) {
- _center.x = value.x
- _center.y = value.y
- }
-
private var _width: Float = width
override var width: Float
@FloatRange(from = 0.0) get() = _width
@@ -57,7 +56,7 @@
// A [Parallelogram] may *not* have a negative width. If an operation is performed on
// [Parallelogram] resulting
// in a negative width, it will be normalized.
- Parallelogram.normalizeAndRun(value, height, rotation) { w: Float, h: Float, r: Float ->
+ normalizeAndRun(value, height, rotation) { w: Float, h: Float, r: Float ->
_width = w
height = h
rotation = r
@@ -65,7 +64,7 @@
}
}
- public constructor() : this(ImmutablePoint(0f, 0f), 0f, 0f, Angle.ZERO, 0f)
+ public constructor() : this(MutableVec(), 0f, 0f, Angle.ZERO, 0f)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
// TODO: b/355248266 - @UsedByNative("parallelogram_jni_helper.cc") must go in Proguard config
@@ -78,13 +77,13 @@
@AngleRadiansFloat rotation: Float,
shearFactor: Float,
): Unit = run {
- Parallelogram.normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
+ normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
this.width = w
this.height = h
this.rotation = r
this.shearFactor = shearFactor
- this._center.x = centerX
- this._center.y = centerY
+ this.center.x = centerX
+ this.center.y = centerY
this
}
}
@@ -93,9 +92,9 @@
other === this || (other is Parallelogram && Parallelogram.areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Parallelogram.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Mutable${Parallelogram.string(this)}"
+ override fun toString(): String = "Mutable${string(this)}"
public companion object {
@@ -106,14 +105,11 @@
*/
@JvmStatic
public fun fromCenterAndDimensions(
- center: Point,
+ center: MutableVec,
@FloatRange(from = 0.0) width: Float,
height: Float,
): MutableParallelogram =
- Parallelogram.normalizeAndRun(width, height, rotation = Angle.ZERO) {
- w: Float,
- h: Float,
- r: Float ->
+ normalizeAndRun(width, height, rotation = Angle.ZERO) { w: Float, h: Float, r: Float ->
MutableParallelogram(center, w, h, r, shearFactor = 0f)
}
@@ -125,12 +121,12 @@
*/
@JvmStatic
public fun fromCenterDimensionsAndRotation(
- center: Point,
+ center: MutableVec,
@FloatRange(from = 0.0) width: Float,
height: Float,
@AngleRadiansFloat rotation: Float,
): MutableParallelogram =
- Parallelogram.normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
+ normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
MutableParallelogram(center, w, h, r, shearFactor = 0f)
}
@@ -141,13 +137,13 @@
*/
@JvmStatic
public fun fromCenterDimensionsRotationAndShear(
- center: Point,
+ center: MutableVec,
@FloatRange(from = 0.0) width: Float,
height: Float,
@AngleRadiansFloat rotation: Float,
shearFactor: Float,
): MutableParallelogram =
- Parallelogram.normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
+ normalizeAndRun(width, height, rotation) { w: Float, h: Float, r: Float ->
MutableParallelogram(center, w, h, r, shearFactor)
}
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutablePoint.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutablePoint.kt
deleted file mode 100644
index 8308e1b..0000000
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutablePoint.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import androidx.annotation.RestrictTo
-
-/**
- * Represents a location in 2-dimensional space. See [ImmutablePoint] for an immutable alternative.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class MutablePoint(
- override var x:
- Float, // TODO: b/355248266 - @set:UsedByNative("point_jni_helper.cc") must go in Proguard
- // config file instead.
- override var y:
- Float, // TODO: b/355248266 - @set:UsedByNative("point_jni_helper.cc") must go in Proguard
- // config file instead.
-) : Point {
-
- /**
- * Constructs a [MutablePoint] without any initial data. This is useful when pre-allocating an
- * instance to be filled later.
- */
- public constructor() : this(0f, 0f)
-
- /** Construct an [ImmutablePoint] out of this [MutablePoint]. */
- public fun build(): ImmutablePoint = ImmutablePoint(x, y)
-
- override fun equals(other: Any?): Boolean =
- other === this || (other is Point && Point.areEquivalent(this, other))
-
- // NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Point.hash(this)
-
- override fun toString(): String = "Mutable${Point.string(this)}"
-}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableSegment.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableSegment.kt
index ecade29..de47366 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableSegment.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableSegment.kt
@@ -21,77 +21,39 @@
/**
* Represents a mutable directed line segment between two points. See [ImmutableSegment] for the
* immutable alternative.
+ *
+ * @constructor Create the [MutableSegment] from two existing [MutableVec] instances. Note that
+ * these instances will become the internal state of this [MutableSegment], so modifications made
+ * to them directly or through setters on this [MutableSegment] will modify the input [MutableVec]
+ * instances too. This is to allow performance-critical code to avoid any unnecessary allocations.
+ * This can be tricky to manage, especially in multithreaded code, so when calling code is unable
+ * to guarantee ownership of the nested mutable data at a particular time, it may be safest to
+ * construct this with copies of the data to give this [MutableSegment] exclusive ownership of
+ * those copies.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class MutableSegment(start: Vec, end: Vec) : Segment {
-
- private var _start = MutableVec(start.x, start.y)
- private var _end = MutableVec(end.x, end.y)
-
- override var start: Vec = _start
- private set
-
- override var end: Vec = _end
- private set
+public class MutableSegment(override var start: MutableVec, override var end: MutableVec) :
+ Segment() {
/** Constructs a degenerate [MutableSegment] with both [start] and [end] set to (0f, 0f) */
public constructor() : this(MutableVec(0f, 0f), MutableVec(0f, 0f))
- /** Sets this segment’s [start] point. */
- public fun start(point: Vec): MutableSegment {
- this._start.x = point.x
- this._start.y = point.y
- return this
- }
-
- /** Sets this segment's [start] point to ([x], [y]). */
- public fun start(x: Float, y: Float): MutableSegment {
- this._start.x = x
- this._start.y = y
- return this
- }
-
- /** Sets this segment’s [end] point. */
- public fun end(point: Vec): MutableSegment {
- this._end.x = point.x
- this._end.y = point.y
- return this
- }
-
- /** Sets this segment's [end] point to ([x], [y]). */
- public fun end(x: Float, y: Float): MutableSegment {
- this._end.x = x
- this._end.y = y
- return this
- }
-
- /** Fills this [MutableSegment] with the same values contained in [input]. */
+ /** Fills this [MutableSegment] with the same values contained in [input] and returns `this`. */
public fun populateFrom(input: Segment): MutableSegment {
- this._start.x = input.start.x
- this._start.y = input.start.y
- this._end.x = input.end.x
- this._end.y = input.end.y
+ this.start.x = input.start.x
+ this.start.y = input.start.y
+ this.end.x = input.end.x
+ this.end.y = input.end.y
return this
}
- override val vec: ImmutableVec
- get() = ImmutableVec(end.x - start.x, end.y - start.y)
-
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asImmutable(): ImmutableSegment = ImmutableSegment(this.start, this.end)
- @JvmSynthetic
- override fun asImmutable(start: Vec, end: Vec): ImmutableSegment {
- return ImmutableSegment(start, end)
- }
-
- override val midpoint: ImmutableVec
- get() = ImmutableVec((start.x + end.x) / 2, (start.y + end.y) / 2)
-
override fun equals(other: Any?): Boolean =
- other === this || (other is Segment && Segment.areEquivalent(this, other))
+ other === this || (other is Segment && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Segment.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Mutable${Segment.string(this)}"
+ override fun toString(): String = "Mutable${string(this)}"
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableTriangle.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableTriangle.kt
index 05ac2b6..05e5598 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableTriangle.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableTriangle.kt
@@ -21,95 +21,48 @@
/**
* Represents a mutable triangle, defined by its three corners [p0], [p1] and [p2] in order. See
* [ImmutableTriangle] for the immutable version.
+ *
+ * @constructor Create the [MutableTriangle] from three existing [MutableVec] instances. Note that
+ * these instances will become the internal state of this [MutableTriangle], so modifications made
+ * to them directly or through setters on this [MutableTriangle] will modify the input
+ * [MutableVec] instances too. This is to allow performance-critical code to avoid any unnecessary
+ * allocations. This can be tricky to manage, especially in multithreaded code, so when calling
+ * code is unable to guarantee ownership of the nested mutable data at a particular time, it may
+ * be safest to construct this with copies of the data to give this [MutableTriangle] exclusive
+ * ownership of those copies.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public class MutableTriangle(p0: Vec, p1: Vec, p2: Vec) : Triangle {
-
- private var _p0 = MutableVec(p0.x, p0.y)
- private var _p1 = MutableVec(p1.x, p1.y)
- private var _p2 = MutableVec(p2.x, p2.y)
-
- override var p0: Vec = _p0
- private set
-
- override var p1: Vec = _p1
- private set
-
- override var p2: Vec = _p2
- private set
+public class MutableTriangle(
+ override var p0: MutableVec,
+ override var p1: MutableVec,
+ override var p2: MutableVec,
+) : Triangle() {
/** Constructs a degenerate [MutableTriangle] with [p0], [p1], and [p2] set to (0, 0). */
public constructor() : this(MutableVec(0f, 0f), MutableVec(0f, 0f), MutableVec(0f, 0f))
- /** Sets [p0] equal to [value]. */
- public fun p0(value: Vec): MutableTriangle {
- _p0.x = value.x
- _p0.y = value.y
- return this
- }
-
- /** Sets [p0] to the location ([x], [y]). */
- public fun p0(x: Float, y: Float): MutableTriangle {
- _p0.x = x
- _p0.y = y
- return this
- }
-
- /** Sets [p1] equal to [value]. */
- public fun p1(value: Vec): MutableTriangle {
- _p1.x = value.x
- _p1.y = value.y
- return this
- }
-
- /** Sets [p1] to the location ([x], [y]). */
- public fun p1(x: Float, y: Float): MutableTriangle {
- _p1.x = x
- _p1.y = y
- return this
- }
-
- /** Sets [p2] equal to [value]. */
- public fun p2(value: Vec): MutableTriangle {
- _p2.x = value.x
- _p2.y = value.y
- return this
- }
-
- /** Sets [p2] to the location ([x], [y]). */
- public fun p2(x: Float, y: Float): MutableTriangle {
- _p2.x = x
- _p2.y = y
- return this
- }
-
- /** Copies the points from [input] to [this] [MutableTriangle]. */
+ /** Copies the points from [input] to this [MutableTriangle] and returns `this`. */
public fun populateFrom(input: Triangle): MutableTriangle {
- _p0.x = input.p0.x
- _p0.y = input.p0.y
- _p1.x = input.p1.x
- _p1.y = input.p1.y
- _p2.x = input.p2.x
- _p2.y = input.p2.y
+ p0.x = input.p0.x
+ p0.y = input.p0.y
+ p1.x = input.p1.x
+ p1.y = input.p1.y
+ p2.x = input.p2.x
+ p2.y = input.p2.y
return this
}
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asImmutable(): ImmutableTriangle = ImmutableTriangle(this.p0, this.p1, this.p2)
- @JvmSynthetic
- override fun asImmutable(p0: Vec, p1: Vec, p2: Vec): ImmutableTriangle {
- return ImmutableTriangle(p0, p1, p2)
- }
-
/**
* Equality for [MutableTriangle] is defined using the order in which [p0], [p1] and [p2] are
* defined. Rotated/flipped triangles with out-of-order vertices are not considered equal.
*/
override fun equals(other: Any?): Boolean =
- other === this || (other is Triangle && Triangle.areEquivalent(this, other))
+ other === this || (other is Triangle && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Triangle.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Mutable${Triangle.string(this)}"
+ override fun toString(): String = "Mutable${string(this)}"
}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableVec.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableVec.kt
index e1d3d7a..81226b1 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableVec.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/MutableVec.kt
@@ -16,54 +16,33 @@
package androidx.ink.geometry
-import androidx.annotation.FloatRange
import androidx.annotation.RestrictTo
import kotlin.math.cos
-import kotlin.math.hypot
import kotlin.math.sin
/**
- * A mutable 2-dimensional vector, representing an offset in space. See [ImmutableVec] for an
- * immutable alternative, and see [Point] (and its concrete implementations [ImmutablePoint] and
- * [MutablePoint]) for a location in space.
+ * A mutable two-dimensional vector, i.e. an (x, y) coordinate pair. It can be used to represent
+ * either:
+ * 1) A two-dimensional offset, i.e. the difference between two points
+ * 2) A point in space, i.e. treating the vector as an offset from the origin
+ *
+ * This object is mutable and is not inherently thread-safe, so callers should apply their own
+ * synchronization logic or use this object from a single thread. See [ImmutableVec] for an
+ * immutable alternative.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class MutableVec(
override var x:
- Float, // TODO: b/355248266 - @set:UsedByNative("vec_jni.cc") must go in Proguard config
- // file instead.
+ Float, // TODO: b/355248266 - @set:UsedByNative("vec_jni_helper.cc") must go in Proguard
+ // config file instead.
override var y:
- Float, // TODO: b/355248266 - @set:UsedByNative("vec_jni.cc") must go in Proguard config
- // file instead.
-) : Vec {
+ Float, // TODO: b/355248266 - @set:UsedByNative("vec_jni_helper.cc") must go in Proguard
+ // config file instead.
+) : Vec() {
- /**
- * Constructs a [MutableVec] without any initial data. This is useful when pre-allocating an
- * instance to be filled later.
- */
- public constructor() : this(0f, 0f)
+ public constructor() : this(0F, 0F)
- override val magnitude: Float
- @FloatRange(from = 0.0) get() = hypot(x, y)
-
- override val magnitudeSquared: Float
- @FloatRange(from = 0.0) get() = x * x + y * y
-
- override val asImmutable: ImmutableVec = ImmutableVec(x, y)
-
- @JvmSynthetic override fun asImmutable(x: Float, y: Float): ImmutableVec = ImmutableVec(x, y)
-
- /** Sets the value of [x]. */
- public fun x(value: Float): MutableVec {
- x = value
- return this
- }
-
- /** Sets the value of [y]. */
- public fun y(value: Float): MutableVec {
- y = value
- return this
- }
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ override fun asImmutable(): ImmutableVec = ImmutableVec(x, y)
/** Fills this [MutableVec] with the same values contained in [input]. */
public fun populateFrom(input: Vec): MutableVec {
@@ -73,12 +52,12 @@
}
override fun equals(other: Any?): Boolean =
- other === this || (other is Vec && Vec.areEquivalent(this, other))
+ other === this || (other is Vec && areEquivalent(this, other))
// NOMUTANTS -- not testing exact hashCode values, just that equality implies same hashCode
- override fun hashCode(): Int = Vec.hash(this)
+ override fun hashCode(): Int = hash(this)
- override fun toString(): String = "Mutable${Vec.string(this)}"
+ override fun toString(): String = "Mutable${string(this)}"
public companion object {
@JvmStatic
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Parallelogram.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Parallelogram.kt
index eb09035..629984b 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Parallelogram.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Parallelogram.kt
@@ -17,7 +17,6 @@
package androidx.ink.geometry
import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
import kotlin.math.abs
/**
@@ -80,41 +79,38 @@
* axes, and hence might have a non-zero [rotation]). A [Box], an axis-aligned rectangle; is a
* [Parallelogram] with both [rotation] and [shearFactor] of zero.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface Parallelogram {
+public abstract class Parallelogram internal constructor() {
- public val center: Point
+ public abstract val center: Vec
/**
* A [Parallelogram] may *not* have a negative width. If an operation on a parallelogram would
* result in a negative width, it is instead normalized, by negating both the width and the
* height, adding π to the angle of rotation, and normalizing rotation to the range [0, 2π).
*/
- @get:FloatRange(from = 0.0) public val width: Float
+ @get:FloatRange(from = 0.0) public abstract val width: Float
/**
* A [Parallelogram] may have a positive or negative height; a positive height indicates that
* the angle from the first semi-axis to the second will also be positive.
*/
- public val height: Float
+ public abstract val height: Float
- @get:AngleRadiansFloat public val rotation: Float
+ @get:AngleRadiansFloat public abstract val rotation: Float
/**
* A [Parallelogram]] may have a positive or negative shear factor; a positive shear factor
* indicates a smaller absolute angle between the semi-axes (the shear factor is, in fact, the
* cotangent of that angle).
*/
- public val shearFactor: Float
+ public abstract val shearFactor: Float
/**
- * A [Parallelogram] may have a positive or negative height; a positive height indicates that
- * the angle from the first semi-axis to the second will also be positive. A [Parallelogram]]
- * may have a positive or negative shear factor; a positive shear factor indicates a smaller
- * absolute angle between the semi-axes (the shear factor is, in fact, the cotangent of that
- * angle).
+ * Returns the signed area of the [Parallelogram]. If either the width or the height is zero,
+ * this will be equal to zero; if the width is non-zero, then this will have the same sign as
+ * the height.
*/
- public fun signedArea(): Float = width * height
+ public fun computeSignedArea(): Float = width * height
public companion object {
/**
@@ -140,7 +136,7 @@
* [Parallelogram].
*/
internal fun areEquivalent(first: Parallelogram, second: Parallelogram): Boolean =
- Point.areEquivalent(first.center, second.center) &&
+ Vec.areEquivalent(first.center, second.center) &&
first.width == second.width &&
first.height == second.height &&
first.rotation == second.rotation &&
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ModeledShape.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
similarity index 72%
rename from ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ModeledShape.kt
rename to ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index bd67136..d279937 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/ModeledShape.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -16,6 +16,7 @@
package androidx.ink.geometry
+import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
@@ -24,54 +25,57 @@
import androidx.ink.nativeloader.NativeLoader
/**
- * A triangulated shape, consisting of zero or more non-empty [Mesh]es, which may be indexed for
- * faster geometric queries. These meshes are divided among zero or more "render groups"; all the
- * meshes in a render group must have the same [MeshFormat], and can thus be rendered together. A
- * [ModeledShape] also optionally carries one or more "outlines", which are (potentially incomplete)
- * traversals of the vertices in the meshes, which could be used e.g. for path-based rendering. Note
- * that these render groups and outlines are ignored for the purposes of geometric queries; they
- * exist only for rendering purposes.
+ * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
+ * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
+ * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
*
- * This is not meant to be constructed directly by developers. The primary constructor is to have a
- * new instance of this class manage a native `ink::ModeledShape` instance created by another
- * Strokes API utility.
+ * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
+ * supports disjoint meshes such as dashed lines.
+ *
+ * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * index.
+ *
+ * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * however, from the perspective of a caller, its properties do not change over the course of its
+ * lifetime. The entire object is thread-safe.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
-public class ModeledShape
-/** Only for use within the ink library. Constructs a [ModeledShape] from native pointer. */
+public class PartitionedMesh
+/** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public constructor(
/**
* This is the raw pointer address of an `ink::ModeledShape` that has been heap allocated to be
- * owned solely by this JVM [ModeledShape] object. Although the `ink::ModeledShape` is owned
- * exclusively by this [ModeledShape] object, it may be a copy of another `ink::ModeledShape`,
- * where it has a copy of fairly lightweight metadata but shares ownership of the more
- * heavyweight `ink::Mesh` objects. This class is responsible for freeing the
+ * owned solely by this JVM [PartitionedMesh] object. Although the `ink::ModeledShape` is owned
+ * exclusively by this [PartitionedMesh] object, it may be a copy of another
+ * `ink::ModeledShape`, where it has a copy of fairly lightweight metadata but shares ownership
+ * of the more heavyweight `ink::Mesh` objects. This class is responsible for freeing the
* `ink::ModeledShape` through its [finalize] method.
*/
private var nativeAddress: Long
) {
/**
- * Only for use within the ink library. Returns the native pointer held by this [ModeledShape].
+ * Only for use within the ink library. Returns the native pointer held by this
+ * [PartitionedMesh].
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun getNativeAddress(): Long = nativeAddress
private val scratchIntArray by threadLocal { IntArray(2) }
/**
- * Only for tests - creates a new empty [ModeledShape]. Since a [ModeledShape] is immutable,
- * this serves no practical purpose outside of tests.
+ * Only for tests - creates a new empty [PartitionedMesh]. Since a [PartitionedMesh] is
+ * immutable, this serves no practical purpose outside of tests.
*/
@VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
/**
- * Returns the number of render groups in this shape. Each mesh in the [ModeledShape] belongs to
- * exactly one render group, and all meshes in the same render group will have the same
- * [MeshFormat] (and can thus be rendered together). The render groups are numbered in z-order
- * (the group with index zero should be rendered on bottom; the group with the highest index
- * should be rendered on top).
+ * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
+ * exactly one render group, which are numbered in z-order: the group with index zero should be
+ * rendered on bottom; the group with the highest index should be rendered on top.
*/
@IntRange(from = 0)
public val renderGroupCount: Int =
@@ -86,7 +90,10 @@
}
}
- /** The axis-aligned, rectangular region occupied by the [meshes] of this shape. */
+ /**
+ * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
+ * is empty.
+ */
public val bounds: Box? = run {
val envelope = BoxAccumulator()
for (meshes in meshesByGroup) {
@@ -128,8 +135,8 @@
}
/**
- * The number of vertices in the outline at index [outlineIndex], which can be up to (but not
- * including) [outlineCount].
+ * The number of vertices that are in the outline at index [outlineIndex], and within the render
+ * group at [groupIndex].
*/
@IntRange(from = 0)
public fun outlineVertexCount(
@@ -150,11 +157,11 @@
* [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
* will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
*/
- public fun fillOutlinePosition(
+ public fun populateOutlinePosition(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
@IntRange(from = 0) outlineVertexIndex: Int,
- outPosition: MutablePoint,
+ outPosition: MutableVec,
) {
val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
@@ -173,27 +180,24 @@
mesh.fillPosition(meshVertexIndex, outPosition)
}
- override fun toString(): String {
- val address = java.lang.Long.toHexString(nativeAddress)
- return "ModeledShape(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
- }
-
/**
- * Computes an approximate measure of what portion of this [ModeledShape] is covered by or
+ * Computes an approximate measure of what portion of this [PartitionedMesh] is covered by or
* overlaps with [triangle]. This is calculated by finding the sum of areas of the triangles
* that intersect the given [triangle], and dividing that by the sum of the areas of all
- * triangles in the [ModeledShape], all in the [ModeledShape]'s coordinate space. Triangles in
- * the [ModeledShape] that overlap each other (e.g. in the case of a stroke that loops back over
- * itself) are counted individually. Note that, if any triangles have negative area (due to
- * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
- * area will be used instead.
+ * triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
+ * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
+ * loops back over itself) are counted individually. Note that, if any triangles have negative
+ * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
+ * value of their area will be used instead.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [ModeledShape]'s coordinate space, which defaults to the [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
+ * [IDENTITY].
*/
@JvmOverloads
+ @FloatRange(from = 0.0, to = 1.0)
public fun coverage(
triangle: Triangle,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -206,30 +210,31 @@
triangleP1Y = triangle.p1.y,
triangleP2X = triangle.p2.x,
triangleP2Y = triangle.p2.y,
- triangleToThisTransformA = triangleToThis.a,
- triangleToThisTransformB = triangleToThis.b,
- triangleToThisTransformC = triangleToThis.c,
- triangleToThisTransformD = triangleToThis.d,
- triangleToThisTransformE = triangleToThis.e,
- triangleToThisTransformF = triangleToThis.f,
+ triangleToThisTransformA = triangleToThis.m00,
+ triangleToThisTransformB = triangleToThis.m10,
+ triangleToThisTransformC = triangleToThis.m20,
+ triangleToThisTransformD = triangleToThis.m01,
+ triangleToThisTransformE = triangleToThis.m11,
+ triangleToThisTransformF = triangleToThis.m21,
)
/**
- * Computes an approximate measure of what portion of this [ModeledShape] is covered by or
+ * Computes an approximate measure of what portion of this [PartitionedMesh] is covered by or
* overlaps with [box]. This is calculated by finding the sum of areas of the triangles that
* intersect the given [box], and dividing that by the sum of the areas of all triangles in the
- * [ModeledShape], all in the [ModeledShape]'s coordinate space. Triangles in the [ModeledShape]
- * that overlap each other (e.g. in the case of a stroke that loops back over itself) are
- * counted individually. Note that, if any triangles have negative area (due to winding, see
- * [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their area will be
- * used instead.
+ * [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
+ * [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
+ * itself) are counted individually. Note that, if any triangles have negative area (due to
+ * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
+ * area will be used instead.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [ModeledShape]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
*/
@JvmOverloads
+ @FloatRange(from = 0.0, to = 1.0)
public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
ModeledShapeNative.modeledShapeBoxCoverage(
nativeAddress = nativeAddress,
@@ -237,31 +242,32 @@
boxYMin = box.yMin,
boxXMax = box.xMax,
boxYMax = box.yMax,
- boxToThisTransformA = boxToThis.a,
- boxToThisTransformB = boxToThis.b,
- boxToThisTransformC = boxToThis.c,
- boxToThisTransformD = boxToThis.d,
- boxToThisTransformE = boxToThis.e,
- boxToThisTransformF = boxToThis.f,
+ boxToThisTransformA = boxToThis.m00,
+ boxToThisTransformB = boxToThis.m10,
+ boxToThisTransformC = boxToThis.m20,
+ boxToThisTransformD = boxToThis.m01,
+ boxToThisTransformE = boxToThis.m11,
+ boxToThisTransformF = boxToThis.m21,
)
/**
- * Computes an approximate measure of what portion of this [ModeledShape] is covered by or
+ * Computes an approximate measure of what portion of this [PartitionedMesh] is covered by or
* overlaps with [parallelogram]. This is calculated by finding the sum of areas of the
* triangles that intersect the given [parallelogram], and dividing that by the sum of the areas
- * of all triangles in the [ModeledShape], all in the [ModeledShape]'s coordinate space.
- * Triangles in the [ModeledShape] that overlap each other (e.g. in the case of a stroke that
+ * of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
+ * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
* area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
* value of their area will be used instead.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [parallelogramToThis] contains the transform that maps from
- * [parallelogram]'s coordinate space to this [ModeledShape]'s coordinate space, which defaults
- * to the [IDENTITY].
+ * [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
+ * defaults to the [IDENTITY].
*/
@JvmOverloads
+ @FloatRange(from = 0.0, to = 1.0)
public fun coverage(
parallelogram: Parallelogram,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -274,47 +280,49 @@
parallelogramHeight = parallelogram.height,
parallelogramAngleInRadian = parallelogram.rotation,
parallelogramShearFactor = parallelogram.shearFactor,
- parallelogramToThisTransformA = parallelogramToThis.a,
- parallelogramToThisTransformB = parallelogramToThis.b,
- parallelogramToThisTransformC = parallelogramToThis.c,
- parallelogramToThisTransformD = parallelogramToThis.d,
- parallelogramToThisTransformE = parallelogramToThis.e,
- parallelogramToThisTransformF = parallelogramToThis.f,
+ parallelogramToThisTransformA = parallelogramToThis.m00,
+ parallelogramToThisTransformB = parallelogramToThis.m10,
+ parallelogramToThisTransformC = parallelogramToThis.m20,
+ parallelogramToThisTransformD = parallelogramToThis.m01,
+ parallelogramToThisTransformE = parallelogramToThis.m11,
+ parallelogramToThisTransformF = parallelogramToThis.m21,
)
/**
- * Computes an approximate measure of what portion of this [ModeledShape] is covered by or
- * overlaps with the [other] [ModeledShape]. This is calculated by finding the sum of areas of
- * the triangles that intersect [other], and dividing that by the sum of the areas of all
- * triangles in the [ModeledShape], all in the [ModeledShape]'s coordinate space. Triangles in
- * the [ModeledShape] that overlap each other (e.g. in the case of a stroke that loops back over
- * itself) are counted individually. Note that, if any triangles have negative area (due to
- * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
- * area will be used instead.
+ * Computes an approximate measure of what portion of this [PartitionedMesh] is covered by or
+ * overlaps with the [other] [PartitionedMesh]. This is calculated by finding the sum of areas
+ * of the triangles that intersect [other], and dividing that by the sum of the areas of all
+ * triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
+ * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
+ * loops back over itself) are counted individually. Note that, if any triangles have negative
+ * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
+ * value of their area will be used instead.t
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [ModeledShape]'s coordinate space, which defaults to the [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
+ * [IDENTITY].
*/
@JvmOverloads
+ @FloatRange(from = 0.0, to = 1.0)
public fun coverage(
- other: ModeledShape,
+ other: PartitionedMesh,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
ModeledShapeNative.modeledShapeModeledShapeCoverage(
thisShapeNativeAddress = nativeAddress,
otherShapeNativeAddress = other.nativeAddress,
- otherShapeToThisTransformA = otherShapeToThis.a,
- otherShapeToThisTransformB = otherShapeToThis.b,
- otherShapeToThisTransformC = otherShapeToThis.c,
- otherShapeToThisTransformD = otherShapeToThis.d,
- otherShapeToThisTransformE = otherShapeToThis.e,
- otherShapeToThisTransformF = otherShapeToThis.f,
+ otherShapeToThisTransformA = otherShapeToThis.m00,
+ otherShapeToThisTransformB = otherShapeToThis.m10,
+ otherShapeToThisTransformC = otherShapeToThis.m20,
+ otherShapeToThisTransformD = otherShapeToThis.m01,
+ otherShapeToThisTransformE = otherShapeToThis.m11,
+ otherShapeToThisTransformF = otherShapeToThis.m21,
)
/**
- * Returns true if the approximate portion of the [ModeledShape] covered by [triangle] is
+ * Returns true if the approximate portion of the [PartitionedMesh] covered by [triangle] is
* greater than [coverageThreshold].
*
* This is equivalent to:
@@ -324,10 +332,11 @@
*
* but may be faster.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [ModeledShape]'s coordinate space, which defaults to the [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
+ * [IDENTITY].
*/
@JvmOverloads
public fun coverageIsGreaterThan(
@@ -344,16 +353,16 @@
triangleP2X = triangle.p2.x,
triangleP2Y = triangle.p2.y,
coverageThreshold = coverageThreshold,
- triangleToThisTransformA = triangleToThis.a,
- triangleToThisTransformB = triangleToThis.b,
- triangleToThisTransformC = triangleToThis.c,
- triangleToThisTransformD = triangleToThis.d,
- triangleToThisTransformE = triangleToThis.e,
- triangleToThisTransformF = triangleToThis.f,
+ triangleToThisTransformA = triangleToThis.m00,
+ triangleToThisTransformB = triangleToThis.m10,
+ triangleToThisTransformC = triangleToThis.m20,
+ triangleToThisTransformD = triangleToThis.m01,
+ triangleToThisTransformE = triangleToThis.m11,
+ triangleToThisTransformF = triangleToThis.m21,
)
/**
- * Returns true if the approximate portion of the [ModeledShape] covered by [box] is greater
+ * Returns true if the approximate portion of the [PartitionedMesh] covered by [box] is greater
* than [coverageThreshold].
*
* This is equivalent to:
@@ -363,10 +372,10 @@
*
* but may be faster.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [ModeledShape]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
*/
@JvmOverloads
public fun coverageIsGreaterThan(
@@ -381,17 +390,17 @@
boxXMax = box.xMax,
boxYMax = box.yMax,
coverageThreshold = coverageThreshold,
- boxToThisTransformA = boxToThis.a,
- boxToThisTransformB = boxToThis.b,
- boxToThisTransformC = boxToThis.c,
- boxToThisTransformD = boxToThis.d,
- boxToThisTransformE = boxToThis.e,
- boxToThisTransformF = boxToThis.f,
+ boxToThisTransformA = boxToThis.m00,
+ boxToThisTransformB = boxToThis.m10,
+ boxToThisTransformC = boxToThis.m20,
+ boxToThisTransformD = boxToThis.m01,
+ boxToThisTransformE = boxToThis.m11,
+ boxToThisTransformF = boxToThis.m21,
)
/**
- * Returns true if the approximate portion of the [ModeledShape] covered by [parallelogram] is
- * greater than [coverageThreshold].
+ * Returns true if the approximate portion of the [PartitionedMesh] covered by [parallelogram]
+ * is greater than [coverageThreshold].
*
* This is equivalent to:
* ```
@@ -400,11 +409,11 @@
*
* but may be faster.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [parallelogramToThis] contains the transform that maps from
- * [parallelogram]'s coordinate space to this [ModeledShape]'s coordinate space, which defaults
- * to the [IDENTITY].
+ * [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
+ * defaults to the [IDENTITY].
*/
@JvmOverloads
public fun coverageIsGreaterThan(
@@ -421,17 +430,17 @@
parallelogramAngleInRadian = parallelogram.rotation,
parallelogramShearFactor = parallelogram.shearFactor,
coverageThreshold = coverageThreshold,
- parallelogramToThisTransformA = parallelogramToThis.a,
- parallelogramToThisTransformB = parallelogramToThis.b,
- parallelogramToThisTransformC = parallelogramToThis.c,
- parallelogramToThisTransformD = parallelogramToThis.d,
- parallelogramToThisTransformE = parallelogramToThis.e,
- parallelogramToThisTransformF = parallelogramToThis.f,
+ parallelogramToThisTransformA = parallelogramToThis.m00,
+ parallelogramToThisTransformB = parallelogramToThis.m10,
+ parallelogramToThisTransformC = parallelogramToThis.m20,
+ parallelogramToThisTransformD = parallelogramToThis.m01,
+ parallelogramToThisTransformE = parallelogramToThis.m11,
+ parallelogramToThisTransformF = parallelogramToThis.m21,
)
/**
- * Returns true if the approximate portion of this [ModeledShape] covered by the [other]
- * [ModeledShape] is greater than [coverageThreshold].
+ * Returns true if the approximate portion of this [PartitionedMesh] covered by the [other]
+ * [PartitionedMesh] is greater than [coverageThreshold].
*
* This is equivalent to:
* ```
@@ -440,14 +449,15 @@
*
* but may be faster.
*
- * On an empty [ModeledShape], this will always return 0.
+ * On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [ModeledShape]'s coordinate space, which defaults to the [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
+ * [IDENTITY].
*/
@JvmOverloads
public fun coverageIsGreaterThan(
- other: ModeledShape,
+ other: PartitionedMesh,
coverageThreshold: Float,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
): Boolean =
@@ -455,12 +465,12 @@
thisShapeNativeAddress = nativeAddress,
otherShapeNativeAddress = other.nativeAddress,
coverageThreshold = coverageThreshold,
- otherShapeToThisTransformA = otherShapeToThis.a,
- otherShapeToThisTransformB = otherShapeToThis.b,
- otherShapeToThisTransformC = otherShapeToThis.c,
- otherShapeToThisTransformD = otherShapeToThis.d,
- otherShapeToThisTransformE = otherShapeToThis.e,
- otherShapeToThisTransformF = otherShapeToThis.f,
+ otherShapeToThisTransformA = otherShapeToThis.m00,
+ otherShapeToThisTransformB = otherShapeToThis.m10,
+ otherShapeToThisTransformC = otherShapeToThis.m20,
+ otherShapeToThisTransformD = otherShapeToThis.m01,
+ otherShapeToThisTransformE = otherShapeToThis.m11,
+ otherShapeToThisTransformF = otherShapeToThis.m21,
)
/**
@@ -472,9 +482,15 @@
ModeledShapeNative.initializeSpatialIndex(nativeAddress)
/** Returns true if this MutableEnvelope's spatial index has been initialized. */
- public fun isSpatialIndexInitialized(): Boolean =
+ @VisibleForTesting
+ internal fun isSpatialIndexInitialized(): Boolean =
ModeledShapeNative.isSpatialIndexInitialized(nativeAddress)
+ override fun toString(): String {
+ val address = java.lang.Long.toHexString(nativeAddress)
+ return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+ }
+
protected fun finalize() {
// NOMUTANTS--Not tested post garbage collection.
if (nativeAddress == 0L) return
@@ -488,8 +504,8 @@
/**
* Helper object to contain native JNI calls. The alternative to this is putting the methods in
- * [ModeledShape] itself (passes down an unused `jobject`, and doesn't work for native calls used by
- * constructors), or in [ModeledShape.Companion] (makes the `JNI_METHOD` naming less clear).
+ * [PartitionedMesh] itself (passes down an unused `jobject`, and doesn't work for native calls used
+ * by constructors), or in [PartitionedMesh.Companion] (makes the `JNI_METHOD` naming less clear).
*/
private object ModeledShapeNative {
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Point.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Point.kt
deleted file mode 100644
index ce53d31..0000000
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Point.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
-import kotlin.math.abs
-
-/**
- * Represents a location in 2-dimensional space. See [ImmutablePoint] and [MutablePoint] for
- * concrete classes implementing [Point].
- *
- * The [Point] interface is the read-only view of the underlying data which may or may not be
- * mutable. Use the following concrete classes depending on the application requirement:
- *
- * For the [ImmutablePoint], the underlying data like the [x] and [y] coordinates is set once during
- * construction and does not change afterwards. Use this class for a simple [Point] that is
- * inherently thread-safe because of its immutability. A different value of an immutable object can
- * only be obtained by allocating a new one, and allocations can be expensive due to the risk of
- * garbage collection.
- *
- * For the [MutablePoint], the underlying data might change (e.g. by writing the [x] property). Use
- * this class to hold transient data in a performance critical situation, such as the input or
- * render path --- allocate the underlying [MutablePoint] once, perform operations on it and
- * overwrite it with new data.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface Point {
- /** The x-coordinate of the [Point] */
- public val x: Float
-
- /** The y-coordinate of the [Point] */
- public val y: Float
-
- /** Fills the x and y values of [output] with the x and y coordinates of this [Point] */
- public fun getVec(output: MutableVec) {
- output.x = this.x
- output.y = this.y
- }
-
- /**
- * Compares this [Point] with [other], and returns true if the difference between [x] and
- * [other.x] is less than [tolerance], and likewise for [y].
- */
- public fun isAlmostEqual(
- other: Point,
- @FloatRange(from = 0.0) tolerance: Float = 0.0001f,
- ): Boolean = (abs(x - other.x) < tolerance) && (abs(y - other.y) < tolerance)
-
- public companion object {
- /**
- * Adds the x and y values of [lhs] to the x and y values of [rhs] and stores the result in
- * [output].
- */
- @JvmStatic
- public fun add(lhs: Point, rhs: Vec, output: MutablePoint) {
- output.x = lhs.x + rhs.x
- output.y = lhs.y + rhs.y
- }
-
- /**
- * Adds the x and y values of [lhs] to the x and y values of [rhs] and stores the result in
- * [output].
- */
- @JvmStatic
- public fun add(lhs: Vec, rhs: Point, output: MutablePoint) {
- output.x = lhs.x + rhs.x
- output.y = lhs.y + rhs.y
- }
-
- /**
- * Subtracts the x and y values of [rhs] from the x and y values of [lhs] and stores the
- * result in [output].
- */
- @JvmStatic
- public fun subtract(lhs: Point, rhs: Vec, output: MutablePoint) {
- output.x = lhs.x - rhs.x
- output.y = lhs.y - rhs.y
- }
-
- /**
- * Subtracts the x and y values of [rhs] from the x and y values of [lhs] and stores the
- * result in [output].
- */
- @JvmStatic
- public fun subtract(lhs: Point, rhs: Point, output: MutableVec) {
- output.x = lhs.x - rhs.x
- output.y = lhs.y - rhs.y
- }
-
- /**
- * Returns true if [first] and [second] have the same values for all properties of [Point].
- */
- internal fun areEquivalent(first: Point, second: Point): Boolean {
- return first.x == second.x && first.y == second.y
- }
-
- /** Returns a hash code for [point] using its [Point] properties. */
- internal fun hash(point: Point): Int = 31 * point.x.hashCode() + point.y.hashCode()
-
- /** Returns a string representation for [point] using its [Point] properties. */
- internal fun string(point: Point): String = "Point(x=${point.x}, y=${point.y})"
- }
-}
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Segment.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Segment.kt
index fa6d373..6f79eed 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Segment.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Segment.kt
@@ -21,53 +21,68 @@
import kotlin.math.hypot
/** Represents a directed line segment between two points. */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface Segment {
- public val start: Vec
- public val end: Vec
+public abstract class Segment internal constructor() {
+ public abstract val start: Vec
+ public abstract val end: Vec
/** The length of the [Segment]. */
- public val length: Float
- @FloatRange(from = 0.0) get() = hypot(start.x - end.x, start.y - end.y)
+ @FloatRange(from = 0.0)
+ public fun computeLength(): Float = hypot(start.x - end.x, start.y - end.y)
/**
* Returns an ImmutableVec with the displacement from start to end. This is equivalent to
- * subtract(end, start, output).
+ * `subtract(end, start, output)`.
+ *
+ * For performance-sensitive code, prefer to use [computeDisplacement] with a pre-allocated
+ * instance of [MutableVec].
*/
- public val vec: ImmutableVec
+ public fun computeDisplacement(): ImmutableVec = ImmutableVec(end.x - start.x, end.y - start.y)
/**
- * Populates [output] with the displacement from start to end. This is equivalent to
- * subtract(end, start, output).
+ * Populates [outVec] with the displacement from start to end. This is equivalent to
+ * `subtract(end, start, output)`. Returns [outVec].
*/
- public fun populateVec(output: MutableVec) {
- output.x = end.x - start.x
- output.y = end.y - start.y
+ public fun computeDisplacement(outVec: MutableVec): MutableVec {
+ outVec.x = end.x - start.x
+ outVec.y = end.y - start.y
+ return outVec
}
- /** Returns an [ImmutableVec] that lies halfway along the segment. */
- public val midpoint: ImmutableVec
+ /**
+ * Returns an [ImmutableVec] that lies halfway along the segment.
+ *
+ * For performance-sensitive code, prefer to use [computeMidpoint] with a pre-allocated instance
+ * of [MutableVec].
+ */
+ public fun computeMidpoint(): ImmutableVec =
+ ImmutableVec((start.x + end.x) / 2, (start.y + end.y) / 2)
- /** Populates [output] with the point halfway along the segment. */
- public fun populateMidpoint(output: MutableVec) {
- output.x = (start.x + end.x) / 2
- output.y = (start.y + end.y) / 2
+ /** Populates [outVec] with the point halfway along the segment. */
+ public fun computeMidpoint(outVec: MutableVec): MutableVec {
+ outVec.x = (start.x + end.x) / 2
+ outVec.y = (start.y + end.y) / 2
+ return outVec
}
- /** Returns the minimum bounding box containing the [Segment]. */
- public val boundingBox: ImmutableBox
- get() = run {
- // TODO(b/354236964): Optimize unnecessary allocations
- val (minX, maxX, minY, maxY) = getBoundingXYCoordinates(this)
- ImmutableBox.fromTwoPoints(ImmutablePoint(minX, minY), ImmutablePoint(maxX, maxY))
- }
-
- /** Populates [output] with the minimum bounding box containing the [Segment]. */
- public fun populateBoundingBox(output: MutableBox) {
+ /**
+ * Returns the minimum bounding box containing the [Segment].
+ *
+ * For performance-sensitive code, prefer to use [computeBoundingBox] with a pre-allocated
+ * instance of [MutableBox].
+ */
+ public fun computeBoundingBox(): ImmutableBox {
// TODO(b/354236964): Optimize unnecessary allocations
val (minX, maxX, minY, maxY) = getBoundingXYCoordinates(this)
- output.setXBounds(minX, maxX)
- output.setYBounds(minY, maxY)
+ return ImmutableBox.fromTwoPoints(ImmutableVec(minX, minY), ImmutableVec(maxX, maxY))
+ }
+
+ /** Populates [outBox] with the minimum bounding box containing the [Segment]. */
+ public fun computeBoundingBox(outBox: MutableBox): MutableBox {
+ // TODO(b/354236964): Optimize unnecessary allocations
+ val (minX, maxX, minY, maxY) = getBoundingXYCoordinates(this)
+ outBox.setXBounds(minX, maxX)
+ outBox.setYBounds(minY, maxY)
+ return outBox
}
/**
@@ -75,30 +90,34 @@
* the start point. You may also think of this as linearly interpolating from the start of the
* segment to the end. Values outside the interval [0, 1] will be extrapolated along the
* infinite line passing through this segment. This is the inverse of [project].
+ *
+ * For performance-sensitive code, prefer to use [computeLerpPoint] with a pre-allocated
+ * instance of [MutableVec].
*/
- public fun lerpPoint(ratio: Float): ImmutableVec =
+ public fun computeLerpPoint(ratio: Float): ImmutableVec =
ImmutableVec(
(1.0f - ratio) * start.x + ratio * end.x,
(1.0f - ratio) * start.y + ratio * end.y
)
/**
- * Fills [output] with the point on the segment at the given ratio of the segment's length,
+ * Fills [outVec] with the point on the segment at the given ratio of the segment's length,
* measured from the start point. You may also think of this as linearly interpolating from the
* start of the segment to the end. Values outside the interval [0, 1] will be extrapolated
* along the infinite line passing through this segment. This is the inverse of [project].
*/
- public fun populateLerpPoint(ratio: Float, output: MutableVec) {
- output.x = (1.0f - ratio) * start.x + ratio * end.x
- output.y = (1.0f - ratio) * start.y + ratio * end.y
+ public fun computeLerpPoint(ratio: Float, outVec: MutableVec): MutableVec {
+ outVec.x = (1.0f - ratio) * start.x + ratio * end.x
+ outVec.y = (1.0f - ratio) * start.y + ratio * end.y
+ return outVec
}
/**
* Returns the multiple of the segment's length at which the infinite extrapolation of this
- * segment is closest to [pointToProject]. This is the inverse of [populateLerpPoint]. If the
- * [length] of this segment is zero, then the projection is undefined and this will throw an
- * error. Note that the [length] may be zero even if [start] and [end] are not equal, if they
- * are sufficiently close that floating-point underflow occurs.
+ * segment is closest to [pointToProject]. This is the inverse of [computeLerpPoint]. If the
+ * [computeLength] of this segment is zero, then the projection is undefined and this will throw
+ * an error. Note that the [computeLength] may be zero even if [start] and [end] are not equal,
+ * if they are sufficiently close that floating-point underflow occurs.
*/
public fun project(pointToProject: Vec): Float {
// TODO(b/354236964): Optimize unnecessary allocations
@@ -108,31 +127,20 @@
// Sometimes start is not exactly equal to the end, but close enough that the
// magnitude-squared still is not positive due to floating-point
// loss-of-precision.
- val magnitudeSquared = vec.magnitudeSquared
+ val magnitudeSquared = computeDisplacement().computeMagnitudeSquared()
if (magnitudeSquared <= 0) {
throw IllegalArgumentException("Projecting onto a segment of zero length is undefined.")
}
val temp = MutableVec()
Vec.subtract(pointToProject, start, temp)
- return Vec.dotProduct(temp, vec) / magnitudeSquared
+ return Vec.dotProduct(temp, computeDisplacement()) / magnitudeSquared
}
/**
* Returns an immutable copy of this object. This will return itself if called on an immutable
* instance.
*/
- public fun asImmutable(): ImmutableSegment
-
- /**
- * Returns an [ImmutableSegment] with some or all of its values taken from `this`. For each
- * value, the returned [ImmutableSegment] will use the given value; if no value is given, it
- * will instead be set to the value on `this`. If `this` is an [ImmutableSegment], and the
- * result would be an identical [ImmutableSegment], then `this` is returned. This occurs when
- * either no values are given, or when all given values are structurally equal to the values in
- * `this`.
- */
- @JvmSynthetic
- public fun asImmutable(start: Vec = this.start, end: Vec = this.end): ImmutableSegment
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract fun asImmutable(): ImmutableSegment
/**
* Compares this [Segment] with [other], and returns true if both [start] points are considered
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Triangle.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Triangle.kt
index ee975b2..abf59f2 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Triangle.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Triangle.kt
@@ -22,47 +22,60 @@
import androidx.ink.nativeloader.NativeLoader
/**
- * A triangle defined by its three corners [p0], [p1] and [p2] in order. This is a read-only
- * interface that has mutable and immutable implementations. See [MutableTriangle] and
- * [ImmutableTriangle].
+ * A triangle defined by its three corners [p0], [p1] and [p2]. The order of these points matter - a
+ * triangle with [p0, p1, p2] is not the same as the permuted [p1, p0, p2], or even the rotated
+ * [p2, p0, p1].
+ *
+ * A [Triangle] may be degenerate, meaning it is constructed with its 3 points colinear. One way
+ * that a [Triangle] may be degenerate is if two or three of its points are at the same location
+ * (coincident).
+ *
+ * This is a read-only interface that has mutable and immutable implementations. See
+ * [MutableTriangle] and [ImmutableTriangle].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface Triangle {
+public abstract class Triangle internal constructor() {
- /** The three points that define the [Triangle]. */
- public val p0: Vec
- public val p1: Vec
- public val p2: Vec
+ /** One of the three points that define the [Triangle]. */
+ public abstract val p0: Vec
+
+ /** One of the three points that define the [Triangle]. */
+ public abstract val p1: Vec
+
+ /** One of the three points that define the [Triangle]. */
+ public abstract val p2: Vec
/**
- * The signed area of the Triangle. If the Triangle's points wind in a positive direction (as
- * defined by [Angle]), then the Triangle's area will be positive. Otherwise, it will be
+ * Return the signed area of the [Triangle]. If the [Triangle] is degenerate, meaning its 3
+ * points are all colinear, then the result will be zero. If its points wind in a positive
+ * direction (as defined by [Angle]), then the result will be positive. Otherwise, it will be
* negative.
*/
- public val signedArea: Float
- get() = run {
- // TODO(b/354236964): Optimize unnecessary allocations
- val p1MinusP0 = MutableVec()
- val p2MinusP1 = MutableVec()
- Vec.subtract(p1, p0, p1MinusP0)
- Vec.subtract(p2, p1, p2MinusP1)
- return 0.5f * Vec.determinant(p1MinusP0, p2MinusP1)
- }
+ public fun computeSignedArea(): Float {
+ // TODO(b/354236964): Optimize unnecessary allocations
+ val p1MinusP0 = MutableVec()
+ val p2MinusP1 = MutableVec()
+ Vec.subtract(p1, p0, p1MinusP0)
+ Vec.subtract(p2, p1, p2MinusP1)
+ return 0.5f * Vec.determinant(p1MinusP0, p2MinusP1)
+ }
/** Returns the minimum bounding box containing the [Triangle]. */
- public val boundingBox: ImmutableBox
- get() = run {
- // TODO(b/354236964): Optimize unnecessary allocations
- val (minX, maxX, minY, maxY) = getBoundingXYCoordinates(this)
- ImmutableBox.fromTwoPoints(ImmutablePoint(minX, minY), ImmutablePoint(maxX, maxY))
- }
-
- /** Populates [output] with the minimum bounding box containing the [Triangle]. */
- public fun populateBoundingBox(output: MutableBox) {
+ public fun computeBoundingBox(): ImmutableBox {
// TODO(b/354236964): Optimize unnecessary allocations
val (minX, maxX, minY, maxY) = getBoundingXYCoordinates(this)
- output.setXBounds(minX, maxX)
- output.setYBounds(minY, maxY)
+ return ImmutableBox.fromTwoPoints(ImmutableVec(minX, minY), ImmutableVec(maxX, maxY))
+ }
+
+ /**
+ * Populates [outBox] with the minimum bounding box containing the [Triangle] and returns
+ * [outBox].
+ */
+ public fun computeBoundingBox(outBox: MutableBox): MutableBox {
+ // TODO(b/354236964): Optimize unnecessary allocations
+ val (minX, maxX, minY, maxY) = getBoundingXYCoordinates(this)
+ outBox.setXBounds(minX, maxX)
+ outBox.setYBounds(minY, maxY)
+ return outBox
}
/**
@@ -85,7 +98,7 @@
* Returns the segment of the Triangle between the point at [index] and the point at [index] + 1
* modulo 3.
*/
- public fun edge(@IntRange(from = 0, to = 2) index: Int): ImmutableSegment {
+ public fun computeEdge(@IntRange(from = 0, to = 2) index: Int): ImmutableSegment {
val modIndex = index % 3
return when (modIndex) {
0 -> ImmutableSegment(p0, p1)
@@ -96,48 +109,41 @@
}
/**
- * Fills [output] with the segment of the Triangle between the point at [index] and the point at
- * [index] + 1 modulo 3.
+ * Fills [outSegment] with the segment of the Triangle between the point at [index] and the
+ * point at [index] + 1 modulo 3. Returns [outSegment].
*/
- public fun populateEdge(@IntRange(from = 0, to = 2) index: Int, output: MutableSegment) {
+ public fun computeEdge(
+ @IntRange(from = 0, to = 2) index: Int,
+ outSegment: MutableSegment,
+ ): MutableSegment {
val modIndex = index % 3
+ val start: Vec
+ val end: Vec
when (modIndex) {
0 -> {
- output.start(p0)
- output.end(p1)
+ start = p0
+ end = p1
}
1 -> {
- output.start(p1)
- output.end(p2)
+ start = p1
+ end = p2
}
2 -> {
- output.start(p2)
- output.end(p0)
+ start = p2
+ end = p0
}
else -> throw IllegalArgumentException("Invalid index: $index")
}
+ outSegment.start.populateFrom(start)
+ outSegment.end.populateFrom(end)
+ return outSegment
}
/**
- * Returns an immutable copy of [this] object. This will return itself if called on an immutable
+ * Returns an immutable copy of this object. This will return itself if called on an immutable
* instance.
*/
- public fun asImmutable(): ImmutableTriangle
-
- /**
- * Returns an [ImmutableTriangle] with some or all of its values taken from `this`. For each
- * value, the returned [ImmutableTriangle] will use the given value; if no value is given, it
- * will instead be set to the value on `this`. If `this` is an [ImmutableTriangle], and the
- * result would be an identical [ImmutableTriangle], then `this` is returned. This occurs when
- * either no values are given, or when all given values are structurally equal to the values in
- * `this`.
- */
- @JvmSynthetic
- public fun asImmutable(
- p0: Vec = this.p0,
- p1: Vec = this.p1,
- p2: Vec = this.p2
- ): ImmutableTriangle
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract fun asImmutable(): ImmutableTriangle
public fun isAlmostEqual(other: Triangle, @FloatRange(from = 0.0) tolerance: Float): Boolean =
p0.isAlmostEqual(other.p0, tolerance) &&
@@ -186,17 +192,14 @@
}
}
-/** Helper object to contain native JNI calls */
+/** Helper object to contain native JNI calls. */
private object TriangleNative {
init {
NativeLoader.load()
}
- /**
- * Helper method to construct a native C++ [Triangle] and [Point], check if the native
- * [Triangle] contains the native [Point], and return the result.
- */
+ /** Helper method to check if a native `ink::Triangle` contains the native `ink::Point`. */
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun nativeContains(
triangleP0X: Float,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Vec.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Vec.kt
index c02d918..f918578 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Vec.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Vec.kt
@@ -20,88 +20,105 @@
import androidx.annotation.RestrictTo
import kotlin.math.abs
import kotlin.math.atan2
+import kotlin.math.hypot
/**
- * A 2-dimensional vector, representing an offset in space. See [MutableVec] for a mutable, and
- * [ImmutableVec] for an immutable implementation of [Vec]. See [Point] (and its concrete
- * implementations [ImmutablePoint] and [MutablePoint]) for a location in space.
+ * A two-dimensional vector, i.e. an (x, y) coordinate pair. It can be used to represent either:
+ * 1) A two-dimensional offset, i.e. the difference between two points
+ * 2) A point in space, i.e. treating the vector as an offset from the origin
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public interface Vec {
+public abstract class Vec internal constructor() {
/** The [Vec]'s offset in the x-direction */
- public val x: Float
+ public abstract val x: Float
/** The [Vec]'s offset in the y-direction */
- public val y: Float
+ public abstract val y: Float
/** The length of the [Vec]. */
- public val magnitude: Float
+ @FloatRange(from = 0.0) public fun computeMagnitude(): Float = hypot(x, y)
/** The squared length of the [Vec]. */
- public val magnitudeSquared: Float
+ @FloatRange(from = 0.0) public fun computeMagnitudeSquared(): Float = x * x + y * y
/**
* The direction of the vec, represented as the angle between the positive x-axis and this vec.
- * The [direction] value will lie in the interval [-π, π], and will have the same sign as the
- * vec's y-component.
+ * If either component of the vector is NaN, this returns a NaN angle; otherwise, the returned
+ * value will lie in the interval [-π, π], and will have the same sign as the vector's
+ * y-component.
+ *
+ * Following the behavior of `atan2`, this will return either ±0 or ±π for the zero vector,
+ * depending on the signs of the zeros.
*/
- public val direction: Float
- @FloatRange(from = -Math.PI, to = Math.PI) @AngleRadiansFloat get() = atan2(y, x)
-
- /** Returns a vector with the same direction as this one, but with a magnitude of 1. */
- public val unitVec: ImmutableVec
- get() = VecNative.unitVec(this.x, this.y, ImmutableVec::class.java)
+ @FloatRange(from = -Math.PI, to = Math.PI)
+ @AngleRadiansFloat
+ public fun computeDirection(): Float = atan2(y, x)
/**
- * Modifies [output] into a vector with the same direction as this one, but with a magnitude
- * of 1.
+ * Returns a newly allocated vector with the same direction as this one, but with a magnitude of
+ * `1`. This is equivalent to (but faster than) calling [ImmutableVec.fromDirectionAndMagnitude]
+ * with [computeDirection] and `1`.
+ *
+ * In keeping with the above equivalence, this will return <±1, ±0> for the zero vector,
+ * depending on the signs of the zeros.
+ *
+ * For performance-sensitive code, use [computeUnitVec] with a pre-allocated instance of
+ * [MutableVec].
*/
- public fun populateUnitVec(output: MutableVec) {
- VecNative.populateUnitVec(x, y, output)
+ public fun computeUnitVec(): ImmutableVec =
+ VecNative.unitVec(this.x, this.y, ImmutableVec::class.java)
+
+ /**
+ * Modifies [outVec] into a vector with the same direction as this one, but with a magnitude of
+ * `1`. Returns [outVec]. This is equivalent to (but faster than) calling
+ * [MutableVec.fromDirectionAndMagnitude] with [computeDirection] and `1`.
+ *
+ * In keeping with the above equivalence, this will return <±1, ±0> for the zero vector,
+ * depending on the signs of the zeros.
+ */
+ public fun computeUnitVec(outVec: MutableVec): MutableVec {
+ VecNative.populateUnitVec(x, y, outVec)
+ return outVec
}
/**
- * Returns a vector with the same magnitude as this one, but rotated by (positive) 90 degrees.
+ * Returns a newly allocated vector with the same magnitude as this one, but rotated by
+ * (positive) 90 degrees. For performance-sensitive code, use [computeOrthogonal] with a
+ * pre-allocated instance of [MutableVec].
*/
- public val orthogonal: ImmutableVec
- get() = ImmutableVec(-y, x)
+ public fun computeOrthogonal(): ImmutableVec = ImmutableVec(-y, x)
/**
- * Modifies [output] into a vector with the same magnitude as this one, but rotated by
- * (positive) 90 degrees.
+ * Modifies [outVec] into a vector with the same magnitude as this one, but rotated by
+ * (positive) 90 degrees. Returns [outVec].
*/
- public fun populateOrthogonal(output: MutableVec) {
- output.x = -y
- output.y = x
+ public fun computeOrthogonal(outVec: MutableVec): MutableVec {
+ outVec.x = -y
+ outVec.y = x
+ return outVec
}
- /** Returns a vector with the same magnitude, but pointing in the opposite direction. */
- public val negation: ImmutableVec
- get() = ImmutableVec(-x, -y)
+ /**
+ * Returns a newly allocated vector with the same magnitude, but pointing in the opposite
+ * direction. For performance-sensitive code, use [computeNegation] with a pre-allocated
+ * instance of [MutableVec].
+ */
+ public fun computeNegation(): ImmutableVec = ImmutableVec(-x, -y)
/**
- * Modifies [output] into a vector with the same magnitude, but pointing in the opposite
- * direction.
+ * Modifies [outVec] into a vector with the same magnitude, but pointing in the opposite
+ * direction. Returns [outVec].
*/
- public fun populateNegation(output: MutableVec) {
- output.x = -x
- output.y = -y
+ public fun computeNegation(outVec: MutableVec): MutableVec {
+ outVec.x = -x
+ outVec.y = -y
+ return outVec
}
/**
* Returns an immutable copy of this object. This will return itself if called on an immutable
* instance.
*/
- public val asImmutable: ImmutableVec
-
- /**
- * Returns an [ImmutableVec] with some or all of its values taken from `this`. For each value,
- * the returned [ImmutableVec] will use the given value; if no value is given, it will instead
- * be set to the value on `this`. If `this` is an [ImmutableVec], and the result would be an
- * identical [ImmutableVec], then `this` is returned. This occurs when either no values are
- * given, or when all given values are structurally equal to the values in `this`.
- */
- @JvmSynthetic public fun asImmutable(x: Float = this.x, y: Float = this.y): ImmutableVec
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract fun asImmutable(): ImmutableVec
/**
* Returns true if the angle formed by `this` and [other] is within [angleTolerance] of 0
@@ -131,6 +148,7 @@
* Compares this [Vec] with [other], and returns true if the difference between [x] and
* [other.x] is less than [tolerance], and likewise for [y].
*/
+ @JvmOverloads
public fun isAlmostEqual(
other: Vec,
@FloatRange(from = 0.0) tolerance: Float = 0.0001f,
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxAccumulatorTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxAccumulatorTest.kt
index 6751546..fd7ee5b 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxAccumulatorTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxAccumulatorTest.kt
@@ -77,7 +77,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(0.1F, 2F), ImmutablePoint(3F, 4F))
+ .populateFromTwoPoints(ImmutableVec(0.1F, 2F), ImmutableVec(3F, 4F))
)
)
}
@@ -93,7 +93,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 1.2F), ImmutablePoint(3F, 4F))
+ .populateFromTwoPoints(ImmutableVec(1F, 1.2F), ImmutableVec(3F, 4F))
)
)
}
@@ -109,7 +109,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3.1F, 4F))
+ .populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3.1F, 4F))
)
)
}
@@ -125,7 +125,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4.2F))
+ .populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4.2F))
)
)
}
@@ -143,7 +143,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 1F), ImmutablePoint(4F, 4F))
+ .populateFromTwoPoints(ImmutableVec(1F, 1F), ImmutableVec(4F, 4F))
)
)
}
@@ -172,7 +172,7 @@
envelope.add(
BoxAccumulator(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1.1F, 2.1F), ImmutablePoint(2.9F, 3.9F))
+ ImmutableBox.fromTwoPoints(ImmutableVec(1.1F, 2.1F), ImmutableVec(2.9F, 3.9F))
)
)
@@ -185,7 +185,7 @@
envelope.add(
BoxAccumulator(
- ImmutableBox.fromTwoPoints(ImmutablePoint(0.9F, 1.9F), ImmutablePoint(3.1F, 4.1F))
+ ImmutableBox.fromTwoPoints(ImmutableVec(0.9F, 1.9F), ImmutableVec(3.1F, 4.1F))
)
)
@@ -194,9 +194,9 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(
- ImmutablePoint(0.9F, 1.9F),
- ImmutablePoint(3.1F, 4.1F)
+ .populateFromTwoPoints(
+ ImmutableVec(0.9F, 1.9F),
+ ImmutableVec(3.1F, 4.1F)
)
)
)
@@ -206,12 +206,10 @@
fun addEnvelope_whenNewAndCurrentOverlap_shouldUpdateToUnion() {
val envelope =
BoxAccumulator()
- .add(MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 8F), ImmutablePoint(4F, 9F)))
+ .add(MutableBox().populateFromTwoPoints(ImmutableVec(1F, 8F), ImmutableVec(4F, 9F)))
envelope.add(
- BoxAccumulator(
- ImmutableBox.fromTwoPoints(ImmutablePoint(2F, 7F), ImmutablePoint(3F, 10F))
- )
+ BoxAccumulator(ImmutableBox.fromTwoPoints(ImmutableVec(2F, 7F), ImmutableVec(3F, 10F)))
)
assertThat(envelope)
@@ -219,7 +217,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 7F), ImmutablePoint(4F, 10F))
+ .populateFromTwoPoints(ImmutableVec(1F, 7F), ImmutableVec(4F, 10F))
)
)
}
@@ -229,9 +227,7 @@
val envelope = BoxAccumulator().add(rect1234)
envelope.add(
- BoxAccumulator(
- ImmutableBox.fromTwoPoints(ImmutablePoint(2F, 0F), ImmutablePoint(5F, 1F))
- )
+ BoxAccumulator(ImmutableBox.fromTwoPoints(ImmutableVec(2F, 0F), ImmutableVec(5F, 1F)))
)
assertThat(envelope)
@@ -239,7 +235,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 0F), ImmutablePoint(5F, 4F))
+ .populateFromTwoPoints(ImmutableVec(1F, 0F), ImmutableVec(5F, 4F))
)
)
}
@@ -251,7 +247,7 @@
@Test
fun rect_withBounds_returnsBox() {
- val addition = rect1234.newMutable()
+ val addition = MutableBox().populateFrom(rect1234)
val envelope = BoxAccumulator().add(addition)
val rect = envelope.box
@@ -285,7 +281,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 10F), ImmutablePoint(3F, 15F))
+ .populateFromTwoPoints(ImmutableVec(1F, 10F), ImmutableVec(3F, 15F))
)
)
}
@@ -296,7 +292,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(20F, 25F))
)
val segment = ImmutableSegment(start = ImmutableVec(1f, 10f), end = ImmutableVec(30f, 150f))
@@ -308,7 +304,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 10F), ImmutablePoint(30F, 150F))
+ .populateFromTwoPoints(ImmutableVec(1F, 10F), ImmutableVec(30F, 150F))
)
)
}
@@ -331,7 +327,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 5F), ImmutablePoint(10F, 20F))
+ .populateFromTwoPoints(ImmutableVec(1F, 5F), ImmutableVec(10F, 20F))
)
)
}
@@ -342,7 +338,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(20F, 25F))
)
val triangle =
ImmutableTriangle(
@@ -359,7 +355,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 5F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(1F, 5F), ImmutableVec(20F, 25F))
)
)
}
@@ -367,7 +363,7 @@
@Test
fun add_rectToEmptyEnvelope_updatesEnvelope() {
val envelope = BoxAccumulator()
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 10f), ImmutablePoint(-3f, -20f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(1f, 10f), ImmutableVec(-3f, -20f))
envelope.add(rect)
@@ -377,7 +373,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(-3F, -20F), ImmutablePoint(1F, 10F))
+ .populateFromTwoPoints(ImmutableVec(-3F, -20F), ImmutableVec(1F, 10F))
)
)
}
@@ -388,10 +384,9 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(20F, 25F))
)
- val rect =
- ImmutableBox.fromTwoPoints(ImmutablePoint(100f, 200f), ImmutablePoint(300f, 400f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
envelope.add(rect)
@@ -401,7 +396,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(300F, 400F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(300F, 400F))
)
)
}
@@ -411,7 +406,7 @@
val envelope = BoxAccumulator()
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(10f, 20f),
+ center = ImmutableVec(10f, 20f),
width = 4f,
height = 6f,
rotation = Angle.ZERO,
@@ -426,7 +421,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(8F, 17F), ImmutablePoint(12F, 23F))
+ .populateFromTwoPoints(ImmutableVec(8F, 17F), ImmutableVec(12F, 23F))
)
)
}
@@ -437,11 +432,11 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(20F, 25F))
)
val parallelogram =
ImmutableParallelogram.fromCenterAndDimensions(
- center = ImmutablePoint(100f, 200f),
+ center = ImmutableVec(100f, 200f),
width = 500f,
height = 1000f,
)
@@ -454,9 +449,9 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(
- ImmutablePoint(-150F, -300F),
- ImmutablePoint(350F, 700F)
+ .populateFromTwoPoints(
+ ImmutableVec(-150F, -300F),
+ ImmutableVec(350F, 700F)
)
)
)
@@ -479,13 +474,13 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(20F, 25F))
)
val secondEnvelope =
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(-150F, -300F), ImmutablePoint(350F, 700F))
+ .populateFromTwoPoints(ImmutableVec(-150F, -300F), ImmutableVec(350F, 700F))
)
envelope.add(secondEnvelope)
@@ -496,9 +491,9 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(
- ImmutablePoint(-150F, -300F),
- ImmutablePoint(350F, 700F)
+ .populateFromTwoPoints(
+ ImmutableVec(-150F, -300F),
+ ImmutableVec(350F, 700F)
)
)
)
@@ -517,7 +512,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 10F), ImmutablePoint(1F, 10F))
+ .populateFromTwoPoints(ImmutableVec(1F, 10F), ImmutableVec(1F, 10F))
)
)
}
@@ -528,7 +523,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(10F, 10F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(10F, 10F), ImmutableVec(20F, 25F))
)
val point = MutableVec(1f, 5f)
@@ -540,7 +535,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(1F, 5F), ImmutablePoint(20F, 25F))
+ .populateFromTwoPoints(ImmutableVec(1F, 5F), ImmutableVec(20F, 25F))
)
)
}
@@ -548,7 +543,7 @@
@Test
fun add_emptyMeshToEmptyEnvelope_doesNotUpdateEnvelope() {
val envelope = BoxAccumulator()
- val mesh = ModeledShape()
+ val mesh = PartitionedMesh()
envelope.add(mesh)
@@ -580,8 +575,8 @@
BoxAccumulator()
.add(
ImmutableBox.fromTwoPoints(
- ImmutablePoint(1.00001F, 2.00001F),
- ImmutablePoint(2.99999F, 3.99999F),
+ ImmutableVec(1.00001F, 2.00001F),
+ ImmutableVec(2.99999F, 3.99999F),
)
)
@@ -635,7 +630,7 @@
BoxAccumulator()
.add(
MutableBox()
- .fillFromTwoPoints(ImmutablePoint(2F, 2F), ImmutablePoint(3F, 4F))
+ .populateFromTwoPoints(ImmutableVec(2F, 2F), ImmutableVec(3F, 4F))
)
)
}
@@ -669,8 +664,6 @@
assertThat(string).contains("MutableBox")
}
- private val rect1234 =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- private val rect5678 =
- ImmutableBox.fromTwoPoints(ImmutablePoint(5F, 6F), ImmutablePoint(7F, 8F))
+ private val rect1234 = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
+ private val rect5678 = ImmutableBox.fromTwoPoints(ImmutableVec(5F, 6F), ImmutableVec(7F, 8F))
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxTest.kt
index a6ba94b..74fc31d 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/BoxTest.kt
@@ -26,12 +26,12 @@
@Test
fun isAlmostEqual_withToleranceGiven_returnsCorrectValue() {
- val box = ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 2f), ImmutablePoint(3f, 4f))
+ val box = ImmutableBox.fromTwoPoints(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f))
assertThat(box.isAlmostEqual(box, tolerance = 0.00000001f)).isTrue()
assertThat(
box.isAlmostEqual(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 2f), ImmutablePoint(3f, 4f)),
+ ImmutableBox.fromTwoPoints(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f)),
tolerance = 0.00000001f,
)
)
@@ -39,8 +39,8 @@
assertThat(
box.isAlmostEqual(
ImmutableBox.fromTwoPoints(
- ImmutablePoint(1.00001f, 1.99999f),
- ImmutablePoint(3f, 4f)
+ ImmutableVec(1.00001f, 1.99999f),
+ ImmutableVec(3f, 4f)
),
tolerance = 0.000001f,
)
@@ -49,8 +49,8 @@
assertThat(
box.isAlmostEqual(
ImmutableBox.fromTwoPoints(
- ImmutablePoint(1f, 2f),
- ImmutablePoint(3.00001f, 3.99999f)
+ ImmutableVec(1f, 2f),
+ ImmutableVec(3.00001f, 3.99999f)
),
tolerance = 0.000001f,
)
@@ -58,14 +58,14 @@
.isFalse()
assertThat(
box.isAlmostEqual(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 1.99f), ImmutablePoint(3f, 4f)),
+ ImmutableBox.fromTwoPoints(ImmutableVec(1f, 1.99f), ImmutableVec(3f, 4f)),
tolerance = 0.02f,
)
)
.isTrue()
assertThat(
box.isAlmostEqual(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 2f), ImmutablePoint(3.01f, 4f)),
+ ImmutableBox.fromTwoPoints(ImmutableVec(1f, 2f), ImmutableVec(3.01f, 4f)),
tolerance = 0.02f,
)
)
@@ -74,11 +74,11 @@
@Test
fun isAlmostEqual_whenSameInterface_returnsTrue() {
- val box = MutableBox().fillFromTwoPoints(ImmutablePoint(1f, 2f), ImmutablePoint(3f, 4f))
+ val box = MutableBox().populateFromTwoPoints(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f))
val other =
ImmutableBox.fromTwoPoints(
- ImmutablePoint(0.99999f, 2.00001f),
- ImmutablePoint(3.00001f, 3.99999f),
+ ImmutableVec(0.99999f, 2.00001f),
+ ImmutableVec(3.00001f, 3.99999f)
)
assertThat(box.isAlmostEqual(other, tolerance = 0.0001f)).isTrue()
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableAffineTransformTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableAffineTransformTest.kt
index e498235..f41c54f 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableAffineTransformTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableAffineTransformTest.kt
@@ -95,8 +95,8 @@
@Test
fun equals_whenDifferentF_returnsFalse() {
- val affineTransform = ImmutableAffineTransform(A, B, C, D, E, 6f)
- val otherTransform = ImmutableAffineTransform(A, B, C, D, E, 60f)
+ val affineTransform = ImmutableAffineTransform(A, B, C, D, E, F)
+ val otherTransform = ImmutableAffineTransform(A, B, C, D, E, DIFFERENT_F)
assertThat(affineTransform).isNotEqualTo(otherTransform)
}
@@ -104,8 +104,7 @@
@Test
fun translate_returnsCorrectImmutableAffineTransform() {
val translate = ImmutableAffineTransform.translate(ImmutableVec(4.12f, -19.9f))
- val expected =
- ImmutableAffineTransform(a = 1f, b = 0f, c = 4.12f, d = 0f, e = 1f, f = -19.9f)
+ val expected = ImmutableAffineTransform(1f, 0f, 4.12f, 0f, 1f, -19.9f)
assertThat(translate).isEqualTo(expected)
}
@@ -113,7 +112,7 @@
@Test
fun scale_callsJniAndReturnsCorrectValue() {
val scale = ImmutableAffineTransform.scale(2.9f)
- val expected = ImmutableAffineTransform(a = 2.9f, b = 0f, c = 0f, d = 0f, e = 2.9f, f = 0f)
+ val expected = ImmutableAffineTransform(2.9f, 0f, 0f, 0f, 2.9f, 0f)
assertThat(scale).isEqualTo(expected)
}
@@ -121,8 +120,7 @@
@Test
fun scale_withTwoArguments_callsJniAndReturnsCorrectValue() {
val scale = ImmutableAffineTransform.scale(-7.13f, 19.71f)
- val expected =
- ImmutableAffineTransform(a = -7.13f, b = 0f, c = 0f, d = 0f, e = 19.71f, f = 0f)
+ val expected = ImmutableAffineTransform(-7.13f, 0f, 0f, 0f, 19.71f, 0f)
assertThat(scale).isEqualTo(expected)
}
@@ -130,7 +128,7 @@
@Test
fun scaleX_callsJniAndReturnsCorrectValue() {
val scale = ImmutableAffineTransform.scaleX(100.54f)
- val expected = ImmutableAffineTransform(a = 100.54f, b = 0f, c = 0f, d = 0f, e = 1f, f = 0f)
+ val expected = ImmutableAffineTransform(100.54f, 0f, 0f, 0f, 1f, 0f)
assertThat(scale).isEqualTo(expected)
}
@@ -138,7 +136,7 @@
@Test
fun scaleY_callsJniAndReturnsCorrectValue() {
val scale = ImmutableAffineTransform.scaleY(12f)
- val expected = ImmutableAffineTransform(a = 1f, b = 0f, c = 0f, d = 0f, e = 12f, f = 0f)
+ val expected = ImmutableAffineTransform(1f, 0f, 0f, 0f, 12f, 0f)
assertThat(scale).isEqualTo(expected)
}
@@ -148,32 +146,32 @@
val identityTransform = AffineTransform.IDENTITY
val identityOutput = MutableAffineTransform()
- identityTransform.populateInverse(identityOutput)
+ identityTransform.computeInverse(identityOutput)
assertThat(identityOutput).isEqualTo(AffineTransform.IDENTITY)
val scaleTransform = ImmutableAffineTransform.scale(4f, 10f)
val scaleOutput = MutableAffineTransform()
- scaleTransform.populateInverse(scaleOutput)
+ scaleTransform.computeInverse(scaleOutput)
assertThat(scaleOutput).isEqualTo(ImmutableAffineTransform.scale(0.25f, 0.1f))
val translateTransform = ImmutableAffineTransform.translate(ImmutableVec(5f, 10f))
val translateOutput = MutableAffineTransform()
- translateTransform.populateInverse(translateOutput)
+ translateTransform.computeInverse(translateOutput)
assertThat(translateOutput)
.isEqualTo(ImmutableAffineTransform.translate(ImmutableVec(-5f, -10f)))
val shearXTransform = ImmutableAffineTransform(1f, 5F, 0f, 0f, 1f, 0f)
val shearXOutput = MutableAffineTransform()
- shearXTransform.populateInverse(shearXOutput)
+ shearXTransform.computeInverse(shearXOutput)
assertThat(shearXOutput).isEqualTo(ImmutableAffineTransform(1f, -5f, 0f, 0f, 1f, 0f))
val shearYTransform = ImmutableAffineTransform(1f, 0F, 0f, 5f, 1f, 0f)
val shearYOutput = MutableAffineTransform()
- shearYTransform.populateInverse(shearYOutput)
+ shearYTransform.computeInverse(shearYOutput)
assertThat(shearYOutput).isEqualTo(ImmutableAffineTransform(1f, 0f, 0f, -5f, 1f, 0f))
}
@@ -182,7 +180,7 @@
// This is equivalent to ImmutableAffineTransform.scale(4f, 10f)
val testTransform = MutableAffineTransform(4f, 0f, 0f, 0f, 10f, 0f)
- testTransform.populateInverse(testTransform)
+ testTransform.computeInverse(testTransform)
assertThat(testTransform).isEqualTo(ImmutableAffineTransform.scale(0.25f, 0.1f))
}
@@ -191,14 +189,14 @@
val zeroesTransform = MutableAffineTransform(0f, 0f, 0f, 0f, 0f, 0f)
assertFailsWith<IllegalArgumentException> {
- zeroesTransform.populateInverse(zeroesTransform)
+ zeroesTransform.computeInverse(zeroesTransform)
}
// Determinant = a * e - b * d = 2 * 16 - 4 * 8 = 0
val determinantOfZeroTransform = MutableAffineTransform(2f, 4f, 0f, 8f, 16f, 0f)
assertFailsWith<IllegalArgumentException> {
- determinantOfZeroTransform.populateInverse(determinantOfZeroTransform)
+ determinantOfZeroTransform.computeInverse(determinantOfZeroTransform)
}
}
@@ -254,7 +252,7 @@
val identitySegment = MutableSegment()
identityTransform.applyTransform(testSegment, identitySegment)
assertThat(identitySegment)
- .isEqualTo(MutableSegment(ImmutableVec(4F, 6F), ImmutableVec(40F, 60F)))
+ .isEqualTo(MutableSegment(MutableVec(4F, 6F), MutableVec(40F, 60F)))
val translateTransform = ImmutableAffineTransform.translate(ImmutableVec(3F, -20F))
val translateSegment = MutableSegment()
@@ -370,26 +368,26 @@
@Test
fun applyTransform_whenAppliedToABox_correctlyModifiesParallelogram() {
- val testBox = ImmutableBox.fromCenterAndDimensions(ImmutablePoint(4f, 1f), 6f, 8f)
+ val testBox = ImmutableBox.fromCenterAndDimensions(ImmutableVec(4f, 1f), 6f, 8f)
val identityTransform = AffineTransform.IDENTITY
val identityParallelogram = MutableParallelogram()
identityTransform.applyTransform(testBox, identityParallelogram)
assertThat(identityParallelogram)
- .isEqualTo(MutableParallelogram.fromCenterAndDimensions(ImmutablePoint(4f, 1f), 6f, 8f))
+ .isEqualTo(MutableParallelogram.fromCenterAndDimensions(MutableVec(4f, 1f), 6f, 8f))
val translateTransform = ImmutableAffineTransform.translate(ImmutableVec(1F, 3F))
val translateParallelogram = MutableParallelogram()
translateTransform.applyTransform(testBox, translateParallelogram)
assertThat(translateParallelogram)
- .isEqualTo(MutableParallelogram.fromCenterAndDimensions(ImmutablePoint(5f, 4f), 6f, 8f))
+ .isEqualTo(MutableParallelogram.fromCenterAndDimensions(MutableVec(5f, 4f), 6f, 8f))
val scaleBy2ValuesTransform = ImmutableAffineTransform.scale(2.5F, -.5F)
val scaleBy2ValuesParallelogram = MutableParallelogram()
scaleBy2ValuesTransform.applyTransform(testBox, scaleBy2ValuesParallelogram)
assertThat(scaleBy2ValuesParallelogram)
.isEqualTo(
- MutableParallelogram.fromCenterAndDimensions(ImmutablePoint(10f, -0.5f), 15f, -4f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(10f, -0.5f), 15f, -4f)
)
val scaleBy1ValueTransform = ImmutableAffineTransform.scale(2.5F)
@@ -397,24 +395,20 @@
scaleBy1ValueTransform.applyTransform(testBox, scaleBy1ValueParallelogram)
assertThat(scaleBy1ValueParallelogram)
.isEqualTo(
- MutableParallelogram.fromCenterAndDimensions(ImmutablePoint(10f, 2.5f), 15f, 20f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(10f, 2.5f), 15f, 20f)
)
val scaleXTransform = ImmutableAffineTransform.scaleX(2.5F)
val scaleXParallelogram = MutableParallelogram()
scaleXTransform.applyTransform(testBox, scaleXParallelogram)
assertThat(scaleXParallelogram)
- .isEqualTo(
- MutableParallelogram.fromCenterAndDimensions(ImmutablePoint(10f, 1f), 15f, 8f)
- )
+ .isEqualTo(MutableParallelogram.fromCenterAndDimensions(MutableVec(10f, 1f), 15f, 8f))
val scaleYTransform = ImmutableAffineTransform.scaleY(2.5F)
val scaleYParallelogram = MutableParallelogram()
scaleYTransform.applyTransform(testBox, scaleYParallelogram)
assertThat(scaleYParallelogram)
- .isEqualTo(
- MutableParallelogram.fromCenterAndDimensions(ImmutablePoint(4f, 2.5f), 6f, 20f)
- )
+ .isEqualTo(MutableParallelogram.fromCenterAndDimensions(MutableVec(4f, 2.5f), 6f, 20f))
val shearXTransform = ImmutableAffineTransform(1f, 2.5F, 0f, 0f, 1f, 0f)
val shearXParallelogram = MutableParallelogram()
@@ -422,7 +416,7 @@
assertThat(shearXParallelogram)
.isEqualTo(
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(6.5f, 1f),
+ MutableVec(6.5f, 1f),
6f,
8f,
0.0f,
@@ -439,7 +433,7 @@
Parallelogram.areNear(
rotateParallelogram,
MutableParallelogram.fromCenterDimensionsAndRotation(
- ImmutablePoint(-4f, -1f),
+ MutableVec(-4f, -1f),
6f,
8f,
Angle.HALF_TURN_RADIANS,
@@ -453,7 +447,7 @@
fun applyTransform_whenAppliedToAParallelogram_correctlyModifiesParallelogram() {
val testParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(4f, 1f),
+ ImmutableVec(4f, 1f),
6f,
8f,
Angle.QUARTER_TURN_RADIANS,
@@ -466,7 +460,7 @@
assertThat(identityParallelogram)
.isEqualTo(
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(4f, 1f),
+ MutableVec(4f, 1f),
6f,
8f,
Angle.QUARTER_TURN_RADIANS,
@@ -480,7 +474,7 @@
assertThat(translateParallelogram)
.isEqualTo(
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(5f, 4f),
+ MutableVec(5f, 4f),
6f,
8f,
Angle.QUARTER_TURN_RADIANS,
@@ -495,7 +489,7 @@
Parallelogram.areNear(
scaleBy2ValuesParallelogram,
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(10f, -0.5f),
+ MutableVec(10f, -0.5f),
3f,
-20f,
Angle.QUARTER_TURN_RADIANS + Angle.HALF_TURN_RADIANS,
@@ -513,7 +507,7 @@
Parallelogram.areNear(
scaleBy1ValueParallelogram,
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(10f, 2.5f),
+ MutableVec(10f, 2.5f),
15f,
20f,
Angle.QUARTER_TURN_RADIANS,
@@ -531,7 +525,7 @@
Parallelogram.areNear(
scaleXParallelogram,
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(10f, 1f),
+ MutableVec(10f, 1f),
6f,
20f,
Angle.QUARTER_TURN_RADIANS,
@@ -549,7 +543,7 @@
Parallelogram.areNear(
scaleYParallelogram,
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(4f, 2.5f),
+ MutableVec(4f, 2.5f),
15f,
8f,
Angle.QUARTER_TURN_RADIANS,
@@ -569,7 +563,7 @@
Parallelogram.areNear(
rotateParallelogram,
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-4f, -1f),
+ MutableVec(-4f, -1f),
6f,
8f,
Angle.HALF_TURN_RADIANS + Angle.QUARTER_TURN_RADIANS,
@@ -584,7 +578,7 @@
fun applyTransform_whenAppliedToAMutableParallelogram_canModifyInputAsOutput() {
val testMutableParallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(4f, 1f),
+ MutableVec(4f, 1f),
6f,
8f,
Angle.QUARTER_TURN_RADIANS,
@@ -596,7 +590,7 @@
assertThat(testMutableParallelogram)
.isEqualTo(
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(5f, 4f),
+ MutableVec(5f, 4f),
6f,
8f,
Angle.QUARTER_TURN_RADIANS,
@@ -606,6 +600,47 @@
}
@Test
+ fun constructWithValuesAndGetValues_shouldRoundTrip() {
+ val affineTransform = ImmutableAffineTransform(1F, 2F, 3F, 4F, 5F, 6F)
+
+ val outValues = FloatArray(6)
+ affineTransform.getValues(outValues)
+
+ assertThat(outValues)
+ .usingExactEquality()
+ .containsExactly(floatArrayOf(1F, 2F, 3F, 4F, 5F, 6F))
+ }
+
+ @Test
+ fun constructWithArrayAndGetValues_shouldRoundTrip() {
+ val values = floatArrayOf(1F, 2F, 3F, 4F, 5F, 6F)
+ val affineTransform = ImmutableAffineTransform(values)
+
+ val outValues = FloatArray(6)
+ affineTransform.getValues(outValues)
+
+ assertThat(outValues).usingExactEquality().containsExactly(values)
+ }
+
+ @Test
+ fun constructWithValues_shouldMatchConstructedWithFactoryFunctions() {
+ assertThat(ImmutableAffineTransform(7F, 0F, 0F, 0F, 7F, 0F))
+ .isEqualTo(ImmutableAffineTransform.scale(7F))
+
+ assertThat(ImmutableAffineTransform(3F, 0F, 0F, 0F, 5F, 0F))
+ .isEqualTo(ImmutableAffineTransform.scale(3F, 5F))
+
+ assertThat(ImmutableAffineTransform(4F, 0F, 0F, 0F, 1F, 0F))
+ .isEqualTo(ImmutableAffineTransform.scaleX(4F))
+
+ assertThat(ImmutableAffineTransform(1F, 0F, 0F, 0F, 2F, 0F))
+ .isEqualTo(ImmutableAffineTransform.scaleY(2F))
+
+ assertThat(ImmutableAffineTransform(1F, 0F, 8F, 0F, 1F, 9F))
+ .isEqualTo(ImmutableAffineTransform.translate(ImmutableVec(8F, 9F)))
+ }
+
+ @Test
fun asImmutable_returnsSelf() {
val affineTransform = ImmutableAffineTransform(A, B, C, D, E, F)
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableBoxTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableBoxTest.kt
index ccde8423..e05f766 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableBoxTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableBoxTest.kt
@@ -26,7 +26,7 @@
@Test
fun fromCenterAndDimensions_constructsCorrectImmutableBox() {
- val rect = ImmutableBox.fromCenterAndDimensions(ImmutablePoint(20f, -50f), 10f, 20f)
+ val rect = ImmutableBox.fromCenterAndDimensions(ImmutableVec(20f, -50f), 10f, 20f)
assertThat(rect.xMin).isEqualTo(15f)
assertThat(rect.xMax).isEqualTo(25f)
@@ -38,7 +38,7 @@
@Test
fun fromTwoPoints_constructsCorrectImmutableBox() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(20f, -50f), MutablePoint(-70f, 100f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(20f, -50f), MutableVec(-70f, 100f))
assertThat(rect.xMin).isEqualTo(-70f)
assertThat(rect.xMax).isEqualTo(20f)
@@ -50,7 +50,7 @@
@Test
fun minMaxFields_whenAllZeroes_allAreZero() {
- val zeroes = ImmutableBox.fromTwoPoints(ImmutablePoint(0F, 0F), ImmutablePoint(0F, 0F))
+ val zeroes = ImmutableBox.fromTwoPoints(ImmutableVec(0F, 0F), ImmutableVec(0F, 0F))
assertThat(zeroes.xMin).isEqualTo(0F)
assertThat(zeroes.yMin).isEqualTo(0F)
assertThat(zeroes.xMax).isEqualTo(0F)
@@ -59,7 +59,7 @@
@Test
fun minMaxFields_whenDeclaredInMinMaxOrder_matchOrder() {
- val inOrder = ImmutableBox.fromTwoPoints(ImmutablePoint(-1F, -2F), ImmutablePoint(3F, 4F))
+ val inOrder = ImmutableBox.fromTwoPoints(ImmutableVec(-1F, -2F), ImmutableVec(3F, 4F))
assertThat(inOrder.xMin).isEqualTo(-1F)
assertThat(inOrder.yMin).isEqualTo(-2F)
assertThat(inOrder.xMax).isEqualTo(3F)
@@ -68,8 +68,7 @@
@Test
fun minMaxFields_whenDeclaredOutOfOrder_doNotMatchOrder() {
- val outOfOrder =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(-3F, -4F))
+ val outOfOrder = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(-3F, -4F))
assertThat(outOfOrder.xMin).isEqualTo(-3F)
assertThat(outOfOrder.yMin).isEqualTo(-4F)
assertThat(outOfOrder.xMax).isEqualTo(1F)
@@ -78,7 +77,7 @@
@Test
fun widthHeight_whenAllZeroes_areAllZero() {
- val zeroes = ImmutableBox.fromTwoPoints(ImmutablePoint(0F, 0F), ImmutablePoint(0F, 0F))
+ val zeroes = ImmutableBox.fromTwoPoints(ImmutableVec(0F, 0F), ImmutableVec(0F, 0F))
assertThat(zeroes.width).isEqualTo(0)
assertThat(zeroes.height).isEqualTo(0)
@@ -86,7 +85,7 @@
@Test
fun widthHeight_whenDeclaredInOrder_areCorrectValues() {
- val inOrder = ImmutableBox.fromTwoPoints(ImmutablePoint(-1F, -2F), ImmutablePoint(3F, 4F))
+ val inOrder = ImmutableBox.fromTwoPoints(ImmutableVec(-1F, -2F), ImmutableVec(3F, 4F))
assertThat(inOrder.width).isEqualTo(4F)
assertThat(inOrder.height).isEqualTo(6F)
@@ -94,8 +93,7 @@
@Test
fun widthHeight_whenDeclaredOutOfOrder_areCorrectValues() {
- val outOfOrder =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(-3F, -4F))
+ val outOfOrder = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(-3F, -4F))
assertThat(outOfOrder.width).isEqualTo(4F)
assertThat(outOfOrder.height).isEqualTo(6F)
@@ -103,8 +101,7 @@
@Test
fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(immutableBox).isEqualTo(immutableBox)
assertThat(immutableBox.hashCode()).isEqualTo(immutableBox.hashCode())
@@ -112,18 +109,17 @@
@Test
fun equals_whenDifferentType_returnsFalse() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
- assertThat(immutableBox).isNotEqualTo(ImmutablePoint(1F, 2F))
+ assertThat(immutableBox).isNotEqualTo(ImmutableVec(1F, 2F))
}
@Test
fun equals_whenSameInterfacePropertiesAndDifferentType_returnsTrue() {
- val point1 = ImmutablePoint(1F, 2F)
- val point2 = ImmutablePoint(3F, 4F)
+ val point1 = ImmutableVec(1F, 2F)
+ val point2 = ImmutableVec(3F, 4F)
val immutableBox = ImmutableBox.fromTwoPoints(point1, point2)
- val mutableBox = MutableBox().fillFromTwoPoints(point1, point2)
+ val mutableBox = MutableBox().populateFromTwoPoints(point1, point2)
assertThat(immutableBox).isEqualTo(mutableBox)
assertThat(immutableBox.hashCode()).isEqualTo(mutableBox.hashCode())
@@ -131,9 +127,8 @@
@Test
fun equals_whenSameValues_returnsTrueAndSameHashCode() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- val other = ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
+ val other = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(immutableBox).isEqualTo(other)
assertThat(immutableBox.hashCode()).isEqualTo(other.hashCode())
@@ -141,9 +136,8 @@
@Test
fun equals_whenSameValuesOutOfOrder_returnsTrueAndSameHashCode() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- val other = ImmutableBox.fromTwoPoints(ImmutablePoint(3F, 4F), ImmutablePoint(1F, 2F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
+ val other = ImmutableBox.fromTwoPoints(ImmutableVec(3F, 4F), ImmutableVec(1F, 2F))
assertThat(immutableBox).isEqualTo(other)
assertThat(immutableBox.hashCode()).isEqualTo(other.hashCode())
@@ -151,103 +145,65 @@
@Test
fun equals_whenDifferentXMin_returnsFalse() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(immutableBox)
- .isNotEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(-1F, 2F), ImmutablePoint(3F, 4F))
- )
+ .isNotEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(-1F, 2F), ImmutableVec(3F, 4F)))
}
@Test
fun equals_whenDifferentYMin_returnsFalse() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(immutableBox)
- .isNotEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, -2F), ImmutablePoint(3F, 4F))
- )
+ .isNotEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(1F, -2F), ImmutableVec(3F, 4F)))
}
@Test
fun equals_whenDifferentXMax_returnsFalse() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(immutableBox)
- .isNotEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(30F, 4F))
- )
+ .isNotEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(30F, 4F)))
}
@Test
fun equals_whenDifferentYMax_returnsFalse() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(immutableBox)
- .isNotEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 40F))
- )
+ .isNotEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 40F)))
}
@Test
- fun newMutable_matchesValues() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ fun populateCenter_modifiesMutablePoint() {
+ val immutableBox = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 20F), ImmutableVec(3F, 40F))
+ val outCenter = MutableVec()
+ immutableBox.computeCenter(outCenter)
- assertThat(immutableBox.newMutable())
- .isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- )
+ assertThat(outCenter).isEqualTo(MutableVec(2F, 30F))
}
@Test
- fun fillMutable_correctlyModifiesOutput() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- val output = MutableBox()
+ fun corners_modifiesMutableVecs() {
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 20F), ImmutableVec(3F, 40F))
+ val p0 = MutableVec()
+ val p1 = MutableVec()
+ val p2 = MutableVec()
+ val p3 = MutableVec()
+ rect.computeCorners(p0, p1, p2, p3)
- immutableBox.fillMutable(output)
-
- assertThat(output)
- .isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- )
- }
-
- @Test
- fun center_modifiesMutablePoint() {
- val immutableBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 20F), ImmutablePoint(3F, 40F))
- val outCenter = MutablePoint()
- immutableBox.center(outCenter)
-
- assertThat(outCenter).isEqualTo(MutablePoint(2F, 30F))
- }
-
- @Test
- fun corners_modifiesMutablePoints() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 20F), ImmutablePoint(3F, 40F))
- val p0 = MutablePoint()
- val p1 = MutablePoint()
- val p2 = MutablePoint()
- val p3 = MutablePoint()
- rect.corners(p0, p1, p2, p3)
-
- assertThat(p0).isEqualTo(MutablePoint(1F, 20F))
- assertThat(p1).isEqualTo(MutablePoint(3F, 20F))
- assertThat(p2).isEqualTo(MutablePoint(3F, 40F))
- assertThat(p3).isEqualTo(MutablePoint(1F, 40F))
+ assertThat(p0).isEqualTo(MutableVec(1F, 20F))
+ assertThat(p1).isEqualTo(MutableVec(3F, 20F))
+ assertThat(p2).isEqualTo(MutableVec(3F, 40F))
+ assertThat(p3).isEqualTo(MutableVec(1F, 40F))
}
@Test
fun contains_returnsCorrectValuesWithPoint() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(10F, 600F), ImmutablePoint(40F, 900F))
- val innerPoint = ImmutablePoint(30F, 700F)
- val outerPoint = ImmutablePoint(70F, 2000F)
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(10F, 600F), ImmutableVec(40F, 900F))
+ val innerPoint = ImmutableVec(30F, 700F)
+ val outerPoint = ImmutableVec(70F, 2000F)
assertThat(rect.contains(innerPoint)).isTrue()
assertThat(rect.contains(outerPoint)).isFalse()
@@ -255,83 +211,10 @@
@Test
fun contains_returnsCorrectValuesWithBox() {
- val outerRect =
- ImmutableBox.fromTwoPoints(ImmutablePoint(10F, 600F), ImmutablePoint(40F, 900F))
- val innerRect =
- ImmutableBox.fromTwoPoints(ImmutablePoint(20F, 700F), ImmutablePoint(30F, 800F))
+ val outerRect = ImmutableBox.fromTwoPoints(ImmutableVec(10F, 600F), ImmutableVec(40F, 900F))
+ val innerRect = ImmutableBox.fromTwoPoints(ImmutableVec(20F, 700F), ImmutableVec(30F, 800F))
assertThat(outerRect.contains(innerRect)).isTrue()
assertThat(innerRect.contains(outerRect)).isFalse()
}
-
- @Test
- fun copy_withNoArguments_returnsThis() {
- val original = ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
-
- assertThat(original.copy()).isSameInstanceAs(original)
- }
-
- @Test
- fun copy_withArguments_makesCopy() {
- val x1 = 1F
- val y1 = 2F
- val x2 = 3F
- val y2 = 4F
- val original = ImmutableBox.fromTwoPoints(ImmutablePoint(x1, y1), ImmutablePoint(x2, y2))
- // Different values that won't result in the min/max in either x or y dimension flipping.
- val differentX1 = 0.5F
- val differentY1 = 1.5F
- val differentX2 = 2.5F
- val differentY2 = 3.5F
-
- // Change all values.
- assertThat(original.copy(differentX1, differentY1, differentX2, differentY2))
- .isEqualTo(
- ImmutableBox.fromTwoPoints(
- ImmutablePoint(differentX1, differentY1),
- ImmutablePoint(differentX2, differentY2),
- )
- )
-
- // Change x1.
- assertThat(original.copy(x1 = differentX1))
- .isEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(differentX1, y1), ImmutablePoint(x2, y2))
- )
-
- // Change y1.
- assertThat(original.copy(y1 = differentY1))
- .isEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(x1, differentY1), ImmutablePoint(x2, y2))
- )
-
- // Change x2.
- assertThat(original.copy(x2 = differentX2))
- .isEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(x1, y1), ImmutablePoint(differentX2, y2))
- )
-
- // Change y2.
- assertThat(original.copy(y2 = differentY2))
- .isEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(x1, y1), ImmutablePoint(x2, differentY2))
- )
- }
-
- @Test
- fun copy_withArgumentsThatReverseBounds_makesCopyWith() {
- val x1 = 1F
- val y1 = 2F
- val x2 = 3F
- val y2 = 4F
- val original = ImmutableBox.fromTwoPoints(ImmutablePoint(x1, y1), ImmutablePoint(x2, y2))
- // Different value that results in the min/max in x dimension flipping.
- val differentX1 = 5F
-
- // Change x1 will flip x1 and x2 values.
- assertThat(original.copy(x1 = differentX1))
- .isEqualTo(
- ImmutableBox.fromTwoPoints(ImmutablePoint(x2, y1), ImmutablePoint(differentX1, y2))
- )
- }
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableParallelogramTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableParallelogramTest.kt
index c436f4c..b14ff70 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableParallelogramTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableParallelogramTest.kt
@@ -27,9 +27,9 @@
@Test
fun fromCenterAndDimensions_constructsCorrectImmutableParallelogram() {
val parallelogram =
- ImmutableParallelogram.fromCenterAndDimensions(ImmutablePoint(10f, 0f), 6f, 4f)
+ ImmutableParallelogram.fromCenterAndDimensions(ImmutableVec(10f, 0f), 6f, 4f)
- assertThat(parallelogram.center).isEqualTo(ImmutablePoint(10f, 0f))
+ assertThat(parallelogram.center).isEqualTo(ImmutableVec(10f, 0f))
assertThat(parallelogram.width).isEqualTo(6f)
assertThat(parallelogram.height).isEqualTo(4f)
assertThat(parallelogram.rotation).isZero()
@@ -40,13 +40,13 @@
fun fromCenterDimensionsAndRotation_constructsCorrectImmutableParallelogram() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsAndRotation(
- ImmutablePoint(10f, 0f),
+ ImmutableVec(10f, 0f),
6f,
4f,
Angle.FULL_TURN_RADIANS,
)
- assertThat(parallelogram.center).isEqualTo(ImmutablePoint(10f, 0f))
+ assertThat(parallelogram.center).isEqualTo(ImmutableVec(10f, 0f))
assertThat(parallelogram.width).isEqualTo(6f)
assertThat(parallelogram.height).isEqualTo(4f)
assertThat(parallelogram.rotation).isZero()
@@ -57,14 +57,14 @@
fun fromCenterDimensionsRotationAndShear_constructsCorrectImmutableParallelogram() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(10f, 0f),
+ ImmutableVec(10f, 0f),
6f,
4f,
Angle.HALF_TURN_RADIANS,
1f,
)
- assertThat(parallelogram.center).isEqualTo(ImmutablePoint(10f, 0f))
+ assertThat(parallelogram.center).isEqualTo(ImmutableVec(10f, 0f))
assertThat(parallelogram.width).isEqualTo(6f)
assertThat(parallelogram.height).isEqualTo(4f)
assertThat(parallelogram.rotation).isWithin(1e-6f).of(Math.PI.toFloat())
@@ -75,7 +75,7 @@
fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(10f, 10f),
+ ImmutableVec(10f, 10f),
12f,
2f,
Angle.HALF_TURN_RADIANS,
@@ -89,7 +89,7 @@
fun equals_whenSameValues_returnsTrueAndSameHashCode() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -97,7 +97,7 @@
)
val other =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -113,13 +113,13 @@
// An axis-aligned rectangle with center at (0,0) and width and height equal to 2
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(0f, 0f),
+ ImmutableVec(0f, 0f),
2f,
2f,
Angle.ZERO,
0f,
)
- val other = ImmutableBox.fromTwoPoints(ImmutablePoint(-1f, -1f), ImmutablePoint(1f, 1f))
+ val other = ImmutableBox.fromTwoPoints(ImmutableVec(-1f, -1f), ImmutableVec(1f, 1f))
assertThat(parallelogram).isNotEqualTo(other)
}
@@ -128,7 +128,7 @@
fun equals_whenDifferentCenter_returnsFalse() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -136,7 +136,7 @@
)
val other =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(10f, -10.5f),
+ ImmutableVec(10f, -10.5f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -150,7 +150,7 @@
fun equals_whenDifferentWidth_returnsFalse() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
11f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -158,7 +158,7 @@
)
val other =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -172,7 +172,7 @@
fun equals_whenDifferentHeight_returnsFalse() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -180,7 +180,7 @@
)
val other =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
7.5f,
Angle.HALF_TURN_RADIANS,
@@ -194,7 +194,7 @@
fun equals_whenDifferentRotation_returnsFalse() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -202,7 +202,7 @@
)
val other =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.QUARTER_TURN_RADIANS,
@@ -216,7 +216,7 @@
fun equals_whenDifferentShearFactor_returnsFalse() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -224,7 +224,7 @@
)
val other =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(-10f, 10f),
+ ImmutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -238,14 +238,14 @@
fun getters_returnCorrectValues() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(3f, -5f),
+ ImmutableVec(3f, -5f),
8f,
-1f,
Angle.HALF_TURN_RADIANS,
0f,
)
- assertThat(parallelogram.center).isEqualTo(ImmutablePoint(3f, -5f))
+ assertThat(parallelogram.center).isEqualTo(ImmutableVec(3f, -5f))
assertThat(parallelogram.width).isEqualTo(8f)
assertThat(parallelogram.height).isEqualTo(-1f)
assertThat(parallelogram.rotation).isEqualTo(Angle.HALF_TURN_RADIANS)
@@ -255,22 +255,22 @@
@Test
fun signedArea_returnsCorrectValue() {
val parallelogram =
- ImmutableParallelogram.fromCenterAndDimensions(ImmutablePoint(0f, 10f), 6f, 4f)
+ ImmutableParallelogram.fromCenterAndDimensions(ImmutableVec(0f, 10f), 6f, 4f)
val degenerateParallelogram =
- ImmutableParallelogram.fromCenterAndDimensions(ImmutablePoint(0f, 10f), 0f, 4f)
+ ImmutableParallelogram.fromCenterAndDimensions(ImmutableVec(0f, 10f), 0f, 4f)
val negativeAreaParallelogram =
- ImmutableParallelogram.fromCenterAndDimensions(ImmutablePoint(0f, 10f), 2f, -3f)
+ ImmutableParallelogram.fromCenterAndDimensions(ImmutableVec(0f, 10f), 2f, -3f)
- assertThat(parallelogram.signedArea()).isEqualTo(24f)
- assertThat(degenerateParallelogram.signedArea()).isZero()
- assertThat(negativeAreaParallelogram.signedArea()).isEqualTo(-6f)
+ assertThat(parallelogram.computeSignedArea()).isEqualTo(24f)
+ assertThat(degenerateParallelogram.computeSignedArea()).isZero()
+ assertThat(negativeAreaParallelogram.computeSignedArea()).isEqualTo(-6f)
}
@Test
fun toString_returnsCorrectValue() {
val parallelogramString =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(3f, -5f),
+ ImmutableVec(3f, -5f),
8f,
-1f,
Angle.HALF_TURN_RADIANS,
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutablePointTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutablePointTest.kt
deleted file mode 100644
index 564f9bd..0000000
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutablePointTest.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ImmutablePointTest {
-
- @Test
- fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
- val point = ImmutablePoint(1f, 2f)
-
- assertThat(point).isEqualTo(point)
- assertThat(point.hashCode()).isEqualTo(point.hashCode())
- }
-
- @Test
- fun equals_whenDifferentType_returnsFalse() {
- val point = ImmutablePoint(1f, 2f)
- val other = ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- assertThat(point).isNotEqualTo(other)
- }
-
- @Test
- fun equals_whenSameValues_returnsTrueAndSameHashCode() {
- val point = ImmutablePoint(-3f, 1.2f)
- val other = ImmutablePoint(-3f, 1.2f)
-
- assertThat(point).isEqualTo(other)
- assertThat(point.hashCode()).isEqualTo(other.hashCode())
- }
-
- @Test
- fun equals_whenFlippedValues_returnsFalse() {
- val point = ImmutablePoint(10f, 2134f)
- val other = ImmutablePoint(2134f, 10f)
-
- assertThat(point).isNotEqualTo(other)
- }
-
- @Test
- fun getters_returnCorrectValues() {
- val point = ImmutablePoint(10f, 2134f)
-
- assertThat(point.x).isEqualTo(10f)
- assertThat(point.y).isEqualTo(2134f)
- }
-
- @Test
- fun newMutable_returnsCorrectMutablePoint() {
- val point = ImmutablePoint(2.1f, 2134f)
-
- assertThat(point.newMutable()).isEqualTo(MutablePoint(2.1f, 2134f))
- }
-
- @Test
- fun fillMutable_correctlyModifiesMutablePoint() {
- val point = ImmutablePoint(2.1f, 2134f)
- val output = MutablePoint()
-
- point.fillMutable(output)
-
- assertThat(output).isEqualTo(MutablePoint(2.1f, 2134f))
- }
-
- @Test
- fun getVec_correctlyModifiesMutableVec() {
- val point = ImmutablePoint(65.26f, -9228f)
- val output = MutableVec()
-
- point.getVec(output)
-
- assertThat(output).isEqualTo(MutableVec(65.26f, -9228f))
- }
-
- @Test
- fun copy_withNoArguments_returnsThis() {
- val point = ImmutablePoint(1f, 2f)
-
- assertThat(point.copy()).isSameInstanceAs(point)
- }
-
- @Test
- fun copy_withArguments_makesCopy() {
- val x = 1f
- val y = 2f
- val point = ImmutablePoint(x, y)
- val differentX = 3f
- val differentY = 4f
-
- // Change both x and y.
- assertThat(point.copy(x = differentX, y = differentY))
- .isEqualTo(ImmutablePoint(differentX, differentY))
-
- // Change x.
- assertThat(point.copy(x = differentX)).isEqualTo(ImmutablePoint(differentX, y))
-
- // Change y.
- assertThat(point.copy(y = differentY)).isEqualTo(ImmutablePoint(x, differentY))
- }
-
- @Test
- fun add_withPointThenVec_correctlyAddsAndFillsMutablePoint() {
- val point = ImmutablePoint(10f, 40f)
- val vec = ImmutableVec(5f, -2f)
- val output = MutablePoint()
-
- Point.add(point, vec, output)
-
- assertThat(output).isEqualTo(MutablePoint(15f, 38f))
- }
-
- @Test
- fun add_withVecThenPoint_correctlyAddsAndFillsMutablePoint() {
- val point = ImmutablePoint(10f, 40f)
- val vec = ImmutableVec(5f, -2f)
- val output = MutablePoint()
-
- Point.add(vec, point, output)
-
- assertThat(output).isEqualTo(MutablePoint(15f, 38f))
- }
-
- @Test
- fun subtract_pointMinusVec_correctlySubtractsAndFillsMutablePoint() {
- val point = ImmutablePoint(10f, 40f)
- val vec = ImmutableVec(5f, -2f)
- val output = MutablePoint()
-
- Point.subtract(point, vec, output)
-
- assertThat(output).isEqualTo(MutablePoint(5f, 42f))
- }
-
- @Test
- fun subtract_pointMinusPoint_correctlySubtractsAndFillsMutableVec() {
- val lhsPoint = ImmutablePoint(10f, 40f)
- val rhsPoint = ImmutablePoint(5f, -2f)
- val output = MutableVec()
-
- Point.subtract(lhsPoint, rhsPoint, output)
-
- assertThat(output).isEqualTo(MutableVec(5f, 42f))
- }
-}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableSegmentTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableSegmentTest.kt
index 23ca105..a1dc1c6 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableSegmentTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableSegmentTest.kt
@@ -25,14 +25,6 @@
class ImmutableSegmentTest {
@Test
- fun vec_whenPrimaryValuesAreUnchanged_returnsSameInstance() {
- val segment = ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 2f))
- val vec = segment.vec
-
- assertThat(vec).isSameInstanceAs(segment.vec)
- }
-
- @Test
fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
val segment = ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 2f))
@@ -96,18 +88,7 @@
}
@Test
- fun asImmutable_withDifferentValues_returnsNewInstance() {
- val segment = ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 2f))
- val newStart = ImmutableVec(10f, 20f)
- val newEnd = ImmutableVec(30f, 40f)
- val output = segment.asImmutable(newStart, newEnd)
-
- assertThat(output.start).isSameInstanceAs(newStart)
- assertThat(output.end).isSameInstanceAs(newEnd)
- }
-
- @Test
- fun isAlmostEqual_usesTolereneceToCompareValues() {
+ fun isAlmostEqual_usesToleranceToCompareValues() {
val segment = ImmutableSegment(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f))
val other = ImmutableSegment(ImmutableVec(1.01f, 2.02f), ImmutableVec(3.03f, 4.04f))
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableTriangleTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableTriangleTest.kt
index a393d21..fdab00e 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableTriangleTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableTriangleTest.kt
@@ -94,12 +94,12 @@
fun edge_returnsCorrectSegment() {
val triangle = ImmutableTriangle(P0, P1, P2)
- assertThat(triangle.edge(0)).isEqualTo(ImmutableSegment(P0, P1))
- assertThat(triangle.edge(1)).isEqualTo(ImmutableSegment(P1, P2))
- assertThat(triangle.edge(2)).isEqualTo(ImmutableSegment(P2, P0))
- assertThat(triangle.edge(3)).isEqualTo(ImmutableSegment(P0, P1))
- assertThat(triangle.edge(4)).isEqualTo(ImmutableSegment(P1, P2))
- assertThat(triangle.edge(5)).isEqualTo(ImmutableSegment(P2, P0))
+ assertThat(triangle.computeEdge(0)).isEqualTo(ImmutableSegment(P0, P1))
+ assertThat(triangle.computeEdge(1)).isEqualTo(ImmutableSegment(P1, P2))
+ assertThat(triangle.computeEdge(2)).isEqualTo(ImmutableSegment(P2, P0))
+ assertThat(triangle.computeEdge(3)).isEqualTo(ImmutableSegment(P0, P1))
+ assertThat(triangle.computeEdge(4)).isEqualTo(ImmutableSegment(P1, P2))
+ assertThat(triangle.computeEdge(5)).isEqualTo(ImmutableSegment(P2, P0))
}
@Test
@@ -124,8 +124,8 @@
val segment0 = MutableSegment()
val segment6 = MutableSegment()
- triangle.populateEdge(0, segment0)
- triangle.populateEdge(6, segment6)
+ triangle.computeEdge(0, segment0)
+ triangle.computeEdge(6, segment6)
assertThat(segment0).isEqualTo(ImmutableSegment(P0, P1))
assertThat(segment6).isEqualTo(ImmutableSegment(P0, P1))
@@ -137,8 +137,8 @@
val segment1 = MutableSegment()
val segment7 = MutableSegment()
- triangle.populateEdge(1, segment1)
- triangle.populateEdge(7, segment7)
+ triangle.computeEdge(1, segment1)
+ triangle.computeEdge(7, segment7)
assertThat(segment1).isEqualTo(ImmutableSegment(P1, P2))
assertThat(segment7).isEqualTo(ImmutableSegment(P1, P2))
@@ -150,8 +150,8 @@
val segment2 = MutableSegment()
val segment8 = MutableSegment()
- triangle.populateEdge(2, segment2)
- triangle.populateEdge(8, segment8)
+ triangle.computeEdge(2, segment2)
+ triangle.computeEdge(8, segment8)
assertThat(segment2).isEqualTo(ImmutableSegment(P2, P0))
assertThat(segment8).isEqualTo(ImmutableSegment(P2, P0))
@@ -166,28 +166,7 @@
}
@Test
- fun asImmutable_withSameValues_returnsSelf() {
- val triangle = ImmutableTriangle(P0, P1, P2)
- val output = triangle.asImmutable(P0, P1, P2)
-
- assertThat(output).isSameInstanceAs(triangle)
- }
-
- @Test
- fun asImmutable_withDifferentValues_returnsNewInstance() {
- val triangle = ImmutableTriangle(P0, P1, P2)
- val p0 = ImmutableVec(10f, 20f)
- val p1 = ImmutableVec(30f, 40f)
- val p2 = ImmutableVec(50f, 60f)
- val output = triangle.asImmutable(p0, p1, p2)
-
- assertThat(output.p0).isSameInstanceAs(p0)
- assertThat(output.p1).isSameInstanceAs(p1)
- assertThat(output.p2).isSameInstanceAs(p2)
- }
-
- @Test
- fun isAlmostEqual_usesTolereneceToCompareValues() {
+ fun isAlmostEqual_usesToleranceToCompareValues() {
val triangle =
ImmutableTriangle(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f), ImmutableVec(5f, 6f))
val other =
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableVecTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableVecTest.kt
index e6cba99..1990a13 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableVecTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ImmutableVecTest.kt
@@ -36,9 +36,9 @@
@Test
fun equals_whenDifferentType_returnsFalse() {
val vec = ImmutableVec(1f, 2f)
- val point = ImmutablePoint(1f, 2f)
+ val segment = ImmutableSegment(ImmutableVec(1f, 2f), ImmutableVec(3f, 4f))
- assertThat(vec).isNotEqualTo(point)
+ assertThat(vec).isNotEqualTo(segment)
}
@Test
@@ -74,106 +74,75 @@
}
@Test
- fun newMutable_returnsCorrectMutableVec() {
- val vec = ImmutableVec(2.1f, 2134f)
-
- assertThat(vec.newMutable()).isEqualTo(MutableVec(2.1f, 2134f))
- }
-
- @Test
- fun fillMutable_correctlyModifiesMutableVec() {
- val vec = ImmutableVec(2.1f, 2134f)
- val output = MutableVec()
-
- vec.fillMutable(output)
-
- assertThat(output).isEqualTo(MutableVec(2.1f, 2134f))
- }
-
- @Test
fun orthogonal_returnsCorrectValue() {
- assertThat(ImmutableVec(3f, 1f).orthogonal).isEqualTo(ImmutableVec(-1f, 3f))
- assertThat(ImmutableVec(-395f, .005f).orthogonal).isEqualTo(ImmutableVec(-.005f, -395f))
- assertThat(ImmutableVec(-.2f, -.66f).orthogonal).isEqualTo(ImmutableVec(.66f, -.2f))
- assertThat(ImmutableVec(123f, -987f).orthogonal).isEqualTo(ImmutableVec(987f, 123f))
+ assertThat(ImmutableVec(3f, 1f).computeOrthogonal()).isEqualTo(ImmutableVec(-1f, 3f))
+ assertThat(ImmutableVec(-395f, .005f).computeOrthogonal())
+ .isEqualTo(ImmutableVec(-.005f, -395f))
+ assertThat(ImmutableVec(-.2f, -.66f).computeOrthogonal())
+ .isEqualTo(ImmutableVec(.66f, -.2f))
+ assertThat(ImmutableVec(123f, -987f).computeOrthogonal())
+ .isEqualTo(ImmutableVec(987f, 123f))
}
@Test
fun populateOrthogonal_populatesCorrectValue() {
val mutableVec = MutableVec()
- ImmutableVec(3f, 1f).populateOrthogonal(mutableVec)
+ ImmutableVec(3f, 1f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-1f, 3f))
- ImmutableVec(-395f, .005f).populateOrthogonal(mutableVec)
+ ImmutableVec(-395f, .005f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-.005f, -395f))
- ImmutableVec(-.2f, -.66f).populateOrthogonal(mutableVec)
+ ImmutableVec(-.2f, -.66f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(.66f, -.2f))
- ImmutableVec(123f, -987f).populateOrthogonal(mutableVec)
+ ImmutableVec(123f, -987f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(987f, 123f))
}
@Test
fun negation_returnsCorrectValue() {
- assertThat(ImmutableVec(3f, 1f).negation).isEqualTo(ImmutableVec(-3f, -1f))
- assertThat(ImmutableVec(-395f, .005f).negation).isEqualTo(ImmutableVec(395f, -.005f))
- assertThat(ImmutableVec(-.2f, -.66f).negation).isEqualTo(ImmutableVec(.2f, .66f))
- assertThat(ImmutableVec(123f, -987f).negation).isEqualTo(ImmutableVec(-123f, 987f))
+ assertThat(ImmutableVec(3f, 1f).computeNegation()).isEqualTo(ImmutableVec(-3f, -1f))
+ assertThat(ImmutableVec(-395f, .005f).computeNegation())
+ .isEqualTo(ImmutableVec(395f, -.005f))
+ assertThat(ImmutableVec(-.2f, -.66f).computeNegation()).isEqualTo(ImmutableVec(.2f, .66f))
+ assertThat(ImmutableVec(123f, -987f).computeNegation()).isEqualTo(ImmutableVec(-123f, 987f))
}
@Test
fun populateNegation_populatesCorrectValue() {
val mutableVec = MutableVec()
- ImmutableVec(3f, 1f).populateNegation(mutableVec)
+ ImmutableVec(3f, 1f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-3f, -1f))
- ImmutableVec(-395f, .005f).populateNegation(mutableVec)
+ ImmutableVec(-395f, .005f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(395f, -.005f))
- ImmutableVec(-.2f, -.66f).populateNegation(mutableVec)
+ ImmutableVec(-.2f, -.66f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(.2f, .66f))
- ImmutableVec(123f, -987f).populateNegation(mutableVec)
+ ImmutableVec(123f, -987f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-123f, 987f))
}
@Test
fun magnitude_returnsCorrectValue() {
- assertThat(ImmutableVec(1f, 1f).magnitude).isEqualTo(sqrt(2f))
- assertThat(ImmutableVec(-3f, 4f).magnitude).isEqualTo(5f)
- assertThat(ImmutableVec(0f, 0f).magnitude).isEqualTo(0f)
- assertThat(ImmutableVec(0f, 17f).magnitude).isEqualTo(17f)
+ assertThat(ImmutableVec(1f, 1f).computeMagnitude()).isEqualTo(sqrt(2f))
+ assertThat(ImmutableVec(-3f, 4f).computeMagnitude()).isEqualTo(5f)
+ assertThat(ImmutableVec(0f, 0f).computeMagnitude()).isEqualTo(0f)
+ assertThat(ImmutableVec(0f, 17f).computeMagnitude()).isEqualTo(17f)
}
@Test
fun magnitudeSquared_returnsCorrectValue() {
- assertThat(ImmutableVec(1f, 1f).magnitudeSquared).isEqualTo(2f)
- assertThat(ImmutableVec(3f, -4f).magnitudeSquared).isEqualTo(25f)
- assertThat(ImmutableVec(0f, 0f).magnitudeSquared).isEqualTo(0f)
- assertThat(ImmutableVec(15f, 0f).magnitudeSquared).isEqualTo(225f)
+ assertThat(ImmutableVec(1f, 1f).computeMagnitudeSquared()).isEqualTo(2f)
+ assertThat(ImmutableVec(3f, -4f).computeMagnitudeSquared()).isEqualTo(25f)
+ assertThat(ImmutableVec(0f, 0f).computeMagnitudeSquared()).isEqualTo(0f)
+ assertThat(ImmutableVec(15f, 0f).computeMagnitudeSquared()).isEqualTo(225f)
}
@Test
- fun asImmutableVal_returnsThis() {
- val vec = ImmutableVec(1f, 2f)
-
- assertThat(vec.asImmutable).isSameInstanceAs(vec)
- }
-
- @Test
- fun asImmutableFun_withNoArguments_returnsThis() {
+ fun asImmutable_returnsThis() {
val vec = ImmutableVec(1f, 2f)
assertThat(vec.asImmutable()).isSameInstanceAs(vec)
}
@Test
- fun asImmutableFun_withArguments_returnsCorrectNewImmutableVec() {
- val vec = ImmutableVec(1f, 2f)
-
- assertThat(vec.asImmutable(x = 10f)).isEqualTo(ImmutableVec(10f, 2f))
- assertThat(vec.asImmutable(10f)).isEqualTo(ImmutableVec(10f, 2f))
- assertThat(vec.asImmutable(y = 20f)).isEqualTo(ImmutableVec(1f, 20f))
- assertThat(vec.asImmutable(x = 10f, y = 20f)).isEqualTo(ImmutableVec(10f, 20f))
- assertThat(vec.asImmutable(10f, 20f)).isEqualTo(ImmutableVec(10f, 20f))
- }
-
- @Test
fun toString_doesNotCrash() {
assertThat(ImmutableVec(1F, 2F).toString()).isNotEmpty()
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/IntersectionTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/IntersectionTest.kt
index dd0346a..390b62d 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/IntersectionTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/IntersectionTest.kt
@@ -159,7 +159,7 @@
val shearFactor = 1f // = cotangent(PI/4), represents a 45-degree shear
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(center.x, center.y),
+ center = ImmutableVec(center.x, center.y),
width = width,
height = height,
rotation = Angle.ZERO,
@@ -210,7 +210,7 @@
fun intersects_whenPointParallelogramDoesNotIntersect_returnsFalse() {
val parallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(10f, 0f),
+ center = ImmutableVec(10f, 0f),
width = 1f,
height = 1f,
rotation = Angle.HALF_TURN_RADIANS / 4f,
@@ -236,8 +236,7 @@
@Test
fun intersects_whenPointBoxIntersects_returnsTrue() {
- val rect =
- ImmutableBox.fromTwoPoints(ImmutablePoint(3.5f, 10.9f), ImmutablePoint(2.5f, 1.1f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(3.5f, 10.9f), ImmutableVec(2.5f, 1.1f))
val vertex0 = ImmutableVec(3.5f, 10.9f)
val vertex1 = MutableVec(3.5f, 1.1f)
val vertex2 = ImmutableVec(2.5f, 10.9f)
@@ -270,7 +269,7 @@
@Test
fun intersects_whenPointBoxDoesNotIntersect_returnsFalse() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(-1f, 3.2f), ImmutablePoint(7f, 11.8f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(-1f, 3.2f), ImmutableVec(7f, 11.8f))
val closeExteriorPoint = ImmutableVec(7.1f, 3.2f)
val farExteriorPoint = ImmutableVec(-10f, -100f)
@@ -377,11 +376,11 @@
fun intersects_whenSegmentBoxIntersects_returnsTrue() {
val segment = ImmutableSegment(start = ImmutableVec(-1f, 3.2f), end = ImmutableVec(9f, 5f))
val rectWithCommonMinPoint =
- ImmutableBox.fromTwoPoints(ImmutablePoint(-1f, 3.2f), ImmutablePoint(-10f, 0f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(-1f, 3.2f), ImmutableVec(-10f, 0f))
val rectWithCommonMaxPoint =
- ImmutableBox.fromTwoPoints(ImmutablePoint(9f, 5f), ImmutablePoint(20f, 11.4f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(9f, 5f), ImmutableVec(20f, 11.4f))
val intersectingBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(0f, 1f), ImmutablePoint(8f, 21f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(0f, 1f), ImmutableVec(8f, 21f))
assertThat(segment.intersects(rectWithCommonMinPoint)).isTrue()
assertThat(segment.intersects(rectWithCommonMaxPoint)).isTrue()
@@ -394,9 +393,8 @@
@Test
fun intersects_whenSegmentBoxDoesNotIntersect_returnsFalse() {
val segment = ImmutableSegment(start = ImmutableVec(-1f, 3.2f), end = ImmutableVec(9f, 5f))
- val closeBox = ImmutableBox.fromTwoPoints(ImmutablePoint(9.1f, 5f), ImmutablePoint(10f, 6f))
- val farBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(-10f, -2f), ImmutablePoint(-21f, -8f))
+ val closeBox = ImmutableBox.fromTwoPoints(ImmutableVec(9.1f, 5f), ImmutableVec(10f, 6f))
+ val farBox = ImmutableBox.fromTwoPoints(ImmutableVec(-10f, -2f), ImmutableVec(-21f, -8f))
assertThat(segment.intersects(closeBox)).isFalse()
assertThat(segment.intersects(farBox)).isFalse()
@@ -409,7 +407,7 @@
val segment = ImmutableSegment(start = ImmutableVec(-1f, 3.2f), end = ImmutableVec(9f, 5f))
val parallelogramWithCommonVertex =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(1f, 6.2f),
+ center = ImmutableVec(1f, 6.2f),
width = 4f,
height = 6f,
rotation = Angle.ZERO,
@@ -417,7 +415,7 @@
)
val intersectingParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(4f, 4.1f),
+ center = ImmutableVec(4f, 4.1f),
width = 4f,
height = 6f,
rotation = Angle.ZERO,
@@ -435,7 +433,7 @@
val segment = ImmutableSegment(start = ImmutableVec(-1f, 3.2f), end = ImmutableVec(9f, 5f))
val closeParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(10.1f, 7f),
+ center = ImmutableVec(10.1f, 7f),
width = 2f,
height = 4f,
rotation = Angle.ZERO,
@@ -443,7 +441,7 @@
)
val farParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(-100f, -103.1f),
+ center = ImmutableVec(-100f, -103.1f),
width = 4f,
height = 7.2f,
rotation = Angle.QUARTER_TURN_RADIANS,
@@ -547,11 +545,11 @@
p2 = ImmutableVec(4.2f, 10f),
)
val rectWithCommonP2 =
- ImmutableBox.fromTwoPoints(ImmutablePoint(4.2f, 10f), ImmutablePoint(7.9f, 19.2f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(4.2f, 10f), ImmutableVec(7.9f, 19.2f))
val rectWithCommonEdge =
- ImmutableBox.fromTwoPoints(ImmutablePoint(-10f, 1f), ImmutablePoint(0f, 31.6f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(-10f, 1f), ImmutableVec(0f, 31.6f))
val intersectingBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(2.1f, 20f), ImmutablePoint(6.5f, 31.9f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(2.1f, 20f), ImmutableVec(6.5f, 31.9f))
assertThat(triangle.intersects(rectWithCommonP2)).isTrue()
assertThat(triangle.intersects(rectWithCommonEdge)).isTrue()
@@ -569,10 +567,8 @@
p1 = ImmutableVec(0f, 31.6f),
p2 = ImmutableVec(4.2f, 10f),
)
- val closeBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(0f, 0.9f), ImmutablePoint(-51.1f, -2f))
- val farBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(100f, 200f), ImmutablePoint(300f, 400f))
+ val closeBox = ImmutableBox.fromTwoPoints(ImmutableVec(0f, 0.9f), ImmutableVec(-51.1f, -2f))
+ val farBox = ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
assertThat(triangle.intersects(closeBox)).isFalse()
assertThat(triangle.intersects(farBox)).isFalse()
@@ -590,7 +586,7 @@
)
val parallelogramWithCommonP1 =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(1.5f, 32.6f),
+ center = ImmutableVec(1.5f, 32.6f),
width = 3f,
height = 2f,
rotation = Angle.ZERO,
@@ -598,7 +594,7 @@
)
val parallelogramWithCommonEdge =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(-1f, 16.3f),
+ center = ImmutableVec(-1f, 16.3f),
width = 2f,
height = 15.3f,
rotation = Angle.ZERO,
@@ -606,7 +602,7 @@
)
val intersectingParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(2.1f, 17.4f),
+ center = ImmutableVec(2.1f, 17.4f),
width = 10f,
height = 19.4f,
rotation = Angle.ZERO,
@@ -631,7 +627,7 @@
)
val closeParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(-5.1f, 2f),
+ center = ImmutableVec(-5.1f, 2f),
width = 10f,
height = 13.2f,
rotation = Angle.ZERO,
@@ -639,7 +635,7 @@
)
val farParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(100f, 200f),
+ center = ImmutableVec(100f, 200f),
width = 0.6f,
height = 2.3f,
rotation = Angle.QUARTER_TURN_RADIANS,
@@ -654,8 +650,8 @@
@Test
fun intersects_forEqualBoxs_returnsTrue() {
- val rect1 = ImmutableBox.fromTwoPoints(ImmutablePoint(0f, 1f), ImmutablePoint(31.6f, 10f))
- val rect2 = ImmutableBox.fromTwoPoints(ImmutablePoint(0f, 1f), ImmutablePoint(31.6f, 10f))
+ val rect1 = ImmutableBox.fromTwoPoints(ImmutableVec(0f, 1f), ImmutableVec(31.6f, 10f))
+ val rect2 = ImmutableBox.fromTwoPoints(ImmutableVec(0f, 1f), ImmutableVec(31.6f, 10f))
assertThat(rect1.intersects(rect1)).isTrue()
assertThat(rect1.intersects(rect2)).isTrue()
@@ -664,13 +660,13 @@
@Test
fun intersects_whenBoxBoxIntersects_returnsTrue() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(2.1f, 1f), ImmutablePoint(31.6f, 10f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(2.1f, 1f), ImmutableVec(31.6f, 10f))
val rectWithCommonVertex =
- ImmutableBox.fromTwoPoints(ImmutablePoint(2.1f, 1f), ImmutablePoint(-3f, -6.5f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(2.1f, 1f), ImmutableVec(-3f, -6.5f))
val rectWithCommonEdge =
- ImmutableBox.fromTwoPoints(ImmutablePoint(31.6f, 5f), ImmutablePoint(67.9f, 2f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(31.6f, 5f), ImmutableVec(67.9f, 2f))
val intersectingBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(6.7f, 3f), ImmutablePoint(20f, 100.2f))
+ ImmutableBox.fromTwoPoints(ImmutableVec(6.7f, 3f), ImmutableVec(20f, 100.2f))
assertThat(rect.intersects(rectWithCommonVertex)).isTrue()
assertThat(rect.intersects(rectWithCommonEdge)).isTrue()
@@ -682,11 +678,9 @@
@Test
fun intersects_whenBoxBoxDoesNotIntersect_returnsFalse() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(2.1f, 1f), ImmutablePoint(31.6f, 10f))
- val closeBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(2f, 1f), ImmutablePoint(-10f, -11f))
- val farBox =
- ImmutableBox.fromTwoPoints(ImmutablePoint(100f, 200f), ImmutablePoint(300f, 400f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(2.1f, 1f), ImmutableVec(31.6f, 10f))
+ val closeBox = ImmutableBox.fromTwoPoints(ImmutableVec(2f, 1f), ImmutableVec(-10f, -11f))
+ val farBox = ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
assertThat(rect.intersects(closeBox)).isFalse()
assertThat(rect.intersects(farBox)).isFalse()
@@ -696,10 +690,10 @@
@Test
fun intersects_whenBoxParallelogramIntersects_returnsTrue() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(2.1f, 1f), ImmutablePoint(31.6f, 10f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(2.1f, 1f), ImmutableVec(31.6f, 10f))
val parallelogramWithCommonVertex =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(26.6f, 8f),
+ center = ImmutableVec(26.6f, 8f),
width = 10f,
height = 4f,
rotation = Angle.ZERO,
@@ -707,7 +701,7 @@
)
val parallelogramWithCommonEdge =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(10f, 0f),
+ center = ImmutableVec(10f, 0f),
width = 10f,
height = 2f,
rotation = Angle.ZERO,
@@ -715,7 +709,7 @@
)
val intersectingParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(10f, 5f),
+ center = ImmutableVec(10f, 5f),
width = 6f,
height = 4f,
rotation = Angle.ZERO,
@@ -732,10 +726,10 @@
@Test
fun intersects_whenBoxParallelogramDoesNotIntersect_returnsFalse() {
- val rect = ImmutableBox.fromTwoPoints(ImmutablePoint(2.1f, 1f), ImmutablePoint(31.6f, 10f))
+ val rect = ImmutableBox.fromTwoPoints(ImmutableVec(2.1f, 1f), ImmutableVec(31.6f, 10f))
val closeParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(0f, 1f),
+ center = ImmutableVec(0f, 1f),
width = 4f,
height = 10f,
rotation = Angle.ZERO,
@@ -743,7 +737,7 @@
)
val farParallelogram =
ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
- center = ImmutablePoint(100f, 200f),
+ center = ImmutableVec(100f, 200f),
width = 0.6f,
height = 2.3f,
rotation = Angle.QUARTER_TURN_RADIANS,
@@ -756,8 +750,105 @@
assertThat(farParallelogram.intersects(rect)).isFalse()
}
+ @Test
+ fun intersects_forEqualsParallelograms_returnsTrue() {
+ val parallelogram1 =
+ ImmutableParallelogram.fromCenterAndDimensions(
+ center = ImmutableVec(0f, 1f),
+ width = 4f,
+ height = 10f,
+ )
+ val parallelogram2 =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(0f, 1f),
+ width = 4f,
+ height = 10f,
+ rotation = Angle.ZERO,
+ shearFactor = 0f,
+ )
+
+ assertThat(parallelogram1.intersects(parallelogram1)).isTrue()
+ assertThat(parallelogram1.intersects(parallelogram2)).isTrue()
+ assertThat(parallelogram2.intersects(parallelogram1)).isTrue()
+ }
+
+ @Test
+ fun intersects_whenParallelogramParallelogramIntersects_returnsTrue() {
+ val parallelogram =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(10f, 20f),
+ width = 6f,
+ height = 4f,
+ rotation = Angle.ZERO,
+ shearFactor = 0f,
+ )
+ val parallelogramWithCommonVertex =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(6f, 16f),
+ width = 2f,
+ height = 4f,
+ rotation = Angle.ZERO,
+ shearFactor = 0f,
+ )
+ val parallelogramWithCommonEdge =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(100f, 30f),
+ width = 200f,
+ height = 16f,
+ rotation = Angle.ZERO,
+ shearFactor = 0f,
+ )
+ val intersectingParallelogram =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ ImmutableVec(10f, 20f),
+ 2.9f,
+ 2.1f,
+ Angle.HALF_TURN_RADIANS / 4f,
+ 0f,
+ )
+
+ assertThat(parallelogram.intersects(parallelogramWithCommonVertex)).isTrue()
+ assertThat(parallelogram.intersects(parallelogramWithCommonEdge)).isTrue()
+ assertThat(parallelogram.intersects(intersectingParallelogram)).isTrue()
+ assertThat(parallelogramWithCommonVertex.intersects(parallelogram)).isTrue()
+ assertThat(parallelogramWithCommonEdge.intersects(parallelogram)).isTrue()
+ assertThat(intersectingParallelogram.intersects(parallelogram)).isTrue()
+ }
+
+ @Test
+ fun intersects_whenParallelogramParallelogramDoesNotIntersects_returnsFalse() {
+ val parallelogram =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(10f, 20f),
+ width = 6f,
+ height = 4f,
+ rotation = Angle.ZERO,
+ shearFactor = 0f,
+ )
+ val closeParallelogram =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(0.9f, 20f),
+ width = 12f,
+ height = 4f,
+ rotation = Angle.ZERO,
+ shearFactor = 0f,
+ )
+ val farParallelogram =
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ center = ImmutableVec(100f, 200f),
+ width = 0.6f,
+ height = 2.3f,
+ rotation = Angle.QUARTER_TURN_RADIANS,
+ shearFactor = 0f,
+ )
+
+ assertThat(parallelogram.intersects(closeParallelogram)).isFalse()
+ assertThat(parallelogram.intersects(farParallelogram)).isFalse()
+ assertThat(closeParallelogram.intersects(parallelogram)).isFalse()
+ assertThat(farParallelogram.intersects(parallelogram)).isFalse()
+ }
+
companion object {
- private val SCALE_TRANSFORM =
- ImmutableAffineTransform(a = 2f, b = 0f, c = 0f, d = 0f, e = 5f, f = 0f)
+ private val SCALE_TRANSFORM = ImmutableAffineTransform(2f, 0f, 0f, 0f, 5f, 0f)
}
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshAttributeUnpackingParamsTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshAttributeUnpackingParamsTest.kt
index c2011d9..590e4e3 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshAttributeUnpackingParamsTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshAttributeUnpackingParamsTest.kt
@@ -138,8 +138,7 @@
)
),
)
- val notATransform =
- ImmutableBox.fromTwoPoints(ImmutablePoint(2F, 4F), ImmutablePoint(1F, 3F))
+ val notATransform = ImmutableBox.fromTwoPoints(ImmutableVec(2F, 4F), ImmutableVec(1F, 3F))
for (transform in transforms) {
assertThat(transform).isNotEqualTo(notATransform)
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshTest.kt
index 5803e2b..6107970 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MeshTest.kt
@@ -95,9 +95,9 @@
fun fillPosition_shouldThrow() {
val mesh = Mesh()
- assertFailsWith<IllegalArgumentException> { mesh.fillPosition(-1, MutablePoint()) }
- assertFailsWith<IllegalArgumentException> { mesh.fillPosition(0, MutablePoint()) }
- assertFailsWith<IllegalArgumentException> { mesh.fillPosition(1, MutablePoint()) }
+ assertFailsWith<IllegalArgumentException> { mesh.fillPosition(-1, MutableVec()) }
+ assertFailsWith<IllegalArgumentException> { mesh.fillPosition(0, MutableVec()) }
+ assertFailsWith<IllegalArgumentException> { mesh.fillPosition(1, MutableVec()) }
}
@Test
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ModeledShapeTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ModeledShapeTest.kt
deleted file mode 100644
index 7b2ede5..0000000
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ModeledShapeTest.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ModeledShapeTest {
-
- @Test
- fun bounds_shouldBeEmpty() {
- val modeledShape = ModeledShape()
-
- assertThat(modeledShape.bounds).isNull()
- }
-
- @Test
- fun renderGroupCount_whenEmptyShape_shouldBeZero() {
- val modeledShape = ModeledShape()
-
- assertThat(modeledShape.renderGroupCount).isEqualTo(0)
- }
-
- @Test
- fun outlineCount_whenEmptyShape_shouldThrow() {
- val modeledShape = ModeledShape()
-
- assertFailsWith<IllegalArgumentException> { modeledShape.outlineCount(-1) }
- assertFailsWith<IllegalArgumentException> { modeledShape.outlineCount(0) }
- assertFailsWith<IllegalArgumentException> { modeledShape.outlineCount(1) }
- }
-
- @Test
- fun outlineVertexCount_whenEmptyShape_shouldThrow() {
- val modeledShape = ModeledShape()
-
- assertFailsWith<IllegalArgumentException> { modeledShape.outlineVertexCount(-1, 0) }
- assertFailsWith<IllegalArgumentException> { modeledShape.outlineVertexCount(0, 0) }
- assertFailsWith<IllegalArgumentException> { modeledShape.outlineVertexCount(1, 0) }
- }
-
- @Test
- fun fillOutlinePosition_whenEmptyShape_shouldThrow() {
- val modeledShape = ModeledShape()
-
- assertFailsWith<IllegalArgumentException> {
- modeledShape.fillOutlinePosition(-1, 0, 0, MutablePoint())
- }
- assertFailsWith<IllegalArgumentException> {
- modeledShape.fillOutlinePosition(0, 0, 0, MutablePoint())
- }
- assertFailsWith<IllegalArgumentException> {
- modeledShape.fillOutlinePosition(1, 0, 0, MutablePoint())
- }
- }
-
- @Test
- fun toString_returnsAString() {
- val string = ModeledShape().toString()
-
- // Not elaborate checks - this test mainly exists to ensure that toString doesn't crash.
- assertThat(string).contains("ModeledShape")
- assertThat(string).contains("bounds")
- assertThat(string).contains("meshes")
- assertThat(string).contains("nativeAddress")
- }
-}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableAffineTransformTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableAffineTransformTest.kt
index bc9d791..8717fd7 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableAffineTransformTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableAffineTransformTest.kt
@@ -106,6 +106,76 @@
}
@Test
+ fun setValuesAndGetValues_shouldRoundTrip() {
+ val affineTransform = MutableAffineTransform()
+ val values = floatArrayOf(1F, 2F, 3F, 4F, 5F, 6F)
+
+ affineTransform.setValues(values)
+ val outValues = FloatArray(6)
+ affineTransform.getValues(outValues)
+
+ assertThat(outValues).usingExactEquality().containsExactly(values)
+ }
+
+ @Test
+ fun constructWithValuesAndGetValues_shouldRoundTrip() {
+ val affineTransform = MutableAffineTransform(1F, 2F, 3F, 4F, 5F, 6F)
+
+ val outValues = FloatArray(6)
+ affineTransform.getValues(outValues)
+
+ assertThat(outValues)
+ .usingExactEquality()
+ .containsExactly(floatArrayOf(1F, 2F, 3F, 4F, 5F, 6F))
+ }
+
+ @Test
+ fun setValues_shouldMatchConstructedWithFactoryFunctions() {
+ assertThat(MutableAffineTransform().apply { setValues(7F, 0F, 0F, 0F, 7F, 0F) })
+ .isEqualTo(ImmutableAffineTransform.scale(7F))
+
+ assertThat(MutableAffineTransform().apply { setValues(3F, 0F, 0F, 0F, 5F, 0F) })
+ .isEqualTo(ImmutableAffineTransform.scale(3F, 5F))
+
+ assertThat(MutableAffineTransform().apply { setValues(4F, 0F, 0F, 0F, 1F, 0F) })
+ .isEqualTo(ImmutableAffineTransform.scaleX(4F))
+
+ assertThat(MutableAffineTransform().apply { setValues(1F, 0F, 0F, 0F, 2F, 0F) })
+ .isEqualTo(ImmutableAffineTransform.scaleY(2F))
+
+ assertThat(MutableAffineTransform().apply { setValues(1F, 0F, 8F, 0F, 1F, 9F) })
+ .isEqualTo(ImmutableAffineTransform.translate(ImmutableVec(8F, 9F)))
+ }
+
+ @Test
+ fun setValuesArray_shouldMatchConstructedWithFactoryFunctions() {
+ assertThat(
+ MutableAffineTransform().apply { setValues(floatArrayOf(7F, 0F, 0F, 0F, 7F, 0F)) }
+ )
+ .isEqualTo(ImmutableAffineTransform.scale(7F))
+
+ assertThat(
+ MutableAffineTransform().apply { setValues(floatArrayOf(3F, 0F, 0F, 0F, 5F, 0F)) }
+ )
+ .isEqualTo(ImmutableAffineTransform.scale(3F, 5F))
+
+ assertThat(
+ MutableAffineTransform().apply { setValues(floatArrayOf(4F, 0F, 0F, 0F, 1F, 0F)) }
+ )
+ .isEqualTo(ImmutableAffineTransform.scaleX(4F))
+
+ assertThat(
+ MutableAffineTransform().apply { setValues(floatArrayOf(1F, 0F, 0F, 0F, 2F, 0F)) }
+ )
+ .isEqualTo(ImmutableAffineTransform.scaleY(2F))
+
+ assertThat(
+ MutableAffineTransform().apply { setValues(floatArrayOf(1F, 0F, 8F, 0F, 1F, 9F)) }
+ )
+ .isEqualTo(ImmutableAffineTransform.translate(ImmutableVec(8F, 9F)))
+ }
+
+ @Test
fun asImmutable_returnsEquivalentImmutableAffineTransform() {
val affineTransform = MutableAffineTransform(A, B, C, D, E, F)
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableBoxTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableBoxTest.kt
index 94b695f..b44b015 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableBoxTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableBoxTest.kt
@@ -35,8 +35,8 @@
}
@Test
- fun fillFromCenterAndDimensions_correctlyModifiesMutableBox() {
- val rect = MutableBox().fillFromCenterAndDimensions(ImmutablePoint(20f, -50f), 10f, 20f)
+ fun populateFromCenterAndDimensions_correctlyModifiesMutableBox() {
+ val rect = MutableBox().populateFromCenterAndDimensions(ImmutableVec(20f, -50f), 10f, 20f)
assertThat(rect.xMin).isEqualTo(15f)
assertThat(rect.xMax).isEqualTo(25f)
@@ -47,9 +47,9 @@
}
@Test
- fun fillFromTwoPoints_correctlyModifiesMutableBox() {
+ fun populateFromTwoPoints_correctlyModifiesMutableBox() {
val rect =
- MutableBox().fillFromTwoPoints(MutablePoint(20f, -50f), ImmutablePoint(-70f, 100f))
+ MutableBox().populateFromTwoPoints(MutableVec(20f, -50f), ImmutableVec(-70f, 100f))
assertThat(rect.xMin).isEqualTo(-70f)
assertThat(rect.xMax).isEqualTo(20f)
@@ -61,7 +61,7 @@
@Test
fun minMaxFields_whenAllZeroes_allAreZero() {
- val zeroes = MutableBox().fillFromTwoPoints(ImmutablePoint(0F, 0F), ImmutablePoint(0F, 0F))
+ val zeroes = MutableBox().populateFromTwoPoints(ImmutableVec(0F, 0F), ImmutableVec(0F, 0F))
assertThat(zeroes.xMin).isEqualTo(0F)
assertThat(zeroes.yMin).isEqualTo(0F)
assertThat(zeroes.xMax).isEqualTo(0F)
@@ -71,7 +71,7 @@
@Test
fun minMaxFields_whenDeclaredInMinMaxOrder_matchOrder() {
val inOrder =
- MutableBox().fillFromTwoPoints(ImmutablePoint(-1F, -2F), ImmutablePoint(3F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-1F, -2F), ImmutableVec(3F, 4F))
assertThat(inOrder.xMin).isEqualTo(-1F)
assertThat(inOrder.yMin).isEqualTo(-2F)
assertThat(inOrder.xMax).isEqualTo(3F)
@@ -81,7 +81,7 @@
@Test
fun minMaxFields_whenDeclaredOutOfOrder_doNotMatchOrder() {
val outOfOrder =
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(-3F, -4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(-3F, -4F))
assertThat(outOfOrder.xMin).isEqualTo(-3F)
assertThat(outOfOrder.yMin).isEqualTo(-4F)
assertThat(outOfOrder.xMax).isEqualTo(1F)
@@ -90,7 +90,7 @@
@Test
fun widthHeight_whenAllZeroes_areAllZero() {
- val zeroes = MutableBox().fillFromTwoPoints(ImmutablePoint(0F, 0F), ImmutablePoint(0F, 0F))
+ val zeroes = MutableBox().populateFromTwoPoints(ImmutableVec(0F, 0F), ImmutableVec(0F, 0F))
assertThat(zeroes.width).isEqualTo(0)
assertThat(zeroes.height).isEqualTo(0)
@@ -99,7 +99,7 @@
@Test
fun widthHeight_whenDeclaredInOrder_areCorrectValues() {
val inOrder =
- MutableBox().fillFromTwoPoints(ImmutablePoint(-1F, -2F), ImmutablePoint(3F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-1F, -2F), ImmutableVec(3F, 4F))
assertThat(inOrder.width).isEqualTo(4F)
assertThat(inOrder.height).isEqualTo(6F)
@@ -108,7 +108,7 @@
@Test
fun widthHeight_whenDeclaredOutOfOrder_areCorrectValues() {
val outOfOrder =
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(-3F, -4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(-3F, -4F))
assertThat(outOfOrder.width).isEqualTo(4F)
assertThat(outOfOrder.height).isEqualTo(6F)
@@ -116,9 +116,9 @@
@Test
fun widthHeight_whenValuesChanged_areCorrectValues() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(-3F, -4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(-3F, -4F))
- rect.fillFromTwoPoints(MutablePoint(-20f, -5f), ImmutablePoint(30f, 7f))
+ rect.populateFromTwoPoints(MutableVec(-20f, -5f), ImmutableVec(30f, 7f))
assertThat(rect.width).isEqualTo(50F)
assertThat(rect.height).isEqualTo(12F)
@@ -126,66 +126,66 @@
@Test
fun setXBounds_whenInOrder_changesXMinAndXMax() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
rect.setXBounds(5F, 7F)
assertThat(rect)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(5F, 2F), ImmutablePoint(7F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(5F, 2F), ImmutableVec(7F, 4F))
)
}
@Test
fun setXBounds_whenNotInOrder_changesXMinAndXMax() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
rect.setXBounds(7F, 5F)
assertThat(rect)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(5F, 2F), ImmutablePoint(7F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(5F, 2F), ImmutableVec(7F, 4F))
)
}
@Test
fun setYBounds_whenInOrder_changesXMinAndXMax() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
rect.setYBounds(6F, 8F)
assertThat(rect)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 6F), ImmutablePoint(3F, 8F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 6F), ImmutableVec(3F, 8F))
)
}
@Test
fun setYBounds_whenNotInOrder_changesXMinAndXMax() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
rect.setYBounds(8F, 6F)
assertThat(rect)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 6F), ImmutablePoint(3F, 8F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 6F), ImmutableVec(3F, 8F))
)
}
@Test
fun populateFrom_correctlyPopulatesFromBox() {
- val source = ImmutableBox.fromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val source = ImmutableBox.fromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
val dest = MutableBox().populateFrom(source)
assertThat(dest)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
)
}
@Test
fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(rect).isEqualTo(rect)
assertThat(rect.hashCode()).isEqualTo(rect.hashCode())
@@ -193,15 +193,15 @@
@Test
fun equals_whenDifferentType_returnsFalse() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
- assertThat(rect).isNotEqualTo(ImmutablePoint(1F, 2F))
+ assertThat(rect).isNotEqualTo(ImmutableVec(1F, 2F))
}
@Test
fun equals_whenSameValues_returnsTrueAndSameHashCode() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- val other = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
+ val other = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(rect).isEqualTo(other)
assertThat(rect.hashCode()).isEqualTo(other.hashCode())
@@ -209,8 +209,8 @@
@Test
fun equals_whenSameValuesOutOfOrder_returnsTrueAndSameHashCode() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
- val other = MutableBox().fillFromTwoPoints(ImmutablePoint(3F, 4F), ImmutablePoint(1F, 2F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
+ val other = MutableBox().populateFromTwoPoints(ImmutableVec(3F, 4F), ImmutableVec(1F, 2F))
assertThat(rect).isEqualTo(other)
assertThat(rect.hashCode()).isEqualTo(other.hashCode())
@@ -218,63 +218,49 @@
@Test
fun equals_whenDifferentXMin_returnsFalse() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(rect)
.isNotEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(-1F, 2F), ImmutablePoint(3F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-1F, 2F), ImmutableVec(3F, 4F))
)
}
@Test
fun equals_whenDifferentYMin_returnsFalse() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(rect)
.isNotEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, -2F), ImmutablePoint(3F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, -2F), ImmutableVec(3F, 4F))
)
}
@Test
fun equals_whenDifferentXMax_returnsFalse() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(rect)
.isNotEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(30F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(30F, 4F))
)
}
@Test
fun equals_whenDifferentYMax_returnsFalse() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
assertThat(rect)
.isNotEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 40F))
- )
- }
-
- @Test
- fun copy_returnsEqualValueThatCannotModifyOriginal() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
-
- val copy = rect.copy()
- assertThat(copy).isEqualTo(rect)
-
- copy.fillFromTwoPoints(ImmutablePoint(5F, 6F), ImmutablePoint(7F, 8F))
- assertThat(rect)
- .isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 40F))
)
}
@Test
fun overwriteFromValues_whenInOrder_changesAllValues() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
- rect.fillFromTwoPoints(ImmutablePoint(5F, 6F), ImmutablePoint(7F, 8F))
+ rect.populateFromTwoPoints(ImmutableVec(5F, 6F), ImmutableVec(7F, 8F))
assertThat(rect.xMin).isEqualTo(5F)
assertThat(rect.yMin).isEqualTo(6F)
@@ -284,9 +270,9 @@
@Test
fun overwriteFromValues_whenOutOfOrder_changesAllValues() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 2F), ImmutablePoint(3F, 4F))
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 2F), ImmutableVec(3F, 4F))
- rect.fillFromTwoPoints(ImmutablePoint(-1F, -2F), ImmutablePoint(-3F, -4F))
+ rect.populateFromTwoPoints(ImmutableVec(-1F, -2F), ImmutableVec(-3F, -4F))
assertThat(rect.xMin).isEqualTo(-3F)
assertThat(rect.yMin).isEqualTo(-4F)
@@ -295,35 +281,35 @@
}
@Test
- fun center_modifiesMutablePoint() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 20F), ImmutablePoint(3F, 40F))
- val outCenter = MutablePoint()
- rect.center(outCenter)
+ fun populateCenter_modifiesMutableVec() {
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 20F), ImmutableVec(3F, 40F))
+ val outCenter = MutableVec()
+ rect.computeCenter(outCenter)
- assertThat(outCenter).isEqualTo(MutablePoint(2F, 30F))
+ assertThat(outCenter).isEqualTo(MutableVec(2F, 30F))
}
@Test
fun corners_modifiesMutablePoints() {
- val rect = MutableBox().fillFromTwoPoints(ImmutablePoint(1F, 20F), ImmutablePoint(3F, 40F))
- val p0 = MutablePoint()
- val p1 = MutablePoint()
- val p2 = MutablePoint()
- val p3 = MutablePoint()
- rect.corners(p0, p1, p2, p3)
+ val rect = MutableBox().populateFromTwoPoints(ImmutableVec(1F, 20F), ImmutableVec(3F, 40F))
+ val p0 = MutableVec()
+ val p1 = MutableVec()
+ val p2 = MutableVec()
+ val p3 = MutableVec()
+ rect.computeCorners(p0, p1, p2, p3)
- assertThat(p0).isEqualTo(MutablePoint(1F, 20F))
- assertThat(p1).isEqualTo(MutablePoint(3F, 20F))
- assertThat(p2).isEqualTo(MutablePoint(3F, 40F))
- assertThat(p3).isEqualTo(MutablePoint(1F, 40F))
+ assertThat(p0).isEqualTo(MutableVec(1F, 20F))
+ assertThat(p1).isEqualTo(MutableVec(3F, 20F))
+ assertThat(p2).isEqualTo(MutableVec(3F, 40F))
+ assertThat(p3).isEqualTo(MutableVec(1F, 40F))
}
@Test
- fun contains_returnsCorrectValuesWithPoint() {
+ fun contains_returnsCorrectValuesWithVec() {
val rect =
- MutableBox().fillFromTwoPoints(ImmutablePoint(10F, 600F), ImmutablePoint(40F, 900F))
- val innerPoint = ImmutablePoint(30F, 700F)
- val outerPoint = ImmutablePoint(70F, 2000F)
+ MutableBox().populateFromTwoPoints(ImmutableVec(10F, 600F), ImmutableVec(40F, 900F))
+ val innerPoint = ImmutableVec(30F, 700F)
+ val outerPoint = ImmutableVec(70F, 2000F)
assertThat(rect.contains(innerPoint)).isTrue()
assertThat(rect.contains(outerPoint)).isFalse()
@@ -332,9 +318,9 @@
@Test
fun contains_returnsCorrectValuesWithBox() {
val outerRect =
- MutableBox().fillFromTwoPoints(ImmutablePoint(10F, 600F), ImmutablePoint(40F, 900F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(10F, 600F), ImmutableVec(40F, 900F))
val innerRect =
- MutableBox().fillFromTwoPoints(ImmutablePoint(20F, 700F), ImmutablePoint(30F, 800F))
+ MutableBox().populateFromTwoPoints(ImmutableVec(20F, 700F), ImmutableVec(30F, 800F))
assertThat(outerRect.contains(innerRect)).isTrue()
assertThat(innerRect.contains(outerRect)).isFalse()
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableParallelogramTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableParallelogramTest.kt
index 9d737bd..da24a51 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableParallelogramTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableParallelogramTest.kt
@@ -28,7 +28,7 @@
fun defaultConstructor_constructsCorrectMutableParallelogram() {
val parallelogram = MutableParallelogram()
- assertThat(parallelogram.center).isEqualTo(MutablePoint(0f, 0f))
+ assertThat(parallelogram.center).isEqualTo(MutableVec(0f, 0f))
assertThat(parallelogram.width).isZero()
assertThat(parallelogram.height).isZero()
assertThat(parallelogram.rotation).isZero()
@@ -38,7 +38,7 @@
@Test
fun setCenter_changesCenter() {
val parallelogram = MutableParallelogram()
- val newCenter = MutablePoint(5f, -2f)
+ val newCenter = MutableVec(5f, -2f)
parallelogram.center = newCenter
assertThat(parallelogram.center).isEqualTo(newCenter)
}
@@ -47,7 +47,7 @@
fun setWidth_toNegativeValue_forcesNormalizationOfParallelogram() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsAndRotation(
- MutablePoint(10f, 0f),
+ MutableVec(10f, 0f),
6f,
4f,
Angle.QUARTER_TURN_RADIANS,
@@ -65,7 +65,7 @@
fun setRotation_toOutOfRangeNormalRange_forcesNormalizationOfAngle() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsAndRotation(
- MutablePoint(10f, 0f),
+ MutableVec(10f, 0f),
6f,
4f,
Angle.QUARTER_TURN_RADIANS,
@@ -77,9 +77,9 @@
@Test
fun fromCenterAndDimensions_constructsCorrectMutableParallelogram() {
val parallelogram =
- MutableParallelogram.fromCenterAndDimensions(MutablePoint(10f, 0f), 6f, 4f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(10f, 0f), 6f, 4f)
- assertThat(parallelogram.center).isEqualTo(MutablePoint(10f, 0f))
+ assertThat(parallelogram.center).isEqualTo(MutableVec(10f, 0f))
assertThat(parallelogram.width).isEqualTo(6f)
assertThat(parallelogram.height).isEqualTo(4f)
assertThat(parallelogram.rotation).isZero()
@@ -89,9 +89,9 @@
@Test
fun fromCenterAndDimensions_forNegativeWidth_constructsCorrectMutableParallelogram() {
val parallelogramWithNegativeWidth =
- MutableParallelogram.fromCenterAndDimensions(MutablePoint(10f, 0f), -6f, 4f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(10f, 0f), -6f, 4f)
- assertThat(parallelogramWithNegativeWidth.center).isEqualTo(MutablePoint(10f, 0f))
+ assertThat(parallelogramWithNegativeWidth.center).isEqualTo(MutableVec(10f, 0f))
assertThat(parallelogramWithNegativeWidth.width).isEqualTo(6f)
assertThat(parallelogramWithNegativeWidth.height).isEqualTo(-4f)
assertThat(parallelogramWithNegativeWidth.rotation).isEqualTo(Math.PI.toFloat())
@@ -102,13 +102,13 @@
fun fromCenterDimensionsAndRotation_constructsCorrectMutableParallelogram() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsAndRotation(
- MutablePoint(10f, 0f),
+ MutableVec(10f, 0f),
6f,
4f,
Angle.FULL_TURN_RADIANS,
)
- assertThat(parallelogram.center).isEqualTo(MutablePoint(10f, 0f))
+ assertThat(parallelogram.center).isEqualTo(MutableVec(10f, 0f))
assertThat(parallelogram.width).isEqualTo(6f)
assertThat(parallelogram.height).isEqualTo(4f)
assertThat(parallelogram.rotation).isZero()
@@ -119,13 +119,13 @@
fun fromCenterDimensionsAndRotation_forNegativeWidth_constructsCorrectMutableParallelogram() {
val parallelogramWithNegativeWidth =
MutableParallelogram.fromCenterDimensionsAndRotation(
- MutablePoint(10f, 0f),
+ MutableVec(10f, 0f),
-6f,
4f,
Angle.FULL_TURN_RADIANS,
)
- assertThat(parallelogramWithNegativeWidth.center).isEqualTo(MutablePoint(10f, 0f))
+ assertThat(parallelogramWithNegativeWidth.center).isEqualTo(MutableVec(10f, 0f))
assertThat(parallelogramWithNegativeWidth.width).isEqualTo(6f)
assertThat(parallelogramWithNegativeWidth.height).isEqualTo(-4f)
assertThat(parallelogramWithNegativeWidth.rotation).isWithin(1e-6f).of(Math.PI.toFloat())
@@ -136,14 +136,14 @@
fun fromCenterDimensionsRotationAndShear_constructsCorrectMutableParallelogram() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(10f, 0f),
+ MutableVec(10f, 0f),
6f,
4f,
Angle.HALF_TURN_RADIANS,
1f,
)
- assertThat(parallelogram.center).isEqualTo(MutablePoint(10f, 0f))
+ assertThat(parallelogram.center).isEqualTo(MutableVec(10f, 0f))
assertThat(parallelogram.width).isEqualTo(6f)
assertThat(parallelogram.height).isEqualTo(4f)
assertThat(parallelogram.rotation).isWithin(1e-6f).of(Math.PI.toFloat())
@@ -154,14 +154,14 @@
fun fromCenterDimensionsRotationAndShear_forNegativeWidth_constructsCorrectMutableParallelogram() {
val parallelogramWithNegativeWidth =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(10f, 0f),
+ MutableVec(10f, 0f),
-6f,
4f,
Angle.FULL_TURN_RADIANS,
1f,
)
- assertThat(parallelogramWithNegativeWidth.center).isEqualTo(MutablePoint(10f, 0f))
+ assertThat(parallelogramWithNegativeWidth.center).isEqualTo(MutableVec(10f, 0f))
assertThat(parallelogramWithNegativeWidth.width).isEqualTo(6f)
assertThat(parallelogramWithNegativeWidth.height).isEqualTo(-4f)
assertThat(parallelogramWithNegativeWidth.rotation).isWithin(1e-6f).of(Math.PI.toFloat())
@@ -172,7 +172,7 @@
fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(10f, 10f),
+ MutableVec(10f, 10f),
12f,
2f,
Angle.HALF_TURN_RADIANS,
@@ -186,7 +186,7 @@
fun equals_whenSameValues_returnsTrueAndSameHashCode() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -194,7 +194,7 @@
)
val other =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -210,13 +210,13 @@
// An axis-aligned rectangle with center at (0,0) and width and height equal to 2
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(0f, 0f),
+ MutableVec(0f, 0f),
2f,
2f,
Angle.ZERO,
0f,
)
- val other = MutableBox().fillFromTwoPoints(ImmutablePoint(-1f, -1f), ImmutablePoint(1f, 1f))
+ val other = MutableBox().populateFromTwoPoints(ImmutableVec(-1f, -1f), ImmutableVec(1f, 1f))
assertThat(parallelogram).isNotEqualTo(other)
}
@@ -225,7 +225,7 @@
fun equals_whenDifferentCenter_returnsFalse() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -233,7 +233,7 @@
)
val other =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(10f, -10.5f),
+ MutableVec(10f, -10.5f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -247,7 +247,7 @@
fun equals_whenDifferentWidth_returnsFalse() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
11f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -255,7 +255,7 @@
)
val other =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -269,7 +269,7 @@
fun equals_whenDifferentHeight_returnsFalse() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -277,7 +277,7 @@
)
val other =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
7.5f,
Angle.HALF_TURN_RADIANS,
@@ -291,7 +291,7 @@
fun equals_whenDifferentRotation_returnsFalse() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -299,7 +299,7 @@
)
val other =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.QUARTER_TURN_RADIANS,
@@ -313,7 +313,7 @@
fun equals_whenDifferentShearFactor_returnsFalse() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -321,7 +321,7 @@
)
val other =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(-10f, 10f),
+ MutableVec(-10f, 10f),
12f,
-7.5f,
Angle.HALF_TURN_RADIANS,
@@ -335,14 +335,14 @@
fun getters_returnCorrectValues() {
val parallelogram =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- MutablePoint(3f, -5f),
+ MutableVec(3f, -5f),
8f,
-1f,
Angle.HALF_TURN_RADIANS,
0f,
)
- assertThat(parallelogram.center).isEqualTo(MutablePoint(3f, -5f))
+ assertThat(parallelogram.center).isEqualTo(MutableVec(3f, -5f))
assertThat(parallelogram.width).isEqualTo(8f)
assertThat(parallelogram.height).isEqualTo(-1f)
assertThat(parallelogram.rotation).isEqualTo(Angle.HALF_TURN_RADIANS)
@@ -352,22 +352,22 @@
@Test
fun signedArea_returnsCorrectValue() {
val parallelogram =
- MutableParallelogram.fromCenterAndDimensions(MutablePoint(0f, 10f), 6f, 4f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(0f, 10f), 6f, 4f)
val degenerateParallelogram =
- MutableParallelogram.fromCenterAndDimensions(MutablePoint(0f, 10f), 0f, 4f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(0f, 10f), 0f, 4f)
val negativeAreaParallelogram =
- MutableParallelogram.fromCenterAndDimensions(MutablePoint(0f, 10f), 2f, -3f)
+ MutableParallelogram.fromCenterAndDimensions(MutableVec(0f, 10f), 2f, -3f)
- assertThat(parallelogram.signedArea()).isEqualTo(24f)
- assertThat(degenerateParallelogram.signedArea()).isZero()
- assertThat(negativeAreaParallelogram.signedArea()).isEqualTo(-6f)
+ assertThat(parallelogram.computeSignedArea()).isEqualTo(24f)
+ assertThat(degenerateParallelogram.computeSignedArea()).isZero()
+ assertThat(negativeAreaParallelogram.computeSignedArea()).isEqualTo(-6f)
}
@Test
fun toString_returnsCorrectValue() {
val parallelogramString =
MutableParallelogram.fromCenterDimensionsRotationAndShear(
- ImmutablePoint(3f, -5f),
+ MutableVec(3f, -5f),
8f,
-1f,
Angle.HALF_TURN_RADIANS,
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutablePointTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutablePointTest.kt
deleted file mode 100644
index 2279b4b..0000000
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutablePointTest.kt
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class MutablePointTest {
-
- @Test
- fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
- val point = MutablePoint(1f, 2f)
-
- assertThat(point).isEqualTo(point)
- assertThat(point.hashCode()).isEqualTo(point.hashCode())
- }
-
- @Test
- fun equals_whenDifferentType_returnsFalse() {
- val point = MutablePoint(1f, 2f)
- val vec = ImmutableVec(1f, 2f)
-
- assertThat(point).isNotEqualTo(vec)
- }
-
- @Test
- fun equals_whenSameInterface_returnsTrue() {
- val point = MutablePoint(1f, 2f)
- val other = ImmutablePoint(1f, 2f)
-
- assertThat(point).isEqualTo(other)
- }
-
- @Test
- fun equals_whenSameValues_returnsTrueAndSameHashCode() {
- val point = MutablePoint(-3f, 1.2f)
- val other = MutablePoint(-3f, 1.2f)
-
- assertThat(point).isEqualTo(other)
- assertThat(point.hashCode()).isEqualTo(other.hashCode())
- }
-
- @Test
- fun equals_whenFlippedValues_returnsFalse() {
- val point = MutablePoint(10f, 2134f)
- val other = MutablePoint(2134f, 10f)
-
- assertThat(point).isNotEqualTo(other)
- }
-
- @Test
- fun getters_returnCorrectValues() {
- val point = MutablePoint(10f, 2134f)
-
- assertThat(point.x).isEqualTo(10f)
- assertThat(point.y).isEqualTo(2134f)
- }
-
- @Test
- fun setters_gettersReturnNewValues() {
- val point = MutablePoint(99f, 1234f)
-
- point.x = 10f
- point.y = 2134f
-
- assertThat(point.x).isEqualTo(10f)
- assertThat(point.y).isEqualTo(2134f)
- }
-
- @Test
- fun build_returnsPointWithSameValues() {
- val point = MutablePoint(10f, 2134f)
-
- val builtPoint = point.build()
- assertThat(builtPoint).isEqualTo(ImmutablePoint(10f, 2134f))
- }
-
- @Test
- fun add_withPointThenVec_correctlyAddsAndFillsAndDoesntMutateInputs() {
- val point = MutablePoint(10f, 40f)
- val vec = MutableVec(5f, -2f)
- val output = MutablePoint()
-
- Point.add(point, vec, output)
-
- assertThat(output).isEqualTo(MutablePoint(15f, 38f))
- assertThat(point).isEqualTo(MutablePoint(10f, 40f))
- assertThat(vec).isEqualTo(MutableVec(5f, -2f))
- }
-
- @Test
- fun add_withVecThenPoint_correctlyAddsAndFillsAndDoesntMutateInputs() {
- val point = MutablePoint(10f, 40f)
- val vec = MutableVec(5f, -2f)
- val output = MutablePoint()
-
- Point.add(vec, point, output)
-
- assertThat(output).isEqualTo(MutablePoint(15f, 38f))
- assertThat(point).isEqualTo(MutablePoint(10f, 40f))
- assertThat(vec).isEqualTo(MutableVec(5f, -2f))
- }
-
- @Test
- fun subtract_pointMinusVec_correctlySubtractsAndFillsAndDoesntMutateInputs() {
- val point = MutablePoint(10f, 40f)
- val vec = MutableVec(5f, -2f)
- val output = MutablePoint()
-
- Point.subtract(point, vec, output)
-
- assertThat(output).isEqualTo(MutablePoint(5f, 42f))
- assertThat(point).isEqualTo(MutablePoint(10f, 40f))
- assertThat(vec).isEqualTo(MutableVec(5f, -2f))
- }
-
- @Test
- fun subtract_pointMinusPoint_correctlySubtractsAndFillsAndDoesntMutateInputs() {
- val lhsPoint = MutablePoint(10f, 40f)
- val rhsPoint = MutablePoint(5f, -2f)
- val output = MutableVec()
-
- Point.subtract(lhsPoint, rhsPoint, output)
-
- assertThat(output).isEqualTo(MutableVec(5f, 42f))
- assertThat(lhsPoint).isEqualTo(MutablePoint(10f, 40f))
- assertThat(rhsPoint).isEqualTo(MutablePoint(5f, -2f))
- }
-}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableSegmentTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableSegmentTest.kt
index 39320e1..c3ce145 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableSegmentTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableSegmentTest.kt
@@ -28,18 +28,20 @@
fun vec_whenPrimaryValuesAreUnchanged_returnsCorrectImmutableVec() {
val segment = MutableSegment(MutableVec(0f, 0f), MutableVec(1f, 2f))
- assertThat(segment.vec).isEqualTo(ImmutableVec(1f, 2f))
+ assertThat(segment.computeDisplacement()).isEqualTo(ImmutableVec(1f, 2f))
}
@Test
fun vec_whenPrimaryValuesAreModified_returnsDifferentImmutableVec() {
val segment = MutableSegment(MutableVec(10f, 50f), MutableVec(1f, 2f))
- segment.start(0f, 0f)
- assertThat(segment.vec).isEqualTo(ImmutableVec(1f, 2f))
+ segment.start.x = 0f
+ segment.start.y = 0f
+ assertThat(segment.computeDisplacement()).isEqualTo(ImmutableVec(1f, 2f))
- segment.end(-.005f, -456f)
- assertThat(segment.vec).isEqualTo(ImmutableVec(-.005f, -456f))
+ segment.end.x = -.005f
+ segment.end.y = -456f
+ assertThat(segment.computeDisplacement()).isEqualTo(ImmutableVec(-.005f, -456f))
}
@Test
@@ -90,42 +92,6 @@
}
@Test
- fun start_correctlyModifiesStartValue() {
- val segment = MutableSegment(MutableVec(10f, 20f), MutableVec(1f, 2f))
-
- segment.start(ImmutableVec(1.5f, 21.6f))
-
- assertThat(segment.start).isEqualTo(MutableVec(1.5f, 21.6f))
- }
-
- @Test
- fun start_withXYArgs_correctlyModifiesStartValue() {
- val segment = MutableSegment(MutableVec(10f, 20f), MutableVec(1f, 2f))
-
- segment.start(x = 1.5f, y = 21.6f)
-
- assertThat(segment.start).isEqualTo(MutableVec(1.5f, 21.6f))
- }
-
- @Test
- fun end_correctlyModifiesEndValue() {
- val segment = MutableSegment(MutableVec(10f, 20f), MutableVec(1f, 2f))
-
- segment.end(ImmutableVec(-1.5f, -21.6f))
-
- assertThat(segment.end).isEqualTo(MutableVec(-1.5f, -21.6f))
- }
-
- @Test
- fun end_withXYArgs_correctlyModifiesEndValue() {
- val segment = MutableSegment(MutableVec(10f, 20f), MutableVec(1f, 2f))
-
- segment.end(x = -1.5f, y = -21.6f)
-
- assertThat(segment.end).isEqualTo(MutableVec(-1.5f, -21.6f))
- }
-
- @Test
fun asImmutable_returnsImmutableCopy() {
val start = MutableVec(10f, 20f)
val end = MutableVec(1f, 2f)
@@ -137,18 +103,7 @@
}
@Test
- fun asImmutable_withNewValues_ReturnsNewImmutable() {
- val segment = MutableSegment(MutableVec(0f, 0f), MutableVec(-100f, -200f))
- val newStart = ImmutableVec(10f, 20f)
- val newEnd = ImmutableVec(30f, 40f)
- val output = segment.asImmutable(newStart, newEnd)
-
- assertThat(output.start).isEqualTo(newStart)
- assertThat(output.end).isEqualTo(newEnd)
- }
-
- @Test
- fun isAlmostEqual_usesTolereneceToCompareValues() {
+ fun isAlmostEqual_usesToleranceToCompareValues() {
val segment = MutableSegment(MutableVec(1f, 2f), MutableVec(3f, 4f))
val other = MutableSegment(MutableVec(1.01f, 2.02f), MutableVec(3.03f, 4.04f))
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableTriangleTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableTriangleTest.kt
index 9984cde..028443d 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableTriangleTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableTriangleTest.kt
@@ -24,9 +24,13 @@
@RunWith(JUnit4::class)
class MutableTriangleTest {
+ private val p0 = MutableVec(1f, 2f)
+ private val p1 = MutableVec(5f, 2f)
+ private val p2 = MutableVec(5f, 6f)
+
@Test
fun equals_whenSameInstance_returnsTrueAndSameHashCode() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), P2.newMutable())
+ val triangle = MutableTriangle(p0, p1, p2)
// Ensure test coverage of the same-instance case, but call .equals directly for lint.
assertThat(triangle.equals(triangle)).isTrue()
@@ -34,8 +38,9 @@
@Test
fun equals_whenSameValues_returnsTrueAndSameHashCode() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), P2.newMutable())
- val other = MutableTriangle(P0.newMutable(), P1.newMutable(), P2.newMutable())
+ val triangle = MutableTriangle(p0, p1, p2)
+ val other =
+ MutableTriangle(MutableVec(p0.x, p0.y), MutableVec(p1.x, p1.y), MutableVec(p2.x, p2.y))
assertThat(triangle).isEqualTo(other)
assertThat(triangle.hashCode()).isEqualTo(other.hashCode())
@@ -43,11 +48,9 @@
@Test
fun equals_whenPermutedEndpoints_returnsFalse() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), P2.newMutable())
- val clockWisePermutation =
- MutableTriangle(P1.newMutable(), P2.newMutable(), P0.newMutable())
- val counterClockWisePermutation =
- MutableTriangle(P2.newMutable(), P0.newMutable(), P1.newMutable())
+ val triangle = MutableTriangle(p0, p1, p2)
+ val clockWisePermutation = MutableTriangle(p1, p2, p0)
+ val counterClockWisePermutation = MutableTriangle(p2, p0, p1)
assertThat(triangle).isNotEqualTo(clockWisePermutation)
assertThat(triangle).isNotEqualTo(counterClockWisePermutation)
@@ -55,9 +58,9 @@
@Test
fun equals_whenP0different_returnsFalse() {
- val triangle = MutableTriangle(MutableVec(1f, 2f), P1.newMutable(), P2.newMutable())
- val p0XChange = MutableTriangle(MutableVec(1.23f, 2f), P1.newMutable(), P2.newMutable())
- val p0YChange = MutableTriangle(MutableVec(1f, 21.1f), P1.newMutable(), P2.newMutable())
+ val triangle = MutableTriangle(MutableVec(1f, 2f), p1, p2)
+ val p0XChange = MutableTriangle(MutableVec(1.23f, 2f), p1, p2)
+ val p0YChange = MutableTriangle(MutableVec(1f, 21.1f), p1, p2)
assertThat(triangle).isNotEqualTo(p0XChange)
assertThat(triangle).isNotEqualTo(p0YChange)
@@ -65,9 +68,9 @@
@Test
fun equals_whenP1different_returnsFalse() {
- val triangle = MutableTriangle(P0.newMutable(), MutableVec(3f, 4f), P2.newMutable())
- val p1XChange = MutableTriangle(P0.newMutable(), MutableVec(41.21f, 4f), P2.newMutable())
- val p1YChange = MutableTriangle(P0.newMutable(), MutableVec(3f, -6.77f), P2.newMutable())
+ val triangle = MutableTriangle(p0, MutableVec(3f, 4f), p2)
+ val p1XChange = MutableTriangle(p0, MutableVec(41.21f, 4f), p2)
+ val p1YChange = MutableTriangle(p0, MutableVec(3f, -6.77f), p2)
assertThat(triangle).isNotEqualTo(p1XChange)
assertThat(triangle).isNotEqualTo(p1YChange)
@@ -75,71 +78,17 @@
@Test
fun equals_whenP2different_returnsFalse() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), MutableVec(5f, 6f))
- val p2XChange = MutableTriangle(P0.newMutable(), P1.newMutable(), MutableVec(-0.43f, 6f))
- val p2YChange = MutableTriangle(P0.newMutable(), P1.newMutable(), MutableVec(5f, -10f))
+ val triangle = MutableTriangle(p0, p1, MutableVec(5f, 6f))
+ val p2XChange = MutableTriangle(p0, p1, MutableVec(-0.43f, 6f))
+ val p2YChange = MutableTriangle(p0, p1, MutableVec(5f, -10f))
assertThat(triangle).isNotEqualTo(p2XChange)
assertThat(triangle).isNotEqualTo(p2YChange)
}
@Test
- fun p0_correctlyModifiesP0Value() {
- val triangle = MutableTriangle(MutableVec(1f, 2f), P1.newMutable(), P2.newMutable())
-
- triangle.p0(MutableVec(1.5f, 21.6f))
-
- assertThat(triangle.p0).isEqualTo(MutableVec(1.5f, 21.6f))
- }
-
- @Test
- fun p0_withXYArgs_correctlyModifiesP0Value() {
- val triangle = MutableTriangle(MutableVec(1f, 2f), P1.newMutable(), P2.newMutable())
-
- triangle.p0(x = 1.5f, y = 21.6f)
-
- assertThat(triangle.p0).isEqualTo(MutableVec(1.5f, 21.6f))
- }
-
- @Test
- fun p1_correctlyModifiesP1Value() {
- val triangle = MutableTriangle(P0.newMutable(), MutableVec(3f, 4f), P2.newMutable())
-
- triangle.p1(MutableVec(20.9f, 513f))
-
- assertThat(triangle.p1).isEqualTo(MutableVec(20.9f, 513f))
- }
-
- @Test
- fun p1_withXYArgs_correctlyModifiesP1Value() {
- val triangle = MutableTriangle(P0.newMutable(), MutableVec(3f, 4f), P2.newMutable())
-
- triangle.p1(x = 20.9f, y = 513f)
-
- assertThat(triangle.p1).isEqualTo(MutableVec(20.9f, 513f))
- }
-
- @Test
- fun p2_correctlyModifiesP2Value() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), MutableVec(5f, 6f))
-
- triangle.p2(MutableVec(600f, 900f))
-
- assertThat(triangle.p2).isEqualTo(MutableVec(600f, 900f))
- }
-
- @Test
- fun p2_withXYArgs_correctlyModifiesP2Value() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), MutableVec(5f, 6f))
-
- triangle.p2(x = 600f, y = 900f)
-
- assertThat(triangle.p2).isEqualTo(MutableVec(600f, 900f))
- }
-
- @Test
fun populateFrom_correctlyCopiesValues() {
- val triangle = MutableTriangle(P0.newMutable(), P1.newMutable(), P2.newMutable())
+ val triangle = MutableTriangle(p0, p1, p2)
val other =
ImmutableTriangle(
ImmutableVec(10f, 11f),
@@ -156,7 +105,7 @@
@Test
fun contains_forContainedPoint_returnsTrue() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val point = MutableVec(4f, 3f)
assertThat(triangle.contains(point)).isTrue()
@@ -164,7 +113,7 @@
@Test
fun contains_forExternalPoint_returnsFalse() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val point = MutableVec(6f, 3f)
assertThat(triangle.contains(point)).isFalse()
@@ -172,73 +121,60 @@
@Test
fun edge_returnsCorrectSegment() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
- assertThat(triangle.edge(0)).isEqualTo(MutableSegment(P0, P1))
- assertThat(triangle.edge(1)).isEqualTo(MutableSegment(P1, P2))
- assertThat(triangle.edge(2)).isEqualTo(MutableSegment(P2, P0))
- assertThat(triangle.edge(3)).isEqualTo(MutableSegment(P0, P1))
- assertThat(triangle.edge(4)).isEqualTo(MutableSegment(P1, P2))
- assertThat(triangle.edge(5)).isEqualTo(MutableSegment(P2, P0))
+ assertThat(triangle.computeEdge(0)).isEqualTo(MutableSegment(p0, p1))
+ assertThat(triangle.computeEdge(1)).isEqualTo(MutableSegment(p1, p2))
+ assertThat(triangle.computeEdge(2)).isEqualTo(MutableSegment(p2, p0))
+ assertThat(triangle.computeEdge(3)).isEqualTo(MutableSegment(p0, p1))
+ assertThat(triangle.computeEdge(4)).isEqualTo(MutableSegment(p1, p2))
+ assertThat(triangle.computeEdge(5)).isEqualTo(MutableSegment(p2, p0))
}
@Test
fun populateEdge_zeroIndex_correctlyPopulatesSegment() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val segment0 = MutableSegment()
val segment6 = MutableSegment()
- triangle.populateEdge(0, segment0)
- triangle.populateEdge(6, segment6)
+ triangle.computeEdge(0, segment0)
+ triangle.computeEdge(6, segment6)
- assertThat(segment0).isEqualTo(MutableSegment(P0, P1))
- assertThat(segment6).isEqualTo(MutableSegment(P0, P1))
+ assertThat(segment0).isEqualTo(MutableSegment(p0, p1))
+ assertThat(segment6).isEqualTo(MutableSegment(p0, p1))
}
@Test
fun populateEdge_oneIndex_correctlyPopulatesSegment() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val segment1 = MutableSegment()
val segment7 = MutableSegment()
- triangle.populateEdge(1, segment1)
- triangle.populateEdge(7, segment7)
+ triangle.computeEdge(1, segment1)
+ triangle.computeEdge(7, segment7)
- assertThat(segment1).isEqualTo(MutableSegment(P1, P2))
- assertThat(segment7).isEqualTo(MutableSegment(P1, P2))
+ assertThat(segment1).isEqualTo(MutableSegment(p1, p2))
+ assertThat(segment7).isEqualTo(MutableSegment(p1, p2))
}
@Test
fun populateEdge_twoIndex_correctlyPopulatesSegment() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val segment2 = MutableSegment()
val segment8 = MutableSegment()
- triangle.populateEdge(2, segment2)
- triangle.populateEdge(8, segment8)
+ triangle.computeEdge(2, segment2)
+ triangle.computeEdge(8, segment8)
- assertThat(segment2).isEqualTo(MutableSegment(P2, P0))
- assertThat(segment8).isEqualTo(MutableSegment(P2, P0))
+ assertThat(segment2).isEqualTo(MutableSegment(p2, p0))
+ assertThat(segment8).isEqualTo(MutableSegment(p2, p0))
}
@Test
fun asImmutable_returnsImmutableCopy() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val output = triangle.asImmutable()
- assertThat(output.p0).isEqualTo(P0)
- assertThat(output.p1).isEqualTo(P1)
- assertThat(output.p2).isEqualTo(P2)
- }
-
- @Test
- fun asImmutable_withNewValues_ReturnsNewImmutable() {
- val triangle = MutableTriangle(P0, P1, P2)
- val p0 = ImmutableVec(10f, 20f)
- val p1 = ImmutableVec(30f, 40f)
- val p2 = ImmutableVec(50f, 60f)
- val output = triangle.asImmutable(p0, p1, p2)
-
assertThat(output.p0).isEqualTo(p0)
assertThat(output.p1).isEqualTo(p1)
assertThat(output.p2).isEqualTo(p2)
@@ -260,7 +196,7 @@
@Test
fun toString_correctlyReturnsString() {
- val triangle = MutableTriangle(P0, P1, P2)
+ val triangle = MutableTriangle(p0, p1, p2)
val string = triangle.toString()
@@ -271,12 +207,4 @@
assertThat(string).contains("5")
assertThat(string).contains("6")
}
-
- companion object {
- private val P0 = ImmutableVec(1f, 2f)
-
- private val P1 = ImmutableVec(5f, 2f)
-
- private val P2 = ImmutableVec(5f, 6f)
- }
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableVecTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableVecTest.kt
index afa1451..a4c4c52 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableVecTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/MutableVecTest.kt
@@ -36,9 +36,9 @@
@Test
fun equals_whenDifferentType_returnsFalse() {
val vec = MutableVec(1f, 2f)
- val point = MutablePoint(1f, 2f)
+ val segment = MutableSegment(MutableVec(1f, 2f), MutableVec(3f, 4f))
- assertThat(vec).isNotEqualTo(point)
+ assertThat(vec).isNotEqualTo(segment)
}
@Test
@@ -85,24 +85,6 @@
}
@Test
- fun x_modifiesValue() {
- val testVec = MutableVec(10f, 25f)
-
- testVec.x(999f)
-
- assertThat(testVec).isEqualTo(MutableVec(999f, 25f))
- }
-
- @Test
- fun y_modifiesValue() {
- val testVec = MutableVec(10f, 25f)
-
- testVec.y(999f)
-
- assertThat(testVec).isEqualTo(MutableVec(10f, 999f))
- }
-
- @Test
fun populateFrom_modifiesValue() {
val testVec = MutableVec(10f, 25f)
@@ -112,133 +94,126 @@
}
@Test
- fun orthogonal_returnsCorrectValue() {
- assertThat(MutableVec(3f, 1f).orthogonal).isEqualTo(ImmutableVec(-1f, 3f))
- assertThat(MutableVec(-395f, .005f).orthogonal).isEqualTo(ImmutableVec(-.005f, -395f))
- assertThat(MutableVec(-.2f, -.66f).orthogonal).isEqualTo(ImmutableVec(.66f, -.2f))
- assertThat(MutableVec(123f, -987f).orthogonal).isEqualTo(ImmutableVec(987f, 123f))
+ fun computeOrthogonal_returnsCorrectValue() {
+ assertThat(MutableVec(3f, 1f).computeOrthogonal()).isEqualTo(ImmutableVec(-1f, 3f))
+ assertThat(MutableVec(-395f, .005f).computeOrthogonal())
+ .isEqualTo(ImmutableVec(-.005f, -395f))
+ assertThat(MutableVec(-.2f, -.66f).computeOrthogonal()).isEqualTo(ImmutableVec(.66f, -.2f))
+ assertThat(MutableVec(123f, -987f).computeOrthogonal()).isEqualTo(ImmutableVec(987f, 123f))
}
@Test
- fun orthogonal_whenMutableVecIsModified_returnsCorrectValue() {
+ fun computeOrthogonal_whenMutableVecIsModified_returnsCorrectValue() {
val vec = MutableVec(3f, 1f)
- assertThat(vec.orthogonal).isEqualTo(ImmutableVec(-1f, 3f))
+ assertThat(vec.computeOrthogonal()).isEqualTo(ImmutableVec(-1f, 3f))
vec.x = 10f
vec.y = 2134f
- assertThat(vec.orthogonal).isEqualTo(ImmutableVec(-2134f, 10f))
+ assertThat(vec.computeOrthogonal()).isEqualTo(ImmutableVec(-2134f, 10f))
}
@Test
- fun populateOrthogonal_populatesCorrectValue() {
+ fun computeOrthogonal_populatesCorrectValue() {
val mutableVec = MutableVec()
- MutableVec(3f, 1f).populateOrthogonal(mutableVec)
+ MutableVec(3f, 1f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-1f, 3f))
- MutableVec(-395f, .005f).populateOrthogonal(mutableVec)
+ MutableVec(-395f, .005f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-.005f, -395f))
- MutableVec(-.2f, -.66f).populateOrthogonal(mutableVec)
+ MutableVec(-.2f, -.66f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(.66f, -.2f))
- MutableVec(123f, -987f).populateOrthogonal(mutableVec)
+ MutableVec(123f, -987f).computeOrthogonal(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(987f, 123f))
}
@Test
- fun populateOrthogonal_whenMutableVecIsModified_populatesCorrectValue() {
+ fun computeOrthogonal_whenMutableVecIsModified_populatesCorrectValue() {
val inputVec = MutableVec(3f, 1f)
val outputVec = MutableVec()
- inputVec.populateOrthogonal(outputVec)
+ inputVec.computeOrthogonal(outputVec)
assertThat(outputVec).isEqualTo(ImmutableVec(-1f, 3f))
inputVec.x = -9956f
inputVec.y = -.001f
- inputVec.populateOrthogonal(outputVec)
+ inputVec.computeOrthogonal(outputVec)
assertThat(outputVec).isEqualTo(ImmutableVec(.001f, -9956f))
}
@Test
- fun negation_returnsCorrectValue() {
- assertThat(MutableVec(3f, 1f).negation).isEqualTo(MutableVec(-3f, -1f))
- assertThat(MutableVec(-395f, .005f).negation).isEqualTo(MutableVec(395f, -.005f))
- assertThat(MutableVec(-.2f, -.66f).negation).isEqualTo(MutableVec(.2f, .66f))
- assertThat(MutableVec(123f, -987f).negation).isEqualTo(MutableVec(-123f, 987f))
+ fun computeNegation_returnsCorrectValue() {
+ assertThat(MutableVec(3f, 1f).computeNegation()).isEqualTo(MutableVec(-3f, -1f))
+ assertThat(MutableVec(-395f, .005f).computeNegation()).isEqualTo(MutableVec(395f, -.005f))
+ assertThat(MutableVec(-.2f, -.66f).computeNegation()).isEqualTo(MutableVec(.2f, .66f))
+ assertThat(MutableVec(123f, -987f).computeNegation()).isEqualTo(MutableVec(-123f, 987f))
}
@Test
- fun populateNegation_populatesCorrectValue() {
+ fun computeNegation_populatesCorrectValue() {
val mutableVec = MutableVec()
- MutableVec(3f, 1f).populateNegation(mutableVec)
+ MutableVec(3f, 1f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-3f, -1f))
- MutableVec(-395f, .005f).populateNegation(mutableVec)
+ MutableVec(-395f, .005f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(395f, -.005f))
- MutableVec(-.2f, -.66f).populateNegation(mutableVec)
+ MutableVec(-.2f, -.66f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(.2f, .66f))
- MutableVec(123f, -987f).populateNegation(mutableVec)
+ MutableVec(123f, -987f).computeNegation(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-123f, 987f))
}
@Test
- fun negation_whenMutableVecIsModified_returnsCorrectValue() {
+ fun computeNegation_whenMutableVecIsModified_returnsCorrectValue() {
val vec = MutableVec(3f, 1f)
- assertThat(vec.negation).isEqualTo(MutableVec(-3f, -1f))
+ assertThat(vec.computeNegation()).isEqualTo(MutableVec(-3f, -1f))
vec.x = 10f
vec.y = 2134f
- assertThat(vec.negation).isEqualTo(MutableVec(-10f, -2134f))
+ assertThat(vec.computeNegation()).isEqualTo(MutableVec(-10f, -2134f))
}
@Test
- fun populateNegation_whenMutableVecIsModified_populatesCorrectValue() {
+ fun computeNegation_whenMutableVecIsModified_populatesCorrectValue() {
val inputVec = MutableVec(3f, 1f)
val outputVec = MutableVec()
- inputVec.populateNegation(outputVec)
+ inputVec.computeNegation(outputVec)
assertThat(outputVec).isEqualTo(MutableVec(-3f, -1f))
inputVec.x = -9956f
inputVec.y = -.001f
- inputVec.populateNegation(outputVec)
+ inputVec.computeNegation(outputVec)
assertThat(outputVec).isEqualTo(MutableVec(9956f, .001f))
}
@Test
- fun magnitude_returnsCorrectValue() {
- assertThat(MutableVec(1f, 1f).magnitude).isEqualTo(sqrt(2f))
- assertThat(MutableVec(-3f, 4f).magnitude).isEqualTo(5f)
- assertThat(MutableVec(0f, 0f).magnitude).isEqualTo(0f)
- assertThat(MutableVec(0f, 17f).magnitude).isEqualTo(17f)
+ fun computeMagnitude_returnsCorrectValue() {
+ assertThat(MutableVec(1f, 1f).computeMagnitude()).isEqualTo(sqrt(2f))
+ assertThat(MutableVec(-3f, 4f).computeMagnitude()).isEqualTo(5f)
+ assertThat(MutableVec(0f, 0f).computeMagnitude()).isEqualTo(0f)
+ assertThat(MutableVec(0f, 17f).computeMagnitude()).isEqualTo(17f)
}
@Test
- fun magnitude_whenMutableVecIsModified_returnsCorrectValue() {
+ fun computeMagnitude_whenMutableVecIsModified_returnsCorrectValue() {
val vec = MutableVec(-3f, 4f)
- assertThat(vec.magnitude).isEqualTo(5f)
+ assertThat(vec.computeMagnitude()).isEqualTo(5f)
vec.x = 5f
vec.y = 12f
- assertThat(vec.magnitude).isEqualTo(13f)
+ assertThat(vec.computeMagnitude()).isEqualTo(13f)
}
@Test
- fun magnitudeSquared_returnsCorrectValue() {
- assertThat(MutableVec(1f, 1f).magnitudeSquared).isEqualTo(2f)
- assertThat(MutableVec(3f, -4f).magnitudeSquared).isEqualTo(25f)
- assertThat(MutableVec(0f, 0f).magnitudeSquared).isEqualTo(0f)
- assertThat(MutableVec(15f, 0f).magnitudeSquared).isEqualTo(225f)
+ fun computeMagnitudeSquared_returnsCorrectValue() {
+ assertThat(MutableVec(1f, 1f).computeMagnitudeSquared()).isEqualTo(2f)
+ assertThat(MutableVec(3f, -4f).computeMagnitudeSquared()).isEqualTo(25f)
+ assertThat(MutableVec(0f, 0f).computeMagnitudeSquared()).isEqualTo(0f)
+ assertThat(MutableVec(15f, 0f).computeMagnitudeSquared()).isEqualTo(225f)
}
@Test
- fun magnitudeSquared_whenMutableVecIsModified_returnsCorrectValue() {
+ fun computeMagnitudeSquared_whenMutableVecIsModified_returnsCorrectValue() {
val vec = MutableVec(-3f, 4f)
- assertThat(vec.magnitudeSquared).isEqualTo(25f)
+ assertThat(vec.computeMagnitudeSquared()).isEqualTo(25f)
vec.x = 5f
vec.y = 12f
- assertThat(vec.magnitudeSquared).isEqualTo(169f)
+ assertThat(vec.computeMagnitudeSquared()).isEqualTo(169f)
}
@Test
- fun asImmutableVal_returnsNewEquivalentImmutableVec() {
- val vec = MutableVec(1f, 2f)
-
- assertThat(vec.asImmutable).isNotSameInstanceAs(vec)
- assertThat(vec.asImmutable).isEqualTo(vec)
- }
-
- @Test
- fun asImmutableFun_withNoArguments_returnsNewEquivalentImmutableVec() {
+ fun asImmutable_returnsNewEquivalentImmutableVec() {
val vec = MutableVec(1f, 2f)
assertThat(vec.asImmutable()).isNotSameInstanceAs(vec)
@@ -246,23 +221,12 @@
}
@Test
- fun asImmutableFun_withArguments_returnsCorrectNewImmutableVec() {
- val vec = MutableVec(1f, 2f)
-
- assertThat(vec.asImmutable(x = 10f)).isEqualTo(ImmutableVec(10f, 2f))
- assertThat(vec.asImmutable(10f)).isEqualTo(ImmutableVec(10f, 2f))
- assertThat(vec.asImmutable(y = 20f)).isEqualTo(ImmutableVec(1f, 20f))
- assertThat(vec.asImmutable(x = 10f, y = 20f)).isEqualTo(ImmutableVec(10f, 20f))
- assertThat(vec.asImmutable(10f, 20f)).isEqualTo(ImmutableVec(10f, 20f))
- }
-
- @Test
- fun unitVec_whenModified_returnsCorrectValue() {
+ fun computeUnitVec_whenModified_returnsCorrectValue() {
val vec = MutableVec(4f, 0f)
- assertThat(vec.unitVec).isEqualTo(ImmutableVec(1f, 0f))
+ assertThat(vec.computeUnitVec()).isEqualTo(ImmutableVec(1f, 0f))
vec.x = 0f
vec.y = -.05f
- assertThat(vec.unitVec).isEqualTo(ImmutableVec(0f, -1f))
+ assertThat(vec.computeUnitVec()).isEqualTo(ImmutableVec(0f, -1f))
}
@Test
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt
index f9982d6..f4e8ead 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/ParallelogramInterfaceTest.kt
@@ -29,13 +29,13 @@
val expectedWidth = 5f
val expectedHeight = -3f
val expectedRotation = Angle.QUARTER_TURN_RADIANS + Angle.HALF_TURN_RADIANS
- val assertExpectedValues: (Float, Float, Float) -> TestParallelogram =
+ val assertExpectedValues: (Float, Float, Float) -> Parallelogram =
{ normalizedWidth: Float, normalizedHeight: Float, normalizedRotation: Float ->
assertThat(normalizedWidth).isEqualTo(expectedWidth)
assertThat(normalizedHeight).isEqualTo(expectedHeight)
assertThat(normalizedRotation).isWithin(tolerance).of(expectedRotation)
- TestParallelogram(
- ImmutablePoint(0f, 0f),
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ ImmutableVec(0f, 0f),
expectedWidth,
expectedHeight,
expectedRotation,
@@ -55,13 +55,13 @@
val expectedWidth = 5f
val expectedHeight = 3f
val expectedRotation = Angle.QUARTER_TURN_RADIANS // 5 Pi normalized to range [0, 2*pi]
- val assertExpectedValues: (Float, Float, Float) -> TestParallelogram =
+ val assertExpectedValues: (Float, Float, Float) -> Parallelogram =
{ normalizedWidth: Float, normalizedHeight: Float, normalizedRotation: Float ->
assertThat(normalizedWidth).isEqualTo(expectedWidth)
assertThat(normalizedHeight).isEqualTo(expectedHeight)
assertThat(normalizedRotation).isWithin(tolerance).of(expectedRotation)
- TestParallelogram(
- ImmutablePoint(0f, 0f),
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ ImmutableVec(0f, 0f),
expectedWidth,
expectedHeight,
expectedRotation,
@@ -84,23 +84,17 @@
width = 5f,
height = 3f,
rotation = Angle.QUARTER_TURN_RADIANS,
- runBlock = TestParallelogram.makeTestParallelogram,
+ runBlock = { w: Float, h: Float, r: Float ->
+ ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
+ ImmutableVec(0f, 0f),
+ w,
+ h,
+ r,
+ 0f,
+ )
+ },
)
- assertThat(parallelogram.signedArea()).isEqualTo(15f)
- }
-
- private class TestParallelogram(
- override val center: ImmutablePoint,
- override val width: Float,
- override val height: Float,
- override val rotation: Float,
- override val shearFactor: Float,
- ) : Parallelogram {
- companion object {
- val makeTestParallelogram = { w: Float, h: Float, r: Float ->
- TestParallelogram(ImmutablePoint(0f, 0f), w, h, r, 0f)
- }
- }
+ assertThat(parallelogram.computeSignedArea()).isEqualTo(15f)
}
private val tolerance = 0.000001f
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
new file mode 100644
index 0000000..682b88d
--- /dev/null
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.geometry
+
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PartitionedMeshTest {
+
+ @Test
+ fun bounds_shouldBeEmpty() {
+ val partitionedMesh = PartitionedMesh()
+
+ assertThat(partitionedMesh.bounds).isNull()
+ }
+
+ @Test
+ fun renderGroupCount_whenEmptyShape_shouldBeZero() {
+ val partitionedMesh = PartitionedMesh()
+
+ assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+ }
+
+ @Test
+ fun outlineCount_whenEmptyShape_shouldThrow() {
+ val partitionedMesh = PartitionedMesh()
+
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+ }
+
+ @Test
+ fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+ val partitionedMesh = PartitionedMesh()
+
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+ }
+
+ @Test
+ fun populateOutlinePosition_whenEmptyShape_shouldThrow() {
+ val partitionedMesh = PartitionedMesh()
+
+ assertFailsWith<IllegalArgumentException> {
+ partitionedMesh.populateOutlinePosition(-1, 0, 0, MutableVec())
+ }
+ assertFailsWith<IllegalArgumentException> {
+ partitionedMesh.populateOutlinePosition(0, 0, 0, MutableVec())
+ }
+ assertFailsWith<IllegalArgumentException> {
+ partitionedMesh.populateOutlinePosition(1, 0, 0, MutableVec())
+ }
+ }
+
+ @Test
+ fun toString_returnsAString() {
+ val string = PartitionedMesh().toString()
+
+ // Not elaborate checks - this test mainly exists to ensure that toString doesn't crash.
+ assertThat(string).contains("PartitionedMesh")
+ assertThat(string).contains("bounds")
+ assertThat(string).contains("meshes")
+ assertThat(string).contains("nativeAddress")
+ }
+}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PointTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PointTest.kt
deleted file mode 100644
index e6e4eef..0000000
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PointTest.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.ink.geometry
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class PointTest {
-
- @Test
- fun isAlmostEqual_whenNoToleranceGiven_returnsCorrectValue() {
- val point = ImmutablePoint(1f, 2f)
-
- assertThat(point.isAlmostEqual(point)).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1f, 2f))).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1.00001f, 1.99999f))).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1f, 1.99f))).isFalse()
- assertThat(point.isAlmostEqual(ImmutablePoint(1.01f, 2f))).isFalse()
- assertThat(point.isAlmostEqual(ImmutablePoint(1.01f, 1.99f))).isFalse()
- }
-
- @Test
- fun isAlmostEqual_withToleranceGiven_returnsCorrectValue() {
- val point = ImmutablePoint(1f, 2f)
-
- assertThat(point.isAlmostEqual(point, tolerance = 0.00000001f)).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1f, 2f), tolerance = 0.00000001f)).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1.00001f, 1.99999f), tolerance = 0.000001f))
- .isFalse()
- assertThat(point.isAlmostEqual(ImmutablePoint(1f, 1.99f), tolerance = 0.02f)).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1.01f, 2f), tolerance = 0.02f)).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(1.01f, 1.99f), tolerance = 0.02f)).isTrue()
- assertThat(point.isAlmostEqual(ImmutablePoint(2.5f, 0.5f), tolerance = 2f)).isTrue()
- }
-
- @Test
- fun isAlmostEqual_whenSameInterface_returnsTrue() {
- val point = MutablePoint(1f, 2f)
- val other = ImmutablePoint(0.99999f, 2.00001f)
- assertThat(point.isAlmostEqual(other)).isTrue()
- }
-}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/SegmentTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/SegmentTest.kt
index 48403e1..ed453d4 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/SegmentTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/SegmentTest.kt
@@ -28,37 +28,37 @@
@Test
fun length_returnsCorrectValue() {
- assertThat(ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f)).computeLength())
.isEqualTo(sqrt(2f))
- assertThat(ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f)).computeLength())
.isEqualTo(5f)
- assertThat(ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f)).computeLength())
.isEqualTo(sqrt(5f))
- assertThat(ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f)).computeLength())
.isEqualTo(sqrt(41f))
}
@Test
fun length_whenSegmentIsHorizontal_returnsCorrectValue() {
- assertThat(ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f)).computeLength())
.isEqualTo(4f)
- assertThat(ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f)).computeLength())
.isEqualTo(6f)
}
@Test
fun length_whenSegmentIsVertical_returnsCorrectValue() {
- assertThat(ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f)).computeLength())
.isEqualTo(1f)
- assertThat(ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f)).computeLength())
.isEqualTo(2f)
}
@Test
fun length_whenSegmentIsDegenerate_returnsZero() {
- assertThat(ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(4f, 1f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(4f, 1f)).computeLength())
.isEqualTo(0f)
- assertThat(ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)).length)
+ assertThat(ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)).computeLength())
.isEqualTo(0f)
}
@@ -66,35 +66,35 @@
fun vec_fillsCorrectValues() {
assertThat(
ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(1f, 1f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(4f, 3f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(-1f, 2f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(-4f, -5f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(0.6f, 1.9f), ImmutableVec(-1.2f, 3.3f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(-1.8f, 1.4f), 0.000001f)
)
.isTrue()
@@ -104,14 +104,14 @@
fun vec_whenSegmentIsHorizontal_fillsCorrectValues() {
assertThat(
ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(0f, -4f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(0f, 6f), 0.000001f)
)
.isTrue()
@@ -121,14 +121,14 @@
fun vec_whenSegmentIsVertical_fillsCorrectValues() {
assertThat(
ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(1f, 0f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(-2f, 0f), 0.000001f)
)
.isTrue()
@@ -138,14 +138,14 @@
fun vec_whenSegmentIsDegenerate_fillsZeroes() {
assertThat(
ImmutableSegment(ImmutableVec(1f, -5f), ImmutableVec(1f, -5f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(0f, 0f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f))
- .vec
+ .computeDisplacement()
.isAlmostEqual(ImmutableVec(0f, 0f), 0.000001f)
)
.isTrue()
@@ -154,50 +154,57 @@
@Test
fun populateVec_fillsCorrectValues() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f)).computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(1f, 1f))
- ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(4f, 3f))
- ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-1f, 2f))
- ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-4f, -5f))
ImmutableSegment(ImmutableVec(0.6f, 1.9f), ImmutableVec(-1.2f, 3.3f))
- .populateVec(mutableVec)
+ .computeDisplacement(mutableVec)
assertThat(mutableVec.isAlmostEqual(MutableVec(-1.8f, 1.4f), 0.000001f)).isTrue()
}
@Test
fun populateVec_whenSegmentIsHorizontal_fillsCorrectValues() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(0f, -4f))
- ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(0f, 6f))
}
@Test
fun populateVec_whenSegmentIsVertical_fillsCorrectValues() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f)).computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(1f, 0f))
- ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-2f, 0f))
}
@Test
fun populateVec_whenSegmentIsDegenerate_fillsZeroes() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(1f, -5f), ImmutableVec(1f, -5f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(1f, -5f), ImmutableVec(1f, -5f))
+ .computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(0f, 0f))
- ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)).populateVec(mutableVec)
+ ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)).computeDisplacement(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(0f, 0f))
}
@@ -205,35 +212,35 @@
fun midpoint_fillsCorrectValues() {
assertThat(
ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(.5f, .5f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(-2f, 3.5f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(-.5f, 2f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(1f, 1.5f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(0.6f, 1.9f), ImmutableVec(-1.2f, 3.3f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(-.3f, 2.6f), 0.000001f)
)
.isTrue()
@@ -243,14 +250,14 @@
fun midpoint_whenSegmentIsHorizontal_fillsCorrectValues() {
assertThat(
ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(1f, -1f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(3f, 1f), 0.000001f)
)
.isTrue()
@@ -260,14 +267,14 @@
fun midpoint_whenSegmentIsVertical_fillsCorrectValues() {
assertThat(
ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(4.5f, 1f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(-2f, -5f), 0.000001f)
)
.isTrue()
@@ -277,14 +284,14 @@
fun midpoint_whenSegmentIsDegenerate_fillsZeroes() {
assertThat(
ImmutableSegment(ImmutableVec(1f, -5f), ImmutableVec(1f, -5f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(1f, -5f), 0.000001f)
)
.isTrue()
assertThat(
ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f))
- .midpoint
+ .computeMidpoint()
.isAlmostEqual(ImmutableVec(0f, 0f), 0.000001f)
)
.isTrue()
@@ -293,113 +300,112 @@
@Test
fun populateMidpoint_fillsCorrectValues() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(1f, 1f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(.5f, .5f))
- ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(-4f, 2f), ImmutableVec(0f, 5f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-2f, 3.5f))
- ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(0f, 1f), ImmutableVec(-1f, 3f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-.5f, 2f))
- ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(3f, 4f), ImmutableVec(-1f, -1f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(1f, 1.5f))
ImmutableSegment(ImmutableVec(0.6f, 1.9f), ImmutableVec(-1.2f, 3.3f))
- .populateMidpoint(mutableVec)
+ .computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-.3f, 2.6f))
}
@Test
fun populateMidpoint_whenSegmentIsHorizontal_fillsCorrectValues() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(1f, 1f), ImmutableVec(1f, -3f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(1f, -1f))
- ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(3f, -2f), ImmutableVec(3f, 4f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(3f, 1f))
}
@Test
fun populateMidpoint_whenSegmentIsVertical_fillsCorrectValues() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(4f, 1f), ImmutableVec(5f, 1f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(4.5f, 1f))
- ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f))
- .populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(-1f, -5f), ImmutableVec(-3f, -5f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(-2f, -5f))
}
@Test
fun populateMidpoint_whenSegmentIsDegenerate_fillsZeroes() {
val mutableVec = MutableVec(0f, 0f)
- ImmutableSegment(ImmutableVec(1f, -5f), ImmutableVec(1f, -5f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(1f, -5f), ImmutableVec(1f, -5f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(1f, -5f))
- ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)).populateMidpoint(mutableVec)
+ ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)).computeMidpoint(mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(0f, 0f))
}
@Test
fun boundingBox_correctlyReturnsBoundingBox() {
- val segment0 = MutableSegment(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f))
+ val segment0 = MutableSegment(MutableVec(1f, 1f), MutableVec(5f, 2f))
val segment1 = ImmutableSegment(ImmutableVec(-1f, 2f), ImmutableVec(0f, 0f))
- assertThat(segment0.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 1f), ImmutablePoint(5f, 2f)))
- assertThat(segment1.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(-1f, 0f), ImmutablePoint(0f, 2f)))
+ assertThat(segment0.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f)))
+ assertThat(segment1.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(-1f, 0f), ImmutableVec(0f, 2f)))
}
@Test
fun boundingBox_forDegenerateSegment_correctlyReturnsBoundingBox() {
- val segment0 = MutableSegment(ImmutableVec(3f, 2f), ImmutableVec(3f, 2f))
+ val segment0 = MutableSegment(MutableVec(3f, 2f), MutableVec(3f, 2f))
val segment1 = ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f))
- assertThat(segment0.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(3f, 2f), ImmutablePoint(3f, 2f)))
- assertThat(segment1.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(0f, 0f), ImmutablePoint(0f, 0f)))
+ assertThat(segment0.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(3f, 2f), ImmutableVec(3f, 2f)))
+ assertThat(segment1.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f)))
}
@Test
fun populateBoundingBox_correctlyReturnsBoundingBox() {
- val segment0 = MutableSegment(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f))
+ val segment0 = MutableSegment(MutableVec(1f, 1f), MutableVec(5f, 2f))
val segment1 = ImmutableSegment(ImmutableVec(-1f, 2f), ImmutableVec(0f, 0f))
val box0 = MutableBox()
val box1 = MutableBox()
- segment0.populateBoundingBox(box0)
- segment1.populateBoundingBox(box1)
+ segment0.computeBoundingBox(box0)
+ segment1.computeBoundingBox(box1)
assertThat(box0)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1f, 1f), ImmutablePoint(5f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f))
)
assertThat(box1)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(-1f, 0f), ImmutablePoint(0f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-1f, 0f), ImmutableVec(0f, 2f))
)
}
@Test
fun populateBoundingBox_forDegenerateSegment_correctlyReturnsBoundingBox() {
- val segment0 = MutableSegment(ImmutableVec(3f, 2f), ImmutableVec(3f, 2f))
+ val segment0 = MutableSegment(MutableVec(3f, 2f), MutableVec(3f, 2f))
val segment1 = ImmutableSegment(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f))
val box0 = MutableBox()
val box1 = MutableBox()
- segment0.populateBoundingBox(box0)
- segment1.populateBoundingBox(box1)
+ segment0.computeBoundingBox(box0)
+ segment1.computeBoundingBox(box1)
assertThat(box0)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(3f, 2f), ImmutablePoint(3f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(3f, 2f), ImmutableVec(3f, 2f))
)
assertThat(box1)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(0f, 0f), ImmutablePoint(0f, 0f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(0f, 0f), ImmutableVec(0f, 0f))
)
}
@@ -407,21 +413,28 @@
fun lerpPoint_withZeroOrOneRatio_fillsCorrectValues() {
val segment = ImmutableSegment(ImmutableVec(6f, 3f), ImmutableVec(8f, -5f))
- assertThat(segment.lerpPoint(0.0f).isAlmostEqual(ImmutableVec(6f, 3f), 0.000001f)).isTrue()
+ assertThat(segment.computeLerpPoint(0.0f).isAlmostEqual(ImmutableVec(6f, 3f), 0.000001f))
+ .isTrue()
- assertThat(segment.lerpPoint(1.0f).isAlmostEqual(ImmutableVec(8f, -5f), 0.000001f)).isTrue()
+ assertThat(segment.computeLerpPoint(1.0f).isAlmostEqual(ImmutableVec(8f, -5f), 0.000001f))
+ .isTrue()
}
@Test
fun lerpPoint_withRatioBetweenZeroAndOne_fillsCorrectValues() {
val segment = ImmutableSegment(ImmutableVec(6f, 3f), ImmutableVec(8f, -5f))
- assertThat(segment.lerpPoint(0.2f).isAlmostEqual(ImmutableVec(6.4f, 1.4f), 0.000001f))
+ assertThat(
+ segment.computeLerpPoint(0.2f).isAlmostEqual(ImmutableVec(6.4f, 1.4f), 0.000001f)
+ )
.isTrue()
- assertThat(segment.lerpPoint(0.5f).isAlmostEqual(ImmutableVec(7f, -1f), 0.000001f)).isTrue()
+ assertThat(segment.computeLerpPoint(0.5f).isAlmostEqual(ImmutableVec(7f, -1f), 0.000001f))
+ .isTrue()
- assertThat(segment.lerpPoint(0.9f).isAlmostEqual(ImmutableVec(7.8f, -4.2f), 0.000001f))
+ assertThat(
+ segment.computeLerpPoint(0.9f).isAlmostEqual(ImmutableVec(7.8f, -4.2f), 0.000001f)
+ )
.isTrue()
}
@@ -429,9 +442,12 @@
fun lerpPoint_withRatioOutsideZeroAndOne_fillsCorrectValues() {
val segment = ImmutableSegment(ImmutableVec(6f, 3f), ImmutableVec(8f, -5f))
- assertThat(segment.lerpPoint(-1f).isAlmostEqual(ImmutableVec(4f, 11f), 0.000001f)).isTrue()
+ assertThat(segment.computeLerpPoint(-1f).isAlmostEqual(ImmutableVec(4f, 11f), 0.000001f))
+ .isTrue()
- assertThat(segment.lerpPoint(1.3f).isAlmostEqual(ImmutableVec(8.6f, -7.4f), 0.000001f))
+ assertThat(
+ segment.computeLerpPoint(1.3f).isAlmostEqual(ImmutableVec(8.6f, -7.4f), 0.000001f)
+ )
.isTrue()
}
@@ -440,10 +456,10 @@
val segment = ImmutableSegment(ImmutableVec(6f, 3f), ImmutableVec(8f, -5f))
val mutableVec = MutableVec(0f, 0f)
- segment.populateLerpPoint(0.0f, mutableVec)
+ segment.computeLerpPoint(0.0f, mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(6f, 3f))
- segment.populateLerpPoint(1.0f, mutableVec)
+ segment.computeLerpPoint(1.0f, mutableVec)
assertThat(mutableVec).isEqualTo(MutableVec(8f, -5f))
}
@@ -452,13 +468,13 @@
val segment = ImmutableSegment(ImmutableVec(6f, 3f), ImmutableVec(8f, -5f))
val mutableVec = MutableVec(0f, 0f)
- segment.populateLerpPoint(0.2f, mutableVec)
+ segment.computeLerpPoint(0.2f, mutableVec)
assertThat(mutableVec.isAlmostEqual(MutableVec(6.4f, 1.4f), .000001f)).isTrue()
- segment.populateLerpPoint(0.5f, mutableVec)
+ segment.computeLerpPoint(0.5f, mutableVec)
assertThat(mutableVec.isAlmostEqual(MutableVec(7f, -1f), .000001f)).isTrue()
- segment.populateLerpPoint(0.9f, mutableVec)
+ segment.computeLerpPoint(0.9f, mutableVec)
assertThat(mutableVec.isAlmostEqual(MutableVec(7.8f, -4.2f), .000001f)).isTrue()
}
@@ -467,10 +483,10 @@
val segment = ImmutableSegment(ImmutableVec(6f, 3f), ImmutableVec(8f, -5f))
val mutableVec = MutableVec(0f, 0f)
- segment.populateLerpPoint(-1f, mutableVec)
+ segment.computeLerpPoint(-1f, mutableVec)
assertThat(mutableVec.isAlmostEqual(MutableVec(4f, 11f), .000001f)).isTrue()
- segment.populateLerpPoint(1.3f, mutableVec)
+ segment.computeLerpPoint(1.3f, mutableVec)
assertThat(mutableVec.isAlmostEqual(MutableVec(8.6f, -7.4f), .000001f)).isTrue()
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/TriangleTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/TriangleTest.kt
index 0309d82..2091d2c 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/TriangleTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/TriangleTest.kt
@@ -24,20 +24,20 @@
@RunWith(JUnit4::class)
class TriangleTest {
+ @Test
fun signedArea_correctlyReturnsArea() {
val triangle0 =
ImmutableTriangle(ImmutableVec(-1f, -3f), ImmutableVec(3f, -3f), ImmutableVec(-3f, -1f))
val triangle1 =
- MutableTriangle(ImmutableVec(1f, 1f), ImmutableVec(-5f, 4f), ImmutableVec(-1f, -2f))
+ MutableTriangle(MutableVec(1f, 1f), MutableVec(-5f, 4f), MutableVec(-1f, -2f))
val triangle2 =
ImmutableTriangle(ImmutableVec(-5f, 5f), ImmutableVec(2f, 4f), ImmutableVec(1f, -5f))
- val triangle3 =
- MutableTriangle(ImmutableVec(1f, -4f), ImmutableVec(3f, 1f), ImmutableVec(4f, 2f))
+ val triangle3 = MutableTriangle(MutableVec(1f, -4f), MutableVec(3f, 1f), MutableVec(4f, 2f))
- assertThat(triangle0.signedArea).isWithin(1e-5f).of(4f)
- assertThat(triangle1.signedArea).isWithin(1e-5f).of(12f)
- assertThat(triangle2.signedArea).isWithin(1e-5f).of(-32f)
- assertThat(triangle3.signedArea).isWithin(1e-5f).of(-1.5f)
+ assertThat(triangle0.computeSignedArea()).isWithin(1e-5f).of(4f)
+ assertThat(triangle1.computeSignedArea()).isWithin(1e-5f).of(12f)
+ assertThat(triangle2.computeSignedArea()).isWithin(1e-5f).of(-32f)
+ assertThat(triangle3.computeSignedArea()).isWithin(1e-5f).of(-1.5f)
}
@Test
@@ -45,104 +45,98 @@
val triangle0 =
ImmutableTriangle(ImmutableVec(3f, 2f), ImmutableVec(5f, 2f), ImmutableVec(2f, 2f))
val triangle1 =
- MutableTriangle(ImmutableVec(-1f, 2f), ImmutableVec(0f, 0f), ImmutableVec(1f, -2f))
+ MutableTriangle(MutableVec(-1f, 2f), MutableVec(0f, 0f), MutableVec(1f, -2f))
val triangle2 =
ImmutableTriangle(ImmutableVec(0f, 1f), ImmutableVec(-2f, 3f), ImmutableVec(-2f, 3f))
- val triangle3 =
- MutableTriangle(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f), ImmutableVec(5f, 2f))
+ val triangle3 = MutableTriangle(MutableVec(5f, 2f), MutableVec(5f, 2f), MutableVec(5f, 2f))
- assertThat(triangle0.signedArea).isWithin(1e-5f).of(0f)
- assertThat(triangle1.signedArea).isWithin(1e-5f).of(0f)
- assertThat(triangle2.signedArea).isWithin(1e-5f).of(0f)
- assertThat(triangle3.signedArea).isWithin(1e-5f).of(0f)
+ assertThat(triangle0.computeSignedArea()).isWithin(1e-5f).of(0f)
+ assertThat(triangle1.computeSignedArea()).isWithin(1e-5f).of(0f)
+ assertThat(triangle2.computeSignedArea()).isWithin(1e-5f).of(0f)
+ assertThat(triangle3.computeSignedArea()).isWithin(1e-5f).of(0f)
}
@Test
fun boundingBox_correctlyReturnsBoundingBox() {
- val triangle0 =
- MutableTriangle(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f), ImmutableVec(2f, 2f))
+ val triangle0 = MutableTriangle(MutableVec(1f, 1f), MutableVec(5f, 2f), MutableVec(2f, 2f))
val triangle1 =
ImmutableTriangle(ImmutableVec(-1f, -2f), ImmutableVec(0f, 0f), ImmutableVec(1f, -2f))
val triangle2 =
- MutableTriangle(ImmutableVec(0f, 1f), ImmutableVec(-2f, 3f), ImmutableVec(-2f, 3f))
+ MutableTriangle(MutableVec(0f, 1f), MutableVec(-2f, 3f), MutableVec(-2f, 3f))
val triangle3 =
ImmutableTriangle(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f), ImmutableVec(5f, 2f))
- assertThat(triangle0.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(1f, 1f), ImmutablePoint(5f, 2f)))
- assertThat(triangle1.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(-1f, -2f), ImmutablePoint(1f, 0f)))
- assertThat(triangle2.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(-2f, 1f), ImmutablePoint(0f, 3f)))
- assertThat(triangle3.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(5f, 2f), ImmutablePoint(5f, 2f)))
+ assertThat(triangle0.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f)))
+ assertThat(triangle1.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(-1f, -2f), ImmutableVec(1f, 0f)))
+ assertThat(triangle2.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(-2f, 1f), ImmutableVec(0f, 3f)))
+ assertThat(triangle3.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f)))
}
@Test
fun boundingBox_forDegenerateTriangle_correctlyReturnsBoundingBox() {
- val triangle0 =
- MutableTriangle(ImmutableVec(3f, 2f), ImmutableVec(5f, 2f), ImmutableVec(2f, 2f))
+ val triangle0 = MutableTriangle(MutableVec(3f, 2f), MutableVec(5f, 2f), MutableVec(2f, 2f))
val triangle1 =
- MutableTriangle(ImmutableVec(-1f, 2f), ImmutableVec(0f, 0f), ImmutableVec(1f, -2f))
+ MutableTriangle(MutableVec(-1f, 2f), MutableVec(0f, 0f), MutableVec(1f, -2f))
val triangle2 =
ImmutableTriangle(ImmutableVec(0f, 1f), ImmutableVec(-2f, 3f), ImmutableVec(-2f, 3f))
val triangle3 =
ImmutableTriangle(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f), ImmutableVec(5f, 2f))
- assertThat(triangle0.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(2f, 2f), ImmutablePoint(5f, 2f)))
- assertThat(triangle1.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(-1f, -2f), ImmutablePoint(1f, 2f)))
- assertThat(triangle2.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(-2f, 1f), ImmutablePoint(0f, 3f)))
- assertThat(triangle3.boundingBox)
- .isEqualTo(ImmutableBox.fromTwoPoints(ImmutablePoint(5f, 2f), ImmutablePoint(5f, 2f)))
+ assertThat(triangle0.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(2f, 2f), ImmutableVec(5f, 2f)))
+ assertThat(triangle1.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(-1f, -2f), ImmutableVec(1f, 2f)))
+ assertThat(triangle2.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(-2f, 1f), ImmutableVec(0f, 3f)))
+ assertThat(triangle3.computeBoundingBox())
+ .isEqualTo(ImmutableBox.fromTwoPoints(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f)))
}
@Test
fun populateBoundingBox_correctlyReturnsBoundingBox() {
- val triangle0 =
- MutableTriangle(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f), ImmutableVec(2f, 2f))
+ val triangle0 = MutableTriangle(MutableVec(1f, 1f), MutableVec(5f, 2f), MutableVec(2f, 2f))
val triangle1 =
ImmutableTriangle(ImmutableVec(-1f, -2f), ImmutableVec(0f, 0f), ImmutableVec(1f, -2f))
val triangle2 =
ImmutableTriangle(ImmutableVec(0f, 1f), ImmutableVec(-2f, 3f), ImmutableVec(-2f, 3f))
- val triangle3 =
- MutableTriangle(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f), ImmutableVec(5f, 2f))
+ val triangle3 = MutableTriangle(MutableVec(5f, 2f), MutableVec(5f, 2f), MutableVec(5f, 2f))
val box0 = MutableBox()
val box1 = MutableBox()
val box2 = MutableBox()
val box3 = MutableBox()
- triangle0.populateBoundingBox(box0)
- triangle1.populateBoundingBox(box1)
- triangle2.populateBoundingBox(box2)
- triangle3.populateBoundingBox(box3)
+ triangle0.computeBoundingBox(box0)
+ triangle1.computeBoundingBox(box1)
+ triangle2.computeBoundingBox(box2)
+ triangle3.computeBoundingBox(box3)
assertThat(box0)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(1f, 1f), ImmutablePoint(5f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(1f, 1f), ImmutableVec(5f, 2f))
)
assertThat(box1)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(-1f, -2f), ImmutablePoint(1f, 0f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-1f, -2f), ImmutableVec(1f, 0f))
)
assertThat(box2)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(-2f, 1f), ImmutablePoint(0f, 3f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-2f, 1f), ImmutableVec(0f, 3f))
)
assertThat(box3)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(5f, 2f), ImmutablePoint(5f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f))
)
}
@Test
fun populateBoundingBox_forDegenerateTriangle_correctlyReturnsBoundingBox() {
- val triangle0 =
- MutableTriangle(ImmutableVec(3f, 2f), ImmutableVec(5f, 2f), ImmutableVec(2f, 2f))
+ val triangle0 = MutableTriangle(MutableVec(3f, 2f), MutableVec(5f, 2f), MutableVec(2f, 2f))
val triangle1 =
- MutableTriangle(ImmutableVec(-1f, 2f), ImmutableVec(0f, 0f), ImmutableVec(1f, -2f))
+ MutableTriangle(MutableVec(-1f, 2f), MutableVec(0f, 0f), MutableVec(1f, -2f))
val triangle2 =
ImmutableTriangle(ImmutableVec(0f, 1f), ImmutableVec(-2f, 3f), ImmutableVec(-2f, 3f))
val triangle3 =
@@ -152,26 +146,26 @@
val box2 = MutableBox()
val box3 = MutableBox()
- triangle0.populateBoundingBox(box0)
- triangle1.populateBoundingBox(box1)
- triangle2.populateBoundingBox(box2)
- triangle3.populateBoundingBox(box3)
+ triangle0.computeBoundingBox(box0)
+ triangle1.computeBoundingBox(box1)
+ triangle2.computeBoundingBox(box2)
+ triangle3.computeBoundingBox(box3)
assertThat(box0)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(2f, 2f), ImmutablePoint(5f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(2f, 2f), ImmutableVec(5f, 2f))
)
assertThat(box1)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(-1f, -2f), ImmutablePoint(1f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-1f, -2f), ImmutableVec(1f, 2f))
)
assertThat(box2)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(-2f, 1f), ImmutablePoint(0f, 3f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(-2f, 1f), ImmutableVec(0f, 3f))
)
assertThat(box3)
.isEqualTo(
- MutableBox().fillFromTwoPoints(ImmutablePoint(5f, 2f), ImmutablePoint(5f, 2f))
+ MutableBox().populateFromTwoPoints(ImmutableVec(5f, 2f), ImmutableVec(5f, 2f))
)
}
}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/VecTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/VecTest.kt
index 5bcc734..377a931 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/VecTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/VecTest.kt
@@ -61,37 +61,40 @@
@Test
fun direction_returnsCorrectValue() {
- assertThat(ImmutableVec(5f, 0f).direction).isEqualTo(Angle.degreesToRadians(0f))
- assertThat(ImmutableVec(0f, 5f).direction).isEqualTo(Angle.degreesToRadians(90f))
- assertThat(ImmutableVec(-5f, 0f).direction).isEqualTo(Angle.degreesToRadians(180f))
- assertThat(ImmutableVec(0f, -5f).direction).isEqualTo(Angle.degreesToRadians(-90f))
- assertThat(ImmutableVec(5f, 5f).direction).isEqualTo(Angle.degreesToRadians(45f))
- assertThat(ImmutableVec(-5f, 5f).direction).isEqualTo(Angle.degreesToRadians(135f))
- assertThat(ImmutableVec(-5f, -5f).direction).isEqualTo(Angle.degreesToRadians(-135f))
- assertThat(ImmutableVec(5f, -5f).direction).isEqualTo(Angle.degreesToRadians(-45f))
+ assertThat(ImmutableVec(5f, 0f).computeDirection()).isEqualTo(Angle.degreesToRadians(0f))
+ assertThat(ImmutableVec(0f, 5f).computeDirection()).isEqualTo(Angle.degreesToRadians(90f))
+ assertThat(ImmutableVec(-5f, 0f).computeDirection()).isEqualTo(Angle.degreesToRadians(180f))
+ assertThat(ImmutableVec(0f, -5f).computeDirection()).isEqualTo(Angle.degreesToRadians(-90f))
+ assertThat(ImmutableVec(5f, 5f).computeDirection()).isEqualTo(Angle.degreesToRadians(45f))
+ assertThat(ImmutableVec(-5f, 5f).computeDirection()).isEqualTo(Angle.degreesToRadians(135f))
+ assertThat(ImmutableVec(-5f, -5f).computeDirection())
+ .isEqualTo(Angle.degreesToRadians(-135f))
+ assertThat(ImmutableVec(5f, -5f).computeDirection()).isEqualTo(Angle.degreesToRadians(-45f))
}
@Test
fun direction_whenVecContainsZero_returnsCorrectValue() {
- assertThat(ImmutableVec(+0f, +0f).direction).isEqualTo(Angle.degreesToRadians(0f))
- assertThat(ImmutableVec(+0f, -0f).direction).isEqualTo(Angle.degreesToRadians(-0f))
- assertThat(ImmutableVec(-0f, +0f).direction).isEqualTo(Angle.degreesToRadians(180f))
- assertThat(ImmutableVec(-0f, -0f).direction).isEqualTo(Angle.degreesToRadians(-180f))
+ assertThat(ImmutableVec(+0f, +0f).computeDirection()).isEqualTo(Angle.degreesToRadians(0f))
+ assertThat(ImmutableVec(+0f, -0f).computeDirection()).isEqualTo(Angle.degreesToRadians(-0f))
+ assertThat(ImmutableVec(-0f, +0f).computeDirection())
+ .isEqualTo(Angle.degreesToRadians(180f))
+ assertThat(ImmutableVec(-0f, -0f).computeDirection())
+ .isEqualTo(Angle.degreesToRadians(-180f))
}
@Test
fun unitVec_returnsCorrectValue() {
- assertThat(ImmutableVec(4f, 0f).unitVec).isEqualTo(ImmutableVec(1f, 0f))
- assertThat(MutableVec(0f, -25f).unitVec).isEqualTo(ImmutableVec(0f, -1f))
+ assertThat(ImmutableVec(4f, 0f).computeUnitVec()).isEqualTo(ImmutableVec(1f, 0f))
+ assertThat(MutableVec(0f, -25f).computeUnitVec()).isEqualTo(ImmutableVec(0f, -1f))
assertThat(
ImmutableVec(30f, 30f)
- .unitVec
+ .computeUnitVec()
.isAlmostEqual(ImmutableVec(sqrt(.5f), sqrt(.5f)), tolerance = 0.000001f)
)
.isTrue()
assertThat(
MutableVec(-.05f, -.05f)
- .unitVec
+ .computeUnitVec()
.isAlmostEqual(ImmutableVec(-sqrt(.5f), -sqrt(.5f)), tolerance = 0.000001f)
)
.isTrue()
@@ -99,33 +102,33 @@
@Test
fun unitVec_whenVecContainsZeroes_returnsCorrectValue() {
- assertThat(ImmutableVec(+0f, 0f).unitVec).isEqualTo(ImmutableVec(1f, 0f))
- assertThat(MutableVec(-0f, 0f).unitVec).isEqualTo(ImmutableVec(-1f, 0f))
+ assertThat(ImmutableVec(+0f, 0f).computeUnitVec()).isEqualTo(ImmutableVec(1f, 0f))
+ assertThat(MutableVec(-0f, 0f).computeUnitVec()).isEqualTo(ImmutableVec(-1f, 0f))
}
@Test
fun populateUnitVec_populatesCorrectValue() {
val mutableVec = MutableVec(0f, 0f)
- MutableVec(4f, 0f).populateUnitVec(mutableVec)
+ MutableVec(4f, 0f).computeUnitVec(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(1f, 0f))
- ImmutableVec(0f, -25f).populateUnitVec(mutableVec)
+ ImmutableVec(0f, -25f).computeUnitVec(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(0f, -1f))
- MutableVec(30f, 30f).populateUnitVec(mutableVec)
+ MutableVec(30f, 30f).computeUnitVec(mutableVec)
assertThat(mutableVec.isAlmostEqual(ImmutableVec(sqrt(.5f), sqrt(.5f)))).isTrue()
- ImmutableVec(-.05f, -.05f).populateUnitVec(mutableVec)
+ ImmutableVec(-.05f, -.05f).computeUnitVec(mutableVec)
assertThat(mutableVec.isAlmostEqual(ImmutableVec(-sqrt(.5f), -sqrt(.5f)))).isTrue()
}
@Test
fun populateUnitVec_whenVecContainsZeroes_populatesCorrectValue() {
val mutableVec = MutableVec(0f, 0f)
- MutableVec(+0f, 0f).populateUnitVec(mutableVec)
+ MutableVec(+0f, 0f).computeUnitVec(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(1f, 0f))
- ImmutableVec(-0f, -0f).populateUnitVec(mutableVec)
+ ImmutableVec(-0f, -0f).computeUnitVec(mutableVec)
assertThat(mutableVec).isEqualTo(ImmutableVec(-1f, 0f))
}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/BatchedMotionEvent.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/BatchedMotionEvent.java
index 9595006..9639efb 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/BatchedMotionEvent.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/BatchedMotionEvent.java
@@ -71,7 +71,7 @@
return mMotionEvent;
}
- public @NonNull int getPointerCount() {
+ public int getPointerCount() {
return mPointerCount;
}
diff --git a/libraryversions.toml b/libraryversions.toml
index c937e54..1dee05f 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,9 +1,9 @@
[versions]
-ACTIVITY = "1.10.0-beta01"
+ACTIVITY = "1.10.0-alpha02"
ANNOTATION = "1.9.0-alpha02"
ANNOTATION_EXPERIMENTAL = "1.5.0-alpha01"
APPCOMPAT = "1.8.0-alpha01"
-APPSEARCH = "1.1.0-alpha04"
+APPSEARCH = "1.1.0-alpha05"
ARCH_CORE = "2.3.0-alpha01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
AUTOFILL = "1.3.0-alpha02"
@@ -21,12 +21,12 @@
COLLECTION = "1.5.0-alpha01"
COMPOSE = "1.8.0-alpha01"
COMPOSE_MATERIAL3 = "1.4.0-alpha01"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-alpha01"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-alpha02"
COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-beta01"
-CONSTRAINTLAYOUT = "2.2.0-alpha14"
-CONSTRAINTLAYOUT_COMPOSE = "1.1.0-alpha14"
-CONSTRAINTLAYOUT_CORE = "1.1.0-alpha14"
+CONSTRAINTLAYOUT = "2.2.0-beta01"
+CONSTRAINTLAYOUT_COMPOSE = "1.1.0-beta01"
+CONSTRAINTLAYOUT_CORE = "1.1.0-beta01"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha02"
CORE = "1.15.0-alpha02"
@@ -40,10 +40,10 @@
CORE_PERFORMANCE = "1.0.0"
CORE_REMOTEVIEWS = "1.1.0-rc01"
CORE_ROLE = "1.2.0-alpha01"
-CORE_SPLASHSCREEN = "1.2.0-alpha01"
-CORE_TELECOM = "1.0.0-alpha13"
+CORE_SPLASHSCREEN = "1.2.0-alpha02"
+CORE_TELECOM = "1.0.0-alpha4"
CORE_UWB = "1.0.0-alpha08"
-CREDENTIALS = "1.5.0-alpha04"
+CREDENTIALS = "1.5.0-alpha05"
CREDENTIALS_E2EE_QUARANTINE = "1.0.0-alpha02"
CREDENTIALS_FIDO_QUARANTINE = "1.0.0-alpha02"
CURSORADAPTER = "1.1.0-alpha01"
@@ -96,7 +96,7 @@
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.8.0-alpha01"
METRICS = "1.0.0-beta02"
-NAVIGATION = "2.8.0-rc01"
+NAVIGATION = "2.9.0-alpha01"
PAGING = "3.4.0-alpha01"
PALETTE = "1.1.0-alpha01"
PDF = "1.0.0-alpha02"
@@ -109,7 +109,7 @@
PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha14"
PRIVACYSANDBOX_TOOLS = "1.0.0-alpha09"
PRIVACYSANDBOX_UI = "1.0.0-alpha09"
-PROFILEINSTALLER = "1.4.0-beta01"
+PROFILEINSTALLER = "1.4.0-rc01"
RECOMMENDATION = "1.1.0-alpha01"
RECYCLERVIEW = "1.4.0-beta01"
RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
@@ -134,7 +134,7 @@
SQLITE = "2.5.0-alpha07"
SQLITE_INSPECTOR = "2.1.0-alpha01"
STABLE_AIDL = "1.0.0-alpha01"
-STARTUP = "1.2.0-beta01"
+STARTUP = "1.2.0-rc01"
SWIPEREFRESHLAYOUT = "1.2.0-alpha01"
TESTEXT = "1.0.0-alpha03"
TESTSCREENSHOT = "1.0.0-alpha01"
@@ -168,11 +168,11 @@
WEAR_WATCHFACE = "1.3.0-alpha03"
WEBKIT = "1.12.0-beta01"
# Adding a comment to prevent merge conflicts for Window artifact
-WINDOW = "1.4.0-alpha01"
-WINDOW_EXTENSIONS = "1.4.0-alpha01"
+WINDOW = "1.4.0-alpha02"
+WINDOW_EXTENSIONS = "1.4.0-beta01"
WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
-WORK = "2.10.0-alpha02"
+WORK = "2.10.0-alpha03"
XR = "1.0.0-alpha01"
[groups]
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index b4b5ecc..4799f00 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -83,8 +83,10 @@
RestrictToDetector.RESTRICTED,
ObsoleteCompatDetector.ISSUE,
ReplaceWithDetector.ISSUE,
- // This issue is only enabled when `-Pandroidx.migrateArrayAnnotations=true`.
- ArrayNullnessMigration.ISSUE,
+ // This issue is only enabled when `-Pandroidx.useJSpecifyAnnotations=true`.
+ JSpecifyNullnessMigration.ISSUE,
+ TypeMirrorToString.ISSUE,
+ BanNullMarked.ISSUE,
)
}
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/ArrayNullnessMigration.kt b/lint-checks/src/main/java/androidx/build/lint/ArrayNullnessMigration.kt
deleted file mode 100644
index 7e741be..0000000
--- a/lint-checks/src/main/java/androidx/build/lint/ArrayNullnessMigration.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.lint
-
-import com.android.tools.lint.client.api.UElementHandler
-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.Location
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.isKotlin
-import com.intellij.psi.PsiArrayType
-import com.intellij.psi.PsiEllipsisType
-import java.util.EnumSet
-import org.jetbrains.uast.UAnnotation
-import org.jetbrains.uast.UElement
-import org.jetbrains.uast.UField
-import org.jetbrains.uast.UMethod
-import org.jetbrains.uast.UParameter
-
-/**
- * Repositions nullness annotations on arrays to facilitate migrating the nullness annotations to
- * TYPE_USE. See the issue description in the companion object for more detail.
- */
-class ArrayNullnessMigration : Detector(), Detector.UastScanner {
- override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
-
- override fun createUastHandler(context: JavaContext): UElementHandler {
- return AnnotationChecker(context)
- }
-
- private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
- override fun visitAnnotation(node: UAnnotation) {
- // Nullness annotations are only relevant for Java source.
- if (isKotlin(node.lang)) return
-
- // Verify this is a nullness annotation.
- val annotationName = node.qualifiedName ?: return
- if (annotationName !in nullnessAnnotations) return
-
- // Find the type of the annotated element, and only continue if it is an array.
- val annotated = node.uastParent ?: return
- val type =
- when (annotated) {
- is UParameter -> annotated.type
- is UMethod -> annotated.returnType
- is UField -> annotated.type
- else -> return
- }
- if (type !is PsiArrayType) return
-
- // Determine the file location for the autofix. This is a bit complicated because it
- // needs to avoid editing the wrong thing, like the doc comment preceding a method, but
- // also is doing some reformatting in the area around the annotation.
- // This is where the annotation itself is located.
- val annotationLocation = context.getLocation(node)
- // This is where the element being annotated is located.
- val annotatedLocation = context.getLocation(annotated as UElement)
- // If the annotation and annotated element aren't on the same line, that probably means
- // the annotation is on its own line, with indentation before it. To also get rid of
- // that indentation, start the range at the start of the annotation's line.
- // If the annotation and annotated element are on the same line, just start at the
- // annotation starting place to avoid including e.g. other parameters.
- val startLocation =
- if (annotatedLocation.start!!.sameLine(annotationLocation.start!!)) {
- annotationLocation.start!!
- } else {
- Location.create(
- context.file,
- context.getContents()!!.toString(),
- annotationLocation.start!!.line
- )
- .start!!
- }
- val fixLocation =
- Location.create(annotatedLocation.file, startLocation, annotatedLocation.end)
-
- // Part 1 of the fix: remove the original annotation
- val annotationString = node.asSourceString()
- val removeOriginalAnnotation =
- fix()
- .replace()
- .range(fixLocation)
- // In addition to the annotation, also remove any extra whitespace and trailing
- // new line. The reformat option unfortunately doesn't do this.
- .pattern("(( )*$annotationString ?\n?)")
- .with("")
- // Only remove one instance of the annotation.
- .repeatedly(false)
- .autoFix()
- .build()
-
- // Vararg types are also arrays, determine which array marker is present here.
- val arraySuffix =
- if (type is PsiEllipsisType) {
- "..."
- } else {
- "[]"
- }
- // Part 2 of the fix: add a new annotation.
- val addNewAnnotation =
- fix()
- .replace()
- .range(fixLocation)
- .text(arraySuffix)
- .with(" $annotationString $arraySuffix")
- // Only add one instance of the annotation. This will replace the first instance
- // of []/..., which is correct. In `String @Nullable [][]` the annotation
- // applies to the outer `String[][]` type, while in `String[] @Nullable []` it
- // applies to the inner `String[]` arrays.
- .repeatedly(false)
- .autoFix()
- .build()
-
- // Combine the two elements of the fix and report.
- val fix =
- fix()
- .name("Move annotation")
- .composite()
- .add(removeOriginalAnnotation)
- .add(addNewAnnotation)
- .autoFix()
- .build()
-
- val incident =
- Incident(context)
- .message("Nullness annotation on array will apply to element")
- .issue(ISSUE)
- .location(context.getLocation(annotated as UElement))
- .scope(annotated)
- .fix(fix)
- context.report(incident)
- }
- }
-
- companion object {
- val nullnessAnnotations =
- listOf(
- "androidx.annotation.NonNull",
- "androidx.annotation.Nullable",
- )
- val ISSUE =
- Issue.create(
- "ArrayMigration",
- "Migrate arrays to type-use nullness annotations",
- """
- When nullness annotations do not target TYPE_USE, the following definition means
- that the type of `arg` is nullable:
- @Nullable String[] arg
- However, if the annotation targets TYPE_USE, it now applies to the component
- type of the array, meaning that `arg`'s type is an array of nullable strings.
- To retain the original meaning, the definition needs to be changed to this:
- String @Nullable [] arg
- This check performs that migration to enable converting nullness annotations to
- target TYPE_USE.
- """,
- Category.CORRECTNESS,
- 5,
- Severity.ERROR,
- Implementation(
- ArrayNullnessMigration::class.java,
- EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
- )
- )
- }
-}
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanNullMarked.kt b/lint-checks/src/main/java/androidx/build/lint/BanNullMarked.kt
new file mode 100644
index 0000000..9b735cc
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/BanNullMarked.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UAnnotation
+
+class BanNullMarked : Detector(), Detector.UastScanner {
+
+ override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return AnnotationChecker(context)
+ }
+
+ private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
+ override fun visitAnnotation(node: UAnnotation) {
+ if (node.qualifiedName != "org.jspecify.annotations.NullMarked") return
+
+ val incident =
+ Incident(context)
+ .issue(ISSUE)
+ .location(context.getLocation(node))
+ .scope(node)
+ .message("Should not use @NullMarked annotation")
+ context.report(incident)
+ }
+ }
+
+ companion object {
+ val ISSUE =
+ Issue.create(
+ "BanNullMarked",
+ "Should not use @NullMarked annotation",
+ "Usage of the @NullMarked annotation is not allowed because lint and metalava do not support it",
+ Category.CORRECTNESS,
+ 5,
+ Severity.ERROR,
+ Implementation(BanNullMarked::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/JSpecifyNullnessMigration.kt b/lint-checks/src/main/java/androidx/build/lint/JSpecifyNullnessMigration.kt
new file mode 100644
index 0000000..a57fe59
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/JSpecifyNullnessMigration.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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.LintFix
+import com.android.tools.lint.detector.api.Location
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.isKotlin
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiEllipsisType
+import com.intellij.psi.PsiPrimitiveType
+import java.util.EnumSet
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UField
+import org.jetbrains.uast.ULocalVariable
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+
+/**
+ * Repositions nullness annotations to facilitate migrating the nullness annotations to JSpecify
+ * TYPE_USE annotations. See the issue description in the companion object for more detail.
+ */
+class JSpecifyNullnessMigration : Detector(), Detector.UastScanner {
+ override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return AnnotationChecker(context)
+ }
+
+ private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
+ override fun visitAnnotation(node: UAnnotation) {
+ // Nullness annotations are only relevant for Java source.
+ if (isKotlin(node.lang)) return
+
+ // Verify this is a nullness annotation.
+ val annotationName = node.qualifiedName ?: return
+ if (annotationName !in nullnessAnnotations.keys) return
+ val replacementAnnotationName = nullnessAnnotations[annotationName]!!
+
+ val fix = createFix(node, replacementAnnotationName)
+ val incident =
+ Incident(context)
+ .message("Switch nullness annotation to JSpecify")
+ .issue(ISSUE)
+ .location(context.getLocation(node as UElement))
+ .scope(node)
+ .fix(fix)
+ context.report(incident)
+ }
+
+ fun createFix(node: UAnnotation, replacementAnnotationName: String): LintFix? {
+ // Find the type of the annotated element.
+ val annotated = node.uastParent ?: return null
+ val type =
+ when (annotated) {
+ is UParameter -> annotated.type
+ is UMethod -> annotated.returnType
+ is UField -> annotated.type
+ is ULocalVariable -> annotated.type
+ else -> return null
+ } ?: return null
+
+ // Determine the file location for the autofix. This is a bit complicated because it
+ // needs to avoid editing the wrong thing, like the doc comment preceding a method, but
+ // also is doing some reformatting in the area around the annotation.
+ // This is where the annotation itself is located.
+ val annotationLocation = context.getLocation(node)
+ // This is where the element being annotated is located.
+ val annotatedLocation = context.getLocation(annotated as UElement)
+ // If the annotation and annotated element aren't on the same line, that probably means
+ // the annotation is on its own line, with indentation before it. To also get rid of
+ // that indentation, start the range at the start of the annotation's line.
+ // If the annotation and annotated element are on the same line, just start at the
+ // annotation starting place to avoid including e.g. other parameters.
+ val annotatedStart = annotatedLocation.start ?: return null
+ val annotationStart = annotationLocation.start ?: return null
+ val startLocation =
+ if (annotatedStart.sameLine(annotationStart)) {
+ annotationStart
+ } else {
+ Location.create(
+ context.file,
+ context.getContents()!!.toString(),
+ annotationStart.line
+ )
+ .start!!
+ }
+ val fixLocation =
+ Location.create(annotatedLocation.file, startLocation, annotatedLocation.end)
+
+ // Part 1 of the fix: remove the original annotation
+ val annotationString = node.asSourceString()
+ val removeOriginalAnnotation =
+ fix()
+ .replace()
+ .range(fixLocation)
+ // In addition to the annotation, also remove any extra whitespace and trailing
+ // new line. The reformat option unfortunately doesn't do this.
+ .pattern("(( )*$annotationString ?\n?)")
+ .with("")
+ // Only remove one instance of the annotation.
+ .repeatedly(false)
+ .autoFix()
+ .build()
+
+ // The jspecify annotations can't be applied to primitive types (since primitives are
+ // non-null by definition) or local variables, so just remove the annotation in those
+ // cases. For all other cases, also add a new annotation to the correct position.
+ return if (type is PsiPrimitiveType || annotated is ULocalVariable) {
+ removeOriginalAnnotation
+ } else {
+ // Create a regex pattern for where to insert the annotation. The replacement lint
+ // removes the first capture group (section in parentheses) of the supplied regex.
+ // Since this fix is really just to insert an annotation, use an empty capture group
+ // so nothing is removed.
+ val (prefix, textToReplace) =
+ when {
+ // For a vararg type where the component type is an array, the annotation
+ // goes before the array instead of the vararg ("String @NonNull []..."),
+ // so only match the "..." when the component isn't an array.
+ type is PsiEllipsisType && type.componentType !is PsiArrayType ->
+ Pair(" ", "()\\.\\.\\.")
+ type is PsiArrayType -> Pair(" ", "()\\[\\]")
+ // Make sure to match the right usage of the class name: find the name
+ // preceded by a space or dot, and followed by a space, open angle bracket,
+ // or newline character.
+ type is PsiClassType -> Pair("", "[ .]()${type.className}[ <\\n\\r]")
+ else -> Pair("", "()${type.presentableText}")
+ }
+ val replacement = "$prefix@$replacementAnnotationName "
+
+ // Part 2 of the fix: add a new annotation.
+ val addNewAnnotation =
+ fix()
+ .replace()
+ .range(fixLocation)
+ .pattern(textToReplace)
+ .with(replacement)
+ // Only add one instance of the annotation. For nested array types, this
+ // will replace the first instance of []/..., which is correct. In
+ // `String @Nullable [][]` the annotation applies to the outer `String[][]`
+ // type, while in `String[] @Nullable []` it applies to the inner `String[]`
+ // arrays.
+ .repeatedly(false)
+ .shortenNames()
+ .autoFix()
+ .build()
+
+ // Combine the two elements of the fix.
+ return fix()
+ .name("Move annotation")
+ .composite()
+ .add(removeOriginalAnnotation)
+ .add(addNewAnnotation)
+ .autoFix()
+ .build()
+ }
+ }
+ }
+
+ companion object {
+ val nullnessAnnotations =
+ mapOf(
+ "androidx.annotation.NonNull" to "NonNull",
+ "androidx.annotation.Nullable" to "Nullable",
+ )
+ val ISSUE =
+ Issue.create(
+ "JSpecifyNullness",
+ "Migrate nullness annotations to type-use position",
+ """
+ Switches from AndroidX nullness annotations to JSpecify, which are type-use.
+ Type-use annotations have different syntactic positions than non-type-use
+ annotations in some cases.
+
+ For instance, when nullness annotations do not target TYPE_USE, the following
+ definition means that the type of `arg` is nullable:
+ @Nullable String[] arg
+ However, if the annotation targets TYPE_USE, it now applies to the component
+ type of the array, meaning that `arg`'s type is an array of nullable strings.
+ To retain the original meaning, the definition needs to be changed to this:
+ String @Nullable [] arg
+
+ Type-use nullness annotations must go before the simple class name of a
+ qualified type. For instance, `java.lang.@Nullable String` is required instead
+ of `@Nullable java.lang.String`.
+ """,
+ Category.CORRECTNESS,
+ 5,
+ Severity.ERROR,
+ Implementation(
+ JSpecifyNullnessMigration::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/TypeMirrorToString.kt b/lint-checks/src/main/java/androidx/build/lint/TypeMirrorToString.kt
new file mode 100644
index 0000000..408f826
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/TypeMirrorToString.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiClass
+import org.jetbrains.uast.UCallExpression
+
+class TypeMirrorToString : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return TypeMirrorHandler(context)
+ }
+
+ private inner class TypeMirrorHandler(private val context: JavaContext) : UElementHandler() {
+ override fun visitCallExpression(node: UCallExpression) {
+ if (node.methodName != "toString") return
+ val method = node.resolve() ?: return
+ val containingClass = method.containingClass ?: return
+ if (containingClass.isInstanceOf("javax.lang.model.type.TypeMirror")) {
+ // Instead of calling `receiver.toString()`, call `TypeName.get(receiver).toString`
+ val fix =
+ node.receiver?.asSourceString()?.let { receiver ->
+ fix()
+ .replace()
+ .name("Use TypeName.toString")
+ .text(receiver)
+ .with("com.squareup.javapoet.TypeName.get($receiver)")
+ .reformat(true)
+ .shortenNames()
+ .autoFix()
+ .build()
+ }
+
+ val incident =
+ Incident(context)
+ .fix(fix)
+ .issue(ISSUE)
+ .location(context.getLocation(node))
+ .message("TypeMirror.toString includes annotations")
+ .scope(node)
+ context.report(incident)
+ }
+ }
+
+ /** Checks if the class is [qualifiedName] or has [qualifiedName] as a super type. */
+ private fun PsiClass.isInstanceOf(qualifiedName: String): Boolean =
+ // Recursion will stop when this hits Object, which has no [supers]
+ qualifiedName == this.qualifiedName || supers.any { it.isInstanceOf(qualifiedName) }
+ }
+
+ companion object {
+ val ISSUE =
+ Issue.create(
+ "TypeMirrorToString",
+ "Avoid using TypeMirror.toString",
+ """
+ This method includes type-use annotations in the string, which can lead to bugs
+ when comparing the type string against unannotated type string.
+
+ If you need a type string that includes annotations, you can suppress this lint.
+ """,
+ Category.CORRECTNESS,
+ 5,
+ Severity.ERROR,
+ Implementation(TypeMirrorToString::class.java, Scope.JAVA_FILE_SCOPE)
+ )
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/ArrayNullnessMigrationTest.kt b/lint-checks/src/test/java/androidx/build/lint/ArrayNullnessMigrationTest.kt
deleted file mode 100644
index 556cf75..0000000
--- a/lint-checks/src/test/java/androidx/build/lint/ArrayNullnessMigrationTest.kt
+++ /dev/null
@@ -1,359 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.lint
-
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestMode
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ArrayNullnessMigrationTest :
- AbstractLintDetectorTest(
- useDetector = ArrayNullnessMigration(),
- useIssues = listOf(ArrayNullnessMigration.ISSUE),
- stubs = annotationStubs
- ) {
- @Test
- fun `Nullness annotation on parameter`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.NonNull;
- public class Foo {
- public void foo(@NonNull String[] arr) {}
- }
- """
- .trimIndent()
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:4: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public void foo(@NonNull String[] arr) {}
- ~~~~~~~~~~~~~~~~~~~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 4: Move annotation:
- @@ -4 +4
- - public void foo(@NonNull String[] arr) {}
- + public void foo(String @NonNull [] arr) {}
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on method return`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.Nullable;
- public class Foo {
- @Nullable
- public String[] foo() { return null; }
- }
- """
- .trimIndent(),
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:5: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public String[] foo() { return null; }
- ~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 5: Move annotation:
- @@ -4 +4
- - @Nullable
- - public String[] foo() { return null; }
- + public String @Nullable [] foo() { return null; }
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on method return and parameter`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.Nullable;
- public class Foo {
- @Nullable
- public String[] foo(@Nullable String[] arr) { return null; }
- }
- """
- .trimIndent()
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:5: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public String[] foo(@Nullable String[] arr) { return null; }
- ~~~
- src/test/pkg/Foo.java:5: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public String[] foo(@Nullable String[] arr) { return null; }
- ~~~~~~~~~~~~~~~~~~~~~~
- 2 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 5: Move annotation:
- @@ -4 +4
- - @Nullable
- - public String[] foo(@Nullable String[] arr) { return null; }
- + public String @Nullable [] foo(@Nullable String[] arr) { return null; }
- Autofix for src/test/pkg/Foo.java line 5: Move annotation:
- @@ -5 +5
- - public String[] foo(@Nullable String[] arr) { return null; }
- + public String[] foo(String @Nullable [] arr) { return null; }
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on field`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.Nullable;
- public class Foo {
- @Nullable public String[] foo;
- }
- """
- .trimIndent()
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:4: Error: Nullness annotation on array will apply to element [ArrayMigration]
- @Nullable public String[] foo;
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 4: Move annotation:
- @@ -4 +4
- - @Nullable public String[] foo;
- + public String @Nullable [] foo;
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on 2d array`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.Nullable;
- public class Foo {
- @Nullable public String[][] foo;
- }
- """
- .trimIndent()
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:4: Error: Nullness annotation on array will apply to element [ArrayMigration]
- @Nullable public String[][] foo;
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 4: Move annotation:
- @@ -4 +4
- - @Nullable public String[][] foo;
- + public String @Nullable [][] foo;
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on varargs`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.NonNull;
- public class Foo {
- public void foo(@NonNull String... arr) {}
- }
- """
- .trimIndent(),
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:4: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public void foo(@NonNull String... arr) {}
- ~~~~~~~~~~~~~~~~~~~~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 4: Move annotation:
- @@ -4 +4
- - public void foo(@NonNull String... arr) {}
- + public void foo(String @NonNull ... arr) {}
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on method return with array in comments`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.Nullable;
- public class Foo {
- /**
- * @return A String[]
- */
- @Nullable
- public String[] foo() { return null; }
- }
- """
- .trimIndent(),
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:8: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public String[] foo() { return null; }
- ~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 8: Move annotation:
- @@ -7 +7
- - @Nullable
- - public String[] foo() { return null; }
- + public String @Nullable [] foo() { return null; }
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- @Test
- fun `Nullness annotation on method return with annotation in comments`() {
- val input =
- java(
- """
- package test.pkg;
- import androidx.annotation.Nullable;
- public class Foo {
- /**
- * @return A @Nullable string array
- */
- @Nullable
- public String[] foo() { return null; }
- }
- """
- .trimIndent(),
- )
-
- val expected =
- """
- src/test/pkg/Foo.java:8: Error: Nullness annotation on array will apply to element [ArrayMigration]
- public String[] foo() { return null; }
- ~~~
- 1 errors, 0 warnings
- """
- .trimIndent()
-
- val expectedFixDiffs =
- """
- Autofix for src/test/pkg/Foo.java line 8: Move annotation:
- @@ -7 +7
- - @Nullable
- - public String[] foo() { return null; }
- + public String @Nullable [] foo() { return null; }
- """
- .trimIndent()
-
- runArrayNullnessTest(input, expected, expectedFixDiffs)
- }
-
- private fun runArrayNullnessTest(input: TestFile, expected: String, expectedFixDiffs: String) {
- lint()
- .files(*stubs, input)
- .skipTestModes(TestMode.WHITESPACE)
- .run()
- .expect(expected)
- .expectFixDiffs(expectedFixDiffs)
- }
-
- companion object {
- val annotationStubs =
- arrayOf(
- kotlin(
- """
- package androidx.annotation
- annotation class NonNull
- """
- ),
- kotlin(
- """
- package androidx.annotation
- annotation class Nullable
- """
- )
- )
- }
-}
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanNullMarkedTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanNullMarkedTest.kt
new file mode 100644
index 0000000..c16ea81
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanNullMarkedTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BanNullMarkedTest :
+ AbstractLintDetectorTest(
+ useDetector = BanNullMarked(),
+ useIssues = listOf(BanNullMarked.ISSUE),
+ stubs = arrayOf(nullMarkedStub)
+ ) {
+ @Test
+ fun `Usage of NullMarked in a package-info file`() {
+ val input =
+ java(
+ """
+ @NullMarked
+ package test.pkg;
+
+ import org.jspecify.annotations.NullMarked;
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/package-info.java:1: Error: Should not use @NullMarked annotation [BanNullMarked]
+ @NullMarked
+ ~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ check(input).expect(expected)
+ }
+
+ @Test
+ fun `Usage of NullMarked on a class`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+
+ import org.jspecify.annotations.NullMarked;
+
+ @NullMarked
+ public class Foo {}
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:5: Error: Should not use @NullMarked annotation [BanNullMarked]
+ @NullMarked
+ ~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ check(input).expect(expected)
+ }
+
+ @Test
+ fun `Usage of NullMarked on a method`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+
+ import org.jspecify.annotations.NullMarked;
+
+ public class Foo {
+ @NullMarked
+ public void foo() {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:6: Error: Should not use @NullMarked annotation [BanNullMarked]
+ @NullMarked
+ ~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ check(input).expect(expected)
+ }
+
+ @Test
+ fun `Usage of NullMarked on a constructor`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+
+ import org.jspecify.annotations.NullMarked;
+
+ public class Foo {
+ @NullMarked
+ public Foo() {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:6: Error: Should not use @NullMarked annotation [BanNullMarked]
+ @NullMarked
+ ~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ check(input).expect(expected)
+ }
+
+ companion object {
+ private val nullMarkedStub =
+ java(
+ """
+ package org.jspecify.annotations;
+
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Target;
+
+ @Target({ElementType.MODULE, ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
+ public @interface NullMarked {}
+ """
+ .trimIndent()
+ )
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/JSpecifyNullnessMigrationTest.kt b/lint-checks/src/test/java/androidx/build/lint/JSpecifyNullnessMigrationTest.kt
new file mode 100644
index 0000000..2b91a0a
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/JSpecifyNullnessMigrationTest.kt
@@ -0,0 +1,790 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestMode
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class JSpecifyNullnessMigrationTest :
+ AbstractLintDetectorTest(
+ useDetector = JSpecifyNullnessMigration(),
+ useIssues = listOf(JSpecifyNullnessMigration.ISSUE),
+ stubs = annotationStubs
+ ) {
+ @Test
+ fun `Nullness annotation on array parameter`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.NonNull;
+ public class Foo {
+ public void foo(@NonNull String[] arr) {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public void foo(@NonNull String[] arr) {}
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - public void foo(@NonNull String[] arr) {}
+ + public void foo(String @NonNull [] arr) {}
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on array method return`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable
+ public String[] foo() { return null; }
+ }
+ """
+ .trimIndent(),
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - @Nullable
+ - public String[] foo() { return null; }
+ + public String @Nullable [] foo() { return null; }
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on array method return and array parameter`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable
+ public String[] foo(@Nullable String[] arr) { return null; }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ src/test/pkg/Foo.java:5: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public String[] foo(@Nullable String[] arr) { return null; }
+ ~~~~~~~~~
+ 2 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - @Nullable
+ - public String[] foo(@Nullable String[] arr) { return null; }
+ + public String @Nullable [] foo(@Nullable String[] arr) { return null; }
+ Autofix for src/test/pkg/Foo.java line 5: Move annotation:
+ @@ -5 +5
+ - public String[] foo(@Nullable String[] arr) { return null; }
+ + public String[] foo(String @Nullable [] arr) { return null; }
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on array field`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable public String[] foo;
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable public String[] foo;
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - @Nullable public String[] foo;
+ + public String @Nullable [] foo;
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on 2d array`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable public String[][] foo;
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable public String[][] foo;
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - @Nullable public String[][] foo;
+ + public String @Nullable [][] foo;
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on varargs`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.NonNull;
+ public class Foo {
+ public void foo(@NonNull String... arr) {}
+ }
+ """
+ .trimIndent(),
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public void foo(@NonNull String... arr) {}
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - public void foo(@NonNull String... arr) {}
+ + public void foo(String @NonNull ... arr) {}
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on array varargs`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.NonNull;
+ public class Foo {
+ public void foo(@NonNull String[]... args) {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public void foo(@NonNull String[]... args) {}
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - public void foo(@NonNull String[]... args) {}
+ + public void foo(String @NonNull []... args) {}
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on method return with array in comments`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ /**
+ * @return A String[]
+ */
+ @Nullable
+ public String[] foo() { return null; }
+ }
+ """
+ .trimIndent(),
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:7: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 7: Move annotation:
+ @@ -7 +7
+ - @Nullable
+ - public String[] foo() { return null; }
+ + public String @Nullable [] foo() { return null; }
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on method return with annotation in comments`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ /**
+ * @return A @Nullable string array
+ */
+ @Nullable
+ public String[] foo() { return null; }
+ }
+ """
+ .trimIndent(),
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:7: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 7: Move annotation:
+ @@ -7 +7
+ - @Nullable
+ - public String[] foo() { return null; }
+ + public String @Nullable [] foo() { return null; }
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation removed from local variable declaration`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ public String foo() {
+ @Nullable String str = null;
+ return str;
+ }
+ }
+ """
+ .trimIndent()
+ )
+ val expected =
+ """
+ src/test/pkg/Foo.java:5: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable String str = null;
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 5: Delete:
+ @@ -5 +5
+ - @Nullable String str = null;
+ + String str = null;
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation removed from void return type`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable
+ public void foo() {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Delete:
+ @@ -4 +4
+ - @Nullable
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation removed from primitive parameter type`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ public void foo(@Nullable int i) {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public void foo(@Nullable int i) {}
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Delete:
+ @@ -4 +4
+ - public void foo(@Nullable int i) {}
+ + public void foo(int i) {}
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on class type parameter`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.NonNull;
+ public class Foo {
+ public void foo(@NonNull Foo.InnerFoo arr) {}
+ public class InnerFoo {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public void foo(@NonNull Foo.InnerFoo arr) {}
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - public void foo(@NonNull Foo.InnerFoo arr) {}
+ + public void foo(Foo.@NonNull InnerFoo arr) {}
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on class type return`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable
+ public String foo() { return null; }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - @Nullable
+ - public String foo() { return null; }
+ + public @Nullable String foo() { return null; }
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on class type param`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ public String foo(@Nullable String foo) { return null; }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public String foo(@Nullable String foo) { return null; }
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs = ""
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on class type param and return`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.NonNull;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable
+ public String foo(@NonNull String foo) { return null; }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:5: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ src/test/pkg/Foo.java:6: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public String foo(@NonNull String foo) { return null; }
+ ~~~~~~~~
+ 2 errors, 0 warnings
+ """
+ .trimIndent()
+
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 5: Move annotation:
+ @@ -5 +5
+ - @Nullable
+ - public String foo(@NonNull String foo) { return null; }
+ + public @Nullable String foo(@NonNull String foo) { return null; }
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on type parameter return`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class Foo {
+ @Nullable
+ public <T> T foo() {
+ return null;
+ }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 4: Move annotation:
+ @@ -4 +4
+ - @Nullable
+ - public <T> T foo() {
+ + public <T> @Nullable T foo() {
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on inner type where outer type contains name`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ public class RecyclerView {
+ public class Recycler {}
+ @Nullable
+ public RecyclerView.Recycler foo() {
+ return null;
+ }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/RecyclerView.java:5: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/RecyclerView.java line 5: Move annotation:
+ @@ -5 +5
+ - @Nullable
+ - public RecyclerView.Recycler foo() {
+ + public RecyclerView.@Nullable Recycler foo() {
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on parameterized type`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.Nullable;
+ import java.util.List;
+ public class Foo {
+ @Nullable
+ public List<String> foo() {
+ return null;
+ }
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:5: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ @Nullable
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Autofix for src/test/pkg/Foo.java line 5: Move annotation:
+ @@ -5 +5
+ - @Nullable
+ - public List<String> foo() {
+ + public @Nullable List<String> foo() {
+ """
+ .trimIndent()
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ @Test
+ fun `Nullness annotation on parameter with newline after type`() {
+ val input =
+ java(
+ """
+ package test.pkg;
+ import androidx.annotation.NonNull;
+ public class Foo {
+ public void foo(@NonNull String
+ arg) {}
+ }
+ """
+ .trimIndent()
+ )
+
+ val expected =
+ """
+ src/test/pkg/Foo.java:4: Error: Switch nullness annotation to JSpecify [JSpecifyNullness]
+ public void foo(@NonNull String
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs = ""
+
+ runNullnessTest(input, expected, expectedFixDiffs)
+ }
+
+ private fun runNullnessTest(input: TestFile, expected: String, expectedFixDiffs: String) {
+ lint()
+ .files(*stubs, input)
+ // Skip WHITESPACE mode because an array suffix with whitespace in the middle "[ ]" will
+ // break the pattern matching, but is extremely unlikely to happen in practice.
+ // Skip FULLY_QUALIFIED mode because the type-use annotation positioning depends on
+ // whether types are fully qualified, so fixes will be different.
+ .skipTestModes(TestMode.WHITESPACE, TestMode.FULLY_QUALIFIED)
+ .run()
+ .expect(expected)
+ .expectFixDiffs(expectedFixDiffs)
+ }
+
+ companion object {
+ val annotationStubs =
+ arrayOf(
+ kotlin(
+ """
+ package androidx.annotation
+ annotation class NonNull
+ """
+ ),
+ kotlin(
+ """
+ package androidx.annotation
+ annotation class Nullable
+ """
+ )
+ )
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/TypeMirrorToStringTest.kt b/lint-checks/src/test/java/androidx/build/lint/TypeMirrorToStringTest.kt
new file mode 100644
index 0000000..ced7240
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/TypeMirrorToStringTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class TypeMirrorToStringTest :
+ AbstractLintDetectorTest(
+ useDetector = TypeMirrorToString(),
+ useIssues = listOf(TypeMirrorToString.ISSUE),
+ ) {
+ @Test
+ fun `Test usage TypeMirror#toString on simple receiver`() {
+ val input =
+ arrayOf(
+ java(
+ """
+ package androidx.test;
+ import javax.lang.model.type.TypeMirror;
+ public class Foo {
+ public String getStringForType(TypeMirror tm) {
+ return tm.toString();
+ }
+ }
+ """
+ .trimIndent()
+ )
+ )
+ val expected =
+ """
+ src/androidx/test/Foo.java:5: Error: TypeMirror.toString includes annotations [TypeMirrorToString]
+ return tm.toString();
+ ~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Autofix for src/androidx/test/Foo.java line 5: Use TypeName.toString:
+ @@ -2 +2
+ + import com.squareup.javapoet.TypeName;
+ @@ -5 +6
+ - return tm.toString();
+ + return TypeName.get(tm).toString();
+ """
+ .trimIndent()
+
+ check(*input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+
+ @Test
+ fun `Test usage of TypeMirror#toString on method call receiver`() {
+ val input =
+ arrayOf(
+ java(
+ """
+ package androidx.test;
+ import javax.lang.model.type.TypeMirror;
+ public class Foo {
+ public TypeMirror getMirror() {
+ return null;
+ }
+ public String getStringForType() {
+ return getMirror().toString();
+ }
+ }
+ """
+ .trimIndent()
+ )
+ )
+ val expected =
+ """
+ src/androidx/test/Foo.java:8: Error: TypeMirror.toString includes annotations [TypeMirrorToString]
+ return getMirror().toString();
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ val expectedFixDiffs =
+ """
+ Autofix for src/androidx/test/Foo.java line 8: Use TypeName.toString:
+ @@ -2 +2
+ + import com.squareup.javapoet.TypeName;
+ @@ -8 +9
+ - return getMirror().toString();
+ + return TypeName.get(getMirror()).toString();
+ """
+ .trimIndent()
+
+ check(*input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+}
diff --git a/mediarouter/mediarouter/src/main/res/values-in/strings.xml b/mediarouter/mediarouter/src/main/res/values-in/strings.xml
index a646d3d..87abce1 100644
--- a/mediarouter/mediarouter/src/main/res/values-in/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-in/strings.xml
@@ -25,7 +25,7 @@
<string name="mr_chooser_title" msgid="1419936397646839840">"Transmisikan ke"</string>
<string name="mr_chooser_searching" msgid="6114250663023140921">"Mencari perangkat"</string>
<string name="mr_chooser_looking_for_devices" msgid="4257319068277776035">"Mencari perangkat..."</string>
- <string name="mr_controller_disconnect" msgid="7812275474138309497">"Putuskan koneksi"</string>
+ <string name="mr_controller_disconnect" msgid="7812275474138309497">"Berhenti hubungkan"</string>
<string name="mr_controller_stop_casting" msgid="804210341192624074">"Hentikan transmisi"</string>
<string name="mr_controller_close_description" msgid="5684434439232634509">"Tutup"</string>
<string name="mr_controller_play" msgid="1253345086594430054">"Putar"</string>
diff --git a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
index 095938c..f063877 100644
--- a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
+++ b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
@@ -15,6 +15,7 @@
*/
package androidx.metrics.performance.test
+import android.os.Build.VERSION.SDK_INT
import android.view.Choreographer
import androidx.metrics.performance.FrameData
import androidx.metrics.performance.FrameDataApi24
@@ -36,6 +37,7 @@
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -107,6 +109,7 @@
@Test
fun testEnable() {
+ assumeTrue("Skip running an API 26 as it is flaky b/361092826", SDK_INT != 26)
assertTrue(jankStats.isTrackingEnabled)
jankStats.isTrackingEnabled = false
assertFalse(jankStats.isTrackingEnabled)
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt
index c2cf3c6..df00b37 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt
@@ -147,10 +147,14 @@
return Class.forName(className)
} catch (_: ClassNotFoundException) {}
}
- throw IllegalArgumentException(
+ var errorMsg =
"Cannot find class with name \"$serialName\". Ensure that the " +
"serialName for this argument is the default fully qualified name"
- )
+ if (kind is SerialKind.ENUM) {
+ errorMsg =
+ "$errorMsg.\nIf the build is minified, try annotating the Enum class with \"androidx.annotation.Keep\" to ensure the Enum is not removed."
+ }
+ throw IllegalArgumentException(errorMsg)
}
/**
diff --git a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt
index 0b7db49..f395dd1 100644
--- a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt
+++ b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt
@@ -825,7 +825,9 @@
assertThat(exception.message)
.isEqualTo(
"Cannot find class with name \"MyCustomSerialName\". Ensure that the " +
- "serialName for this argument is the default fully qualified name"
+ "serialName for this argument is the default fully qualified name." +
+ "\nIf the build is minified, try annotating the Enum class " +
+ "with \"androidx.annotation.Keep\" to ensure the Enum is not removed."
)
}
diff --git a/navigation/navigation-lint-common/build.gradle b/navigation/navigation-lint-common/build.gradle
index 4ed49c5..192dd51 100644
--- a/navigation/navigation-lint-common/build.gradle
+++ b/navigation/navigation-lint-common/build.gradle
@@ -31,6 +31,8 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
+ compileOnly(libs.androidLintTests)
+ compileOnly(libs.junit)
}
androidx {
diff --git a/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/LintUtil.kt b/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/LintUtil.kt
index 611f180..85d67c3 100644
--- a/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/LintUtil.kt
+++ b/navigation/navigation-lint-common/src/main/java/androidx/navigation/lint/common/LintUtil.kt
@@ -16,6 +16,11 @@
package androidx.navigation.lint.common
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import java.util.Locale
+import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind
import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
@@ -66,3 +71,60 @@
symbol.classKind == KtClassKind.COMPANION_OBJECT) to symbol.name?.asString()
}
}
+
+/**
+ * Copied from compose.lint.test.stubs
+ *
+ * Utility for creating a [kotlin] and corresponding [bytecode] stub, to try and make it easier to
+ * configure everything correctly.
+ *
+ * @param filename name of the Kotlin source file, with extension - e.g. "Test.kt". These should be
+ * unique across a test.
+ * @param filepath directory structure matching the package name of the Kotlin source file. E.g. if
+ * the source has `package foo.bar`, this should be `foo/bar`. If this does _not_ match, lint will
+ * not be able to match the generated classes with the source file, and so won't print them to
+ * console.
+ * @param source Kotlin source for the bytecode
+ * @param bytecode generated bytecode that will be used in tests. Leave empty to generate the
+ * bytecode for [source].
+ * @return a pair of kotlin test file, to bytecode test file
+ */
+fun kotlinAndBytecodeStub(
+ filename: String,
+ filepath: String,
+ checksum: Long,
+ @Language("kotlin") source: String,
+ vararg bytecode: String
+): KotlinAndBytecodeStub {
+ val filenameWithoutExtension = filename.substringBefore(".").lowercase(Locale.ROOT)
+ val kotlin = kotlin(source).to("$filepath/$filename")
+ val bytecodeStub =
+ TestFiles.bytecode("libs/$filenameWithoutExtension.jar", kotlin, checksum, *bytecode)
+ return KotlinAndBytecodeStub(kotlin, bytecodeStub)
+}
+
+class KotlinAndBytecodeStub(val kotlin: TestFile, val bytecode: TestFile)
+
+/**
+ * Copied from compose.lint.test.stubs
+ *
+ * Utility for creating a [bytecode] stub, to try and make it easier to configure everything
+ * correctly.
+ *
+ * @param filename name of the Kotlin source file, with extension - e.g. "Test.kt". These should be
+ * unique across a test.
+ * @param filepath directory structure matching the package name of the Kotlin source file. E.g. if
+ * the source has `package foo.bar`, this should be `foo/bar`. If this does _not_ match, lint will
+ * not be able to match the generated classes with the source file, and so won't print them to
+ * console.
+ * @param source Kotlin source for the bytecode
+ * @param bytecode generated bytecode that will be used in tests. Leave empty to generate the
+ * bytecode for [source].
+ */
+fun bytecodeStub(
+ filename: String,
+ filepath: String,
+ checksum: Long,
+ @Language("kotlin") source: String,
+ vararg bytecode: String
+): TestFile = kotlinAndBytecodeStub(filename, filepath, checksum, source, *bytecode).bytecode
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/Stub.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/Stub.kt
index 4b568a9..89df0cd 100644
--- a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/Stub.kt
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/Stub.kt
@@ -16,8 +16,244 @@
package androidx.navigation.runtime.lint
-val TEST_CLASS =
- """
+import androidx.navigation.lint.common.bytecodeStub
+import androidx.navigation.lint.common.kotlinAndBytecodeStub
+
+internal val NAV_CONTROLLER =
+ bytecodeStub(
+ "NavController.kt",
+ "androidx/navigation",
+ 0x40e8c1a8,
+ """
+package androidx.navigation
+
+import kotlin.reflect.KClass
+
+open class NavController {
+
+ fun navigate(resId: Int) {}
+
+ fun navigate(route: String) {}
+
+ fun <T : Any> navigate(route: T) {}
+}
+
+inline fun NavController.createGraph(
+ startDestination: Any,
+ route: KClass<*>? = null,
+): NavGraph { return NavGraph() }
+""",
+ """
+META-INF/main.kotlin_module:
+H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgMuQSTsxLKcrPTKnQy0ssy0xPLMnM
+zxPi90ssc87PKynKz8lJLfIuEeIECnjkF5d4l3CJcnEn5+fqpVYk5hbkpAqx
+haSChJUYtBgAOMX57WIAAAA=
+""",
+ """
+androidx/navigation/NavController.class:
+H4sIAAAAAAAA/41SW28SQRT+ZoFl2dIW0NYWW7UXLW21Sxt9kaaJNjFug2gs
+4aVPA2zowDKb7A6kj/wW/4FPGh9M46M/ynhmIb1qdJM99++bc2bOz1/fvgN4
+jj2GFS7bYSDaZ47kQ9HhSgTSqfHhYSBVGPi+F6bBGHJdPuSOz2XHed/sei2V
+RoLB3BdSqAOGRGmzkUUKpo0k0gxJdSoihrXqP9krDNYk5xGu5G42GFKhF7lt
+BuYyzJWql2cfq1DITkXXrFWDsON0PdUMuZCRw6UMVHxA5NQCVRv4fkUzBQPl
+WcgzPOgFyhfS6Q77jpDKCyX3HVdqxki0ojTu0GGtU6/Vm8A/8JD3PSpk2Lja
+xPgCKn9qK4s5zNu4i3sMhdsFN6aZEOlplvbrL29nDkr1epwu3M4x5KuTid55
+ire54hQz+sMEPS3TIqMF6BZ7FD8T2iuT1d5lCM9HS7axYNhG7nxkG5Y26LeS
+pC36Z6z5hfPRnlFmr1M/PpmUPFrOJYpGOblqWeejXGqLUntmziwab8cF6aOZ
+cQFFLdKZKz5VlW19Mu2b7qdO+3RtCXZ6it7+MGjTCsxWhfRqg37TC+u86Xt6
++KDF/QYPhfYnwfWPA6lE33PlUESCQhev9epyERgyx6IjuRqEBLGPg0HY8t4I
+jV+c4Btj9BUQVmDQFusvSd3SUpPcJs/RvZNObX2B9ZkMA09JmnHQxDOS2XEB
+MrBJ5zEVRzT4RVxP498EWjFwfpycALU1jRmSmmKWcpqiQlpXpbcLha9YuE5k
+EvCSKH1BlCaKRcrvaFv3n5s0VkTif1izf2VdorwTV9+/zm6gHMst7JKuUnSZ
+ruTBCRIuHrp45NINr5KJNRfreHwCFuEJNk4wFcGOUIpgRtomYzNCPkIxwnTs
+ln4DameR7LoEAAA=
+""",
+ """
+androidx/navigation/NavControllerKt.class:
+H4sIAAAAAAAA/61TbU8TQRB+9o62RxUoRSqv9YWqgC9XKr6WEA0GvVjQiCEx
+JJqlXcrS6x252zYmJuon/4P/wm8aTQzxoz/KOHseCCLiBz7s7Mzs7DPPzM5+
+//H5K4BpzDCMca8W+LL20vZ4W9a5kr5nL/L2nO+pwHddETxUKTCGzAZvc9vl
+Xt1+tLohquQ1GY5VA8GVuB/wzXUGd7xyKFy58idQudLwlSs9OxBrLtn2wzmX
+h2F54iCwKFuZwT/KdDOTs4dnHKv4Qd3eEGo14NILbe55voqiQnvRV4st16Wo
+wr+iKISvuoLCkjNqXYazFtIM+ZjTRrtpS0+JwOOu7VARdF9WwxSOM/RX10W1
+Ead5zAPeFBTIcGH8LzX+9ixpkHp5Yvk4utGTRhcy9Jqh4oG6J0IlvYiZhSzD
+yL/KT+GE5iw9qWYZzHENmMPJNPoxQIAFWVgr7JkG5jD0FnSNe/1j//FqDNn9
+RTEkAr+lBMPJA0aGoW9XqkJNrPGWqxjeHOlgOvsjD52ckX2NeNEqTe8Q7N1O
+tSAUr3HF6YrRbJv0TZkWnVqAetrQikGHL6XWiqTVphjmt97m0ltv08aAES2t
+Zkj8cg2dI33IKLJJo2iUkhmT9I5St2VkEkNW1rDMAVZMPvj2ztJoJZrGw6oh
+Jpk9zbvSoCI65vwaPU5PRXpisdVcFcFTPer6Lf0qd5d5ILUdOzuXZJ1mrxWQ
+Pvyk5SnZFI7XlqGk47u/vwz9pz9Pd4Z/T1jXkuLVxgLfjBOkl/xWUBXzUhuD
+McbyPnxMwUCHbi/tg0ggSdZVsp7H/txk9tgn9F7M9pE0Z7+g/9lHDH6I4qdJ
+Jqkb3cjiGumTdKMbFoYwDP0+OYxgNMLOUUSeIrV2Cqfp7vUIIYUbMYZF+01a
+fWZsbMtOINOJMzhLuib2iq4laM+PJl6/R4ItHEjQxK1IMitimo3q6aFsvQTd
+E3HaZp3bxTqPsZh1fod1PmZt4HbEu4Qy7XforEBkzq3AdHDewQUH45hwCPKi
+g0u4vAIW4grsFaRCJEIUQ4yGyFLXQ5wKcfonI/JCLIwGAAA=
+"""
+ )
+
+internal val NAV_DESTINATION =
+ bytecodeStub(
+ "NavDestination.kt",
+ "androidx/navigation",
+ 0x89e4229a,
+ """
+package androidx.navigation
+
+open class NavDestination
+""",
+ """
+META-INF/main.kotlin_module:
+H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgMuQSTsxLKcrPTKnQy0ssy0xPLMnM
+zxPi90ssc87PKynKz8lJLfIuEeIECnjkF5d4l3CJcnEn5+fqpVYk5hbkpAqx
+haSChJUYtBgAOMX57WIAAAA=
+""",
+ """
+androidx/navigation/NavDestination.class:
+H4sIAAAAAAAA/4VRO08CQRD+ZoEDTpSHiuAjUWOhFh4SO42Jj5iQICZqaKwW
+7oLLYy/hFkLJb/EfWJlYGGLpjzLOnTRWNl++x+zMZPbr+/0DwAm2CLtSu0Nf
+uRNHy7HqSKN87TTk+NoLjNKRTIIIua4cS6cvdce5a3W9tkkiRrDOlFbmnBDb
+P2hmkIBlI44kIW6eVUDYq//f/pSQr/d801faufWMdKWR7InBOMZLUgjpEECg
+HvsTFaoKM/eYsD2b2rYoCVvkmM2mqWJpNq2KCl0mPl8skRNhXZXC1/m/c496
+hve88l2PkK0r7TVGg5Y3fJStPjuFut+W/aYcqlDPTfvBHw3b3o0KRfl+pI0a
+eE0VKE4vtPZN1DjADgSfYb5zeBXGEisn0kDi8A2pVyYCZUYrMuNYZ8z8FiAN
+O8o3IlzDZvRhhAXOMk+I1bBYw1INWeSYIl9DActPoAArWOU8gB2gGMD6AQ9d
+W4PtAQAA
+"""
+ )
+
+internal val NAV_GRAPH =
+ bytecodeStub(
+ "NavGraph.kt",
+ "androidx/navigation",
+ 0x54a108f5,
+ """
+package androidx.navigation
+
+open class NavGraph: NavDestination() {
+ fun <T : Any> setStartDestination(startDestRoute: T) {}
+}
+""",
+ """
+META-INF/main.kotlin_module:
+H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgMuQSTsxLKcrPTKnQy0ssy0xPLMnM
+zxPi90ssc87PKynKz8lJLfIuEeIECnjkF5d4l3CJcnEn5+fqpVYk5hbkpAqx
+haSChJUYtBgAOMX57WIAAAA=
+""",
+ """
+androidx/navigation/NavGraph.class:
+H4sIAAAAAAAA/31SXU8TQRQ9s2232wVpAUGo4AegFFC2Ep8sIfEjaE2tSpu+
+8DRtN2X6MWt2pg2P/S3+A580PpjGR3+U8c62aA3iJnPvnXPvuXNm7/z4+fUb
+gMfYZ1jjshkGonnuST4QLa5FIL0yH7wM+YezJBjDxhUVL3ylhYy2ScQY7EMh
+hT5iiOV2arNIwHYRR5Ihrs+EYrhV+t9RBYZF5euK5qGe6sywlCu1+YB7XS5b
+3tt622/owk6NhB9Wn1zOHOWq1Si9WQrCltf2dT3kQiqPSxnoqKXyyoEu97td
+OnJOXZx3EvS17yBNOjuB7grptQc9T0jth5J3vaLUIbURDZXEPIlqnPmNzqTP
+Ox7ynk+FDNv/EDuFVEyTVsH8nkVcd7GAJYaFyxSG+dJExRtf8ybXnDCrN4jR
+2JgxKWPAwDqEnwuzy1PUfMTwfjTMutaKNV4OrYzljobkjHEsZ3llNDyw8uxZ
+4vtHm5Kv1zOxrJWPbzjOaJhJ7Fp5+8DOJLPWq3GBYxofMGxdNcCpeZFMo6rK
+MHMx2f2OpjfwPGj6DOmSkH6536v7YZXXu765fdDg3RoPhdlPwFRFtKhfP6R4
+66Qvtej5RTkQSlD69+9++mekDG4l6IcN/1gY/uqEUxszpgpxFxa9SvNZJJQe
+Kdlt2nlGNvnE7mc4n6J0jqwdgXHskJ0dFyAFl/w8ZgiJReQCVVvkk3sLmS9Y
+/ptuE8XQl8clE7qJ0rhB+d2o+hr2DGZEzEXAg8jex0Pyx4SuUJvVU8SKyBZx
+s4g1rFOIW0Xcxp1TMHO1jVOkFFyFTQVbYUZhS+FeZNMKs78AJaVXJ/gDAAA=
+"""
+ )
+
+internal val NAV_HOST =
+ bytecodeStub(
+ "NavHost.kt",
+ "androidx/navigation",
+ 0x5ce5aeda,
+ """
+package androidx.navigation
+
+import kotlin.reflect.KClass
+
+interface NavHost
+
+inline fun NavHost.createGraph(
+ startDestination: Any,
+ route: KClass<*>? = null,
+): NavGraph { return NavGraph() }
+""",
+ """
+META-INF/main.kotlin_module:
+H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgMuQSTsxLKcrPTKnQy0ssy0xPLMnM
+zxPi90ssc87PKynKz8lJLfIuEeIECnjkF5d4l3CJcnEn5+fqpVYk5hbkpAqx
+haSChJUYtBgAOMX57WIAAAA=
+""",
+ """
+androidx/navigation/NavHost.class:
+H4sIAAAAAAAA/32OzUoDMRSFz81of8a/qVqoiK9g2uLOlZviQFVQcDOrtBNL
+OtMEmnToss/lQrr2oaR3qmsTOPecc+FLvn8+vwDcoUu4VjZfOpOvpVWVmalg
+nJXPqnp0PjRBhGSuKiVLZWfyZTLXU24jQmdcuFAaK590ULkK6p4gFlXEWKql
+XQsIVHC/NnXqs8sHhO5204pFT8QiYffR226Gok/1cki4Gf/zH36DkfFfui0C
+hze3Wk71yJSacPW6ssEs9LvxZlLqB2td2AN8g/E4wO8RuNjrOS55Dhh5yLeR
+IUrRTNFK0UbMFkcpjnGSgTxOcZZBeCQenR1YLcIOPwEAAA==
+""",
+ """
+androidx/navigation/NavHostKt.class:
+H4sIAAAAAAAA/61TbU8TQRB+9o62R1EsRSogrW9VAV+uVnwtIRqNerGgEUNi
+SDRLu5Sl1ztzu200JsZP/gf/hd80mhjjR3+UcfY8QUTgix92dmZ25plndme/
+//j0BcAMZhmKPGhGoWy+cAPeky2uZRi4C7x3L1T6vs6AMeTWeY+7Pg9a7oOV
+ddEgr80w0IgE1+JuxJ+vMaxO1ncBqtX/hqjV26H2ZeBGYtUn271/y+dK1aZ2
+gonr1Bjk/yk0Oz23d60T9TBquetCr0RcBsrlQRDqOEq5C6Fe6Po+RZV3i6IQ
+vuILCkvP6jWp5hxkGUoJp/Vex5WBFlHAfdcLdET5sqEy2Mcw0lgTjXZS5iGP
+eEdQIMPpyX/0uOlZNCCt2tTSPgziQBb7kaMXVJpH+rZQWgYxMwd5hond2s/g
+oOEsA6nnGOxJA1jAoSxGMEqAZVleLW+ZAOYxDJVNj1v9xV3fiyG/vR2GVBR2
+tWA4tMOYMAz/UaTcFKu862uGl/9pDL3tkXtOy8S25p91qzMb1IZ+l5oXmje5
+5pRidXo2fURmRL8RoHtsG8WiwxfSaBXSmhcY7nx9U8h+fZO1Rq14GTVH4pdr
+/CTp41aFTVsVq5rO2aT3VQcdK5cad/KWY4+ySvret7eOQavSBO7VDTHJJtd2
+vk30+26FTXqQA3UZiIVuZ0VEj81gm/cLG9xf4pE0duLsX5QtmrRuRPrhR91A
+y47wgp5Uko5vbn4Q+j1/n26M+paw/YuaN9rz/HlSILsYdqOGuCONMZZgLG3D
+xwVY6DMXS/sYUkiTdZGsp4m/MJ0f+IihM/lhkvbcZ4w8+YCx93H8DMk03cMg
+BnCJ9GnKGISDcRyGeZkCJlCMsQvIo0SRRjuCo5R7OUbI4EqC4dB+ldawnRi/
+ZT+Q68cxHCfdEHtFaSnaS8XU63dIsfkdCdq4FkvmxEzzcT8OVcuSdIjJJuvC
+H6xLOJGwLm2wLiWsLVyPeVdRo/0GnZWJzMll2B5OeTjtYRJTHkGe8XAW55bB
+FM7DXUZGIaVQUSgq5OnWFY4oHP0JmQ7w4mgGAAA=
+"""
+ )
+
+internal val TEST_NAV_HOST =
+ bytecodeStub(
+ "TestNavHost.kt",
+ "androidx/navigation",
+ 0x2f602e26,
+ """
+package androidx.navigation
+
+class TestNavHost: NavHost
+""",
+ """
+META-INF/main.kotlin_module:
+H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgMuQSTsxLKcrPTKnQy0ssy0xPLMnM
+zxPi90ssc87PKynKz8lJLfIuEeIECnjkF5d4l3CJcnEn5+fqpVYk5hbkpAqx
+haSChJUYtBgAOMX57WIAAAA=
+""",
+ """
+androidx/navigation/TestNavHost.class:
+H4sIAAAAAAAA/4VRTUtCQRQ99z01fVmpfWlW0q5a9EzaFUEFkWAGJW5cjb5H
+Teo8cEZp6W/pH7QKWoS07EdF9z2jWgQt5nDOuXe4Z+68f7y8AjhAiVASyhsE
+0ntwlRjJW2FkoNyGr01djC4CbWZAhMy9GAm3J9Ste9W+9zvs2oTiX1e/r8UJ
+iSOppDkm2Ns7zTRmkHQQQ4oQM3dSE7Zq/ww/JGRr3cD0pHIvfSM8YQR7Vn9k
+c34KIRUCCNRl/0GGqszM2ydsTMaOY+Wt6EzG+cm4YpXpNP72mLAyVthU4aY/
+M/zMn/8VZ69rOPtZ4PmEhZpUfn3Yb/uDhmj32MnVgo7oNcVAhvrLdG6C4aDj
+n8tQFK6Hysi+35RacvVEqcBE8zT2YfFqeGXTx4S7Ylxj5UYaiO8+w3liYqHI
+mIjMGNYZ09MGzDIL6xsRFrAZfTJhjmvzLdhVLFSRqSKLHFMsVrGE5RZIYwWr
+XNdIa+Q1kp+Mv2/DIQIAAA==
+"""
+ )
+
+internal val NAVIGATION_STUBS =
+ arrayOf(NAV_CONTROLLER, NAV_DESTINATION, NAV_GRAPH, NAV_HOST, TEST_NAV_HOST)
+
+internal val TEST_CODE =
+ kotlinAndBytecodeStub(
+ "Test.kt",
+ "androidx/test",
+ 0xef517b0b,
+ """
+package androidx.test
+
val classInstanceRef = TestClass()
val classInstanceWithArgRef = TestClassWithArg(15)
@@ -66,4 +302,477 @@
abstract class TestAbstractComp { companion object }
class AbstractChildClassComp(val arg: Boolean): TestAbstractComp() { companion object }
object AbstractChildObjectComp: TestAbstractComp()
+""",
+ """
+META-INF/main.kotlin_module:
+H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgMuQSTsxLKcrPTKnQy0ssy0xPLMnM
+zxPi90ssc87PKynKz8lJLfIuEeIECnjkF5d4l3CJc/HCtZSkFpcIsYWkgiSU
+GLQYABRWGrdkAAAA
+""",
+ """
+androidx/test/AbstractChildClass.class:
+H4sIAAAAAAAA/4VQTWsTURQ9781XMk3MJH6labW1KrRZmLS4U4ppQAhMu6gl
+i2T1khnaRyYzMO9Fusxvce1GUAQXElz6o8T7JlUQFBfv3HvuO5z78f3Hl68A
+nuMpw65IozyT0XVHx0p3ehOlczHV/SuZRP1EKOWBMbT+VF0Q/FJ6sBjclzKV
++pjB3h8dDBms/YNhBQ48HzZKxEV+ycBGFfjYKIOjQlJ9JRXDXvi/CV6Q/2Ws
+e8aCjEcM9XCW6USmndNYi0hoQRI+f2vRUsxA2QCo4Yzq19KwLmXRIcPJatnw
+eZMXb7X0ebDh85LVXC2PeJedVBtuwFu8a3175/LAPq//ZiVSt+ySE7jG6Yhh
+O/z3UWgeau+Z0rOZpl37WRQz1EKZxmeL+STOL8QkoUojzKYiGYpcGn5T9N9k
+i3wav5aGbJ4vUi3n8VAqSb+9NM200DJLFQ7pkHaxaMPclTJOuQOXcJfYMXFO
+0W9/Rrm99QnVD4XmEaHRADvYI7y3VuEWauZulBk3OjMCemuvjjknRaf9EdX3
+f7WprAU3NhyPC9zBE4qviiEd3B7DGuDOAHcH1PY+pWgOsInWGExhC9tjeAo1
+hQcKvsJDBVchUKj/BOsI9uW0AgAA
+""",
+ """
+androidx/test/AbstractChildClassComp$Companion.class:
+H4sIAAAAAAAA/5VSTW/TQBB9u07jxARIWz4SvgolSC0SdRJxK0IqQUiRUpCg
+yqUHtLG3dBN7jbybqMec+CH8g56QOKCoR34UYtYJcKWX2Zn35s143/rnr+8/
+ADzHE4Y9oeM8U/FZaKWx4cHI2FxEtneqkriXCGN6Wfq55YLQKtM+GEN9LGYi
+TIT+FL4bjWVkfXgM5RdKK/uSwdvZHdawhnKAEnyGkj1VhqE9uNyqfYbOzmCS
+2UTpcDxLQ6WtzLVIwtfyREwT28s0TZhGNssPRT6R+f7uMAB3Kzdb0T/yY1qw
+dNfLTWNY/yM4lFbEwgrCeDrzyDzmQtUFMLAJ4WfKVW3K4g5DazEPAt7gAa9T
+tphXLr54jcW8y9vslV/hF1/LvM5db5e5Ca3/8cbHXYbqX4MY/CNq3ptY8riX
+xZLh+kBp+XaajmR+JEYJIRuDLBLJUOTK1Suw1tda5sVcSS8TfMimeSTfKMc1
+30+1VakcKqOo+UDrzApL6ww6ZG7J3ZhO7h6YPnyLqtBZQOfa02+onBf0Q4rl
+AuziEcXasgFVBECdUXZlJX5GJ1+Ja+eFnU5wawkuBUV2FdeI87BNVVCI7uE+
+mnhcLHyAVvFLkwfUWz+G18d6Hxt9bOIGpbjZp5m3j8EMGmgSbxAY3DEo/wZN
+V6SgDwMAAA==
+""",
+ """
+androidx/test/AbstractChildClassComp.class:
+H4sIAAAAAAAA/41SW08TQRT+ZrfXZZFSEQt4QUEsVdlCfBJCgjWaJoUHJE2E
+p2k7wtDtrNmZEh75LT77QtSQaGKIj/4o45lthRiN4WHPmXP2O9+5/vj55RuA
+p1hhmOeqE0eycxwYoU2w0dIm5m1TO5BhpxZyrWtR710WjOHOn8gdEhfoBOMy
+ZNakkmadIVXeXWwyuOXFpo80sh5SyJHN430GtuvDw0geDnyCmgOpGRYaV6lk
+lXLsC7NhaYh8lyG31g6HSZeuwjBvBVcyUllcZ1guN7qRIYbg8KgXSGVErHgY
+vBBveT+kxhRx9Nsmijd53BXx6qCfGx4mMMmQvyBjqF6pgcv0qz5KmLJDmGaY
+a0TxfnAoTCvmUumAKxUZbgimg63IbPXDkFof/13rpjC8ww0nn9M7cmmZzIq8
+FaABd8l/LK1VpVdnmeHV+UnRc0pO8p2feE5hxHNyqdL5yWx2xamyZyz7fLSY
+KTjTTtX9/j7jFFLb4xdWjkKmU7l0IWPp6GxmG/+/BiqM6sha91LXMMxs95WR
+PVFXR1LLVig2LhukE6hFHcEw1pBKbPV7LRHvcMIwFBtRm4dNHktrD51+XSkR
+JxMVFOy9jvpxW7yU9t/UME/zryxYpkmnaCIOpuzgqbxHZGVI3yJdtBdJ2iU7
+nXgfk7VOaIe0VzlDvjLzGaOnCcOTYSSwgiWSkwMUrmHMboBelo0WhgJ9A67A
+LoZ0uvIJox/+SeMPAEOaHBWVHQaXkKwW/ldMvGFnuPkRM6eJxyVim5DRRTpJ
+Y9WEu0INAzXy3ybGO3tw67hbx2wd93CfnpirYx4P9sA0FvBwDzmNMY2yhqex
+qJHRKGiMa5R+ATUXZWovBAAA
+""",
+ """
+androidx/test/AbstractChildObject.class:
+H4sIAAAAAAAA/4VSXWvUQBQ9M7ubZNPV1vrR3bZ+1PqgPpi2+GYR1kUhECPY
+ZaH0aZIMdrrZDCSzpY/75A/xHxQfCgqy6Js/SrwTV0VETMi995w5c27mJl+/
+ffgE4DHuMWyJIiu1ys4CIysT9JPKlCI1g2OVZ6+SE5kaF4xh/U/ZkMJPqYsG
+g7OvCmWeMjTuPxh10ILjowmXoWmOVcWwHf23zxMGbz/Nax8f3G72wvhg2I8H
+zzu4BL9N5GVrpcs3wYk0SSlUUQWiKLQRRmmqY23iaZ6T1ZVorA2ZBS+lEZkw
+gjg+OW3QuZkNbRvAwMbEnymLdqjKdqnBfOb7vMvrZz7zvrzl3flsj++wZ67H
+P79z+Aq30j2Gzejfc6GGrsWPxoZh4/W0MGoiw+JUVSrJZf/3W9OQBjqTDMuR
+KmQ8nSSyHArSMKxGOhX5SJTK4gXpH+hpmcoXyoLewnj0ly12aV7N+pA9Oz7K
+twg5lFcoc7pbNbpNKLCjoNx6eAHvvF6+sxCDqi2KnR8CtMkK8LD0a/Maqe21
+9BH88AKd91g+rwmOu3W8ie36f6PPQgarR2iEuBriWojruEEl1kJ00TsCq7CO
+DVqv4FfYrOB8Bw71GM6sAgAA
+""",
+ """
+androidx/test/AbstractChildObjectComp.class:
+H4sIAAAAAAAA/41SXWvUQBQ9M7ubzaarrfWju1Zrv6TVB9NW3yzCuigEYgS7
+LEifJpuhnW42I8ls6eM++UP8B8WHgoIs+uaPEu/EpSKCmJB77zlzciY5yfcf
+n74AeIJNhvsiS3KtkjPfyML4nbgwuRiY7rFKk9fxiaRRj97VwRhW/pT2qFzK
+S02FwdlXmTLPGCrbD/pN1OB4qKLOUDXHqmDYCv9rv6cM7v4gLb08cGvgBtFB
+rxN1XzRxBV6DyKsMG6HOj/wTaeJcqKzwRZZpI4zSNEfaROM0Jatr4VAbMvNf
+SSMSYQRxfHRaoQyYLQ1bwMCGxJ8pi3ZoSnZpg+nE83iLl9d04n57z1vTyR7f
+Yc/rLv/6weEL3Er3GFbDf+dDm9Yt92hoGJbfjDOjRjLITlWh4lR2fj85hdXV
+iWSYD1Umo/EolnlPkIZhMdQDkfZFriyekd6BHucD+VJZ0J4Z9/+yxS5lVi1f
+tG0jpL5CyKG+QJ3TWSvRPUK+jYN67eEF3PNyeXUmBh5jjWrzlwANsgJczF3e
+vERqe8x9Bn97geZHzJ+XBMd6We9io/z/6NOQweIhKgGuB7gR4CZu0YilAC20
+D8EK3MYyrRfwCtwp4PwEJ9evBbwCAAA=
+""",
+ """
+androidx/test/InterfaceChildClass.class:
+H4sIAAAAAAAA/4VQz28SQRT+ZhZY2FJZqFYKVq21teXg0sabprElMSFBTWrD
+AU4DO9Ipy26yMzQ98rd49mKiMfFgiEf/KOMbSpqYGD3M9973fnxv3vv569t3
+AM/whGFLxGGaqPAqMFKboB0bmb4XQ9k6V1HYioTWLhiDfyEuRRCJeBS8HVzI
+oXHhMNT/bD4juBFwkWXIvVCxMkcMmb3efpfB2dvvFuGi4CEDj7hIRwysV0QR
+qwVw3KJSc640w3bnvz97TgNG0hxbDVLuMZQ748REKg5eSyNCYQSV8MmlQ9sy
+CwULoIljil8py5rkhQcMJ/NZxeNVvnjzmcf9FY/nnep8dsib7GS1kvN5jTed
+Hx9y3M+clm9YnqprmXzWz1mlQ4bNzj/OQh+i+a6NPR0b2raVhJKh1FGxfDOd
+DGR6JgYRRSqdZCiirkiV5cug9y6ZpkP5SlmycTqNjZrIrtKKssdxnBhhVBJr
+HNApMzQnR69ib0uLcjsXecJHxI6Ic7Je4ytWGvUvKH1a1GwT2i6gjseE69dV
+8FG2pyPPqtGlSXdtqRXYi5LNNj6j9PGvMsXrgqUMx84Ct7BL9iXlblPuTh9O
+G+tt3G2jig1yUWtT/70+mMYm7vfhapQ1HmgUNR5q5DUqGmu/ARpxRv/QAgAA
+""",
+ """
+androidx/test/InterfaceChildClassComp$Companion.class:
+H4sIAAAAAAAA/5VSTW/TQBB9u07jxARIWz4SvqGp1CJRNxW3IiQIQrKUggRV
+Lj2gjb1tN7HXyLuJesyJH8I/6AmJA4p65EchZp0AV3qZnXlv3sz6rX/++v4D
+wHNsMoRCJ0WukrPQSmPDSFtZHItY9k5VmvRSYUwvzz53XBBa5doHY2iOxFSE
+qdAn4fvhSMbWh8dQfaG0si8ZvK3tQQMrqAaowGeo2FNlGLr9S+7aJ81Wf5zb
+VOlwNM1C5RRapOEbeSwmqe3l2thiEtu8OBDFWBb724MA3O1c78T/yE9ZyTLs
+XG4aw+ofwYG0IhFWEMazqUf2MRfqLoCBjQk/U67apSzpMnTmsyDgLR7wJmXz
+We3ii9eaz/b4Lnvt1/jF1ypvcte7x9yEzf8yx8ddhvpfhxj8Q+reGVtyuZcn
+kuF6X2n5bpINZXEohikha/08FulAFMrVS7ARaS2Lcq6ktwk+5pMilm+V49of
+JtqqTA6UUdT8SuvcCkvrDLrkbsV9Mp3cPTHd/CFVofOAzpWn31A7L+lHFKsl
+GOIxxcaiAXUEQJNRdmUpfkYnX4ob56WfTnBrAS4EZXYV14jz8ISqoBTdw320
+sVEufIBO+VeTB9TbPIIXYTXCWoR13KAUNyOaefsIzKCFNvEGgcEdg+pvYvaF
+YBIDAAA=
+""",
+ """
+androidx/test/InterfaceChildClassComp.class:
+H4sIAAAAAAAA/41S308TQRD+9lp67XFIWxRLEUUBKVW5gjwJIcEaTZOCCZIm
+wtO2Xcq11z1zu2145G/x2ReihkQTQ3z0jzLOlgoxGsPDzTczN/PNr/3x88s3
+AGtYY1jgshmFfvPY00JpryK1iA55Q5SP/KBZDrhS5bD7zgZjSLd5n3sBly3v
+db0tGtpGjGH6T4I9EpckNkYYEhu+9PUmQ7ywv1RjiBWWai5spBzE4ZDNoxYD
+23fhYiwFCzcoVB/5imGxeq3u1qlIS+gtw0Ps+wzJjUYwrOpdi2LeCC79UNq4
+xbBSqHZCTRReu9/1fJMjeeC9EIe8F+hyKJWOeg0dRts86oho/WKi2w4mkWNI
+XZIR0/VGuKq/7iKPabOHOwxz1TBqeW2h6xH3paJZZKi5pjDl7YR6pxcENHzm
+d7PbQvMm15x8VrcfoxszI1JGgHbcIf+xb6wSac0VhlfnJ1nHylmD7/zEsdKj
+jpWM585PZu1Vq8SeMfv5WDaRtvJWKfb9fcJKx3czl1aSUvLx5Eg6YehWGWaq
+/3kN1BU1YRvfckfTy9ntSe13RUX2feXXA7F1NR09gXLYFAzjVV+KnV63LqI9
+TjEM2WrY4EGNR76xh063IqWIBusUlOy8CXtRQ7z0zb+pYZ3aX1WwQmuOU1MJ
+wimzd9Kf0HoShHcJs+ZNEsZM40iSXCZrk6ItQqd4htHi9GeMn5JlwRtmgrQS
+ycmLKKSRMQcgzbDRvYh3YsjlmbsQjhQ/YfzDP2nci4AhTRI3kRom5zC4LNyv
+mHzLzjD1ETOnA0+MRjMF2aCJPA23OuB+jKeEZfLfI8bZA8QquF/BgwrmME8q
+Fip4iMUDMIUClg6QVMgoFBVchUfKmFmFCYX8L78zjKNFBAAA
+""",
+ """
+androidx/test/InterfaceChildObject.class:
+H4sIAAAAAAAA/4VSy27TQBQ9M0ljxzU0La+EUh59IGCB24odFVKJQLJkgkSj
+SKiriT1tJ3HGkj2JusyKD+EPKhaVQEIR7PgoxB0TihAS2Jr7OHPvuZ4z/vb9
+42cAT3CfYV3oJM9UchoYWZgg1EbmRyKW7ROVJq/7AxkbB4yhMRATEaRCHwe/
+0ArD6p/dXTIXDA4WGGp7SivzjKHy4GHPhwPXQxV1hqo5UQXDZvT/+U8Z3L04
+LYk8cNvthp2D7n6n/cLHEvw6gQ2GjSjLj4OBNP1cKF0EQuvMCKMyijuZ6YzT
+lKiWo2FmiCx4JY1IhBGE8dGkQoIwa+rWgIENCT9VNtumKNmhAbOp5/EmL9ds
+6n59x5uz6S7fZs8dl395X+MNbkt3GdaifyhDEx0LPB4akvDNWBs1kqGeqEL1
+U7n/+7NJpnaWSIalSGnZGY/6Mu8KqmFYibJYpD2RK5vPQe8gG+exfKls0poT
+9/6ixQ4JVqUz1mi1rILk79JBbb5CntNLV0XZPcoCqwb5hUfn8M7K7fV5MXAL
+G2T9nwVYpAjUeOmi+QZV22fxE/jbc1z+gOWzEuDYLO0dbJX/IsMVIrh6iEqI
+ayGuh9TapBCtEDexeghW0LA12i/gF7hdwP0BVe1yPcgCAAA=
+""",
+ """
+androidx/test/Outer$InnerClass.class:
+H4sIAAAAAAAA/4VU308cVRT+7sz+mB0WmOVXKay0yorL0nYAW62FVgFFBpel
+QkOs+HLZvcLAMoMzs6S+GJ76JzTRFxNjfOKhTRSMTQy2b/5NxnjuznS3LgSS
+mXvOPXPOd757zrnz979//AngJlYZhrhT8Vy78sgMhB+Yy7VAeDnLcYQ3V+W+
+nwRjMLb5Pjer3Nk0lze2RTlIQmVITNuOHdxjiOWt0TUGNT+6lkYcSR0xaAya
+LVFmvE0GZqWhoy0FBWnyD7Zsn+Fq8fzUUwxtmyKwGiiUwGLQy+7unusIJ5gg
+qLK79y3DMDG4GG246Hqb5rYINjxuO77JHccNeGC7pJfcoFSrVqfkARI68exj
+SEvwXEV8zWvVgGEtf1EKyyq2VmrqQl5pdKNHZhygkgXuauDZDh22Jz/6Glho
+pTNcarXN1uxqRXhJDOm4Isve08TOv+rAXQ1vUsP43p5wKgzX86ehT2eLkIng
+MHIS/G2GrCz0eY7vSMe8dJw737EgHcfSyOINqV2nw29xf2vOrQiGTDPScgKx
+Kc83Hg4aTZKJSR0TeJdOJL6p8SrNUm/+jMp/yZA7r+XUb75RFVTVuBtsCY+h
+6zQKkSnuuEHVdswlEfAKDzjZlN19lW4Qk0tKLqAh3yH7I1vuiKtSofH8+eRg
+SFf6FV0xTg50ehRD0xUtQbKNpEqyQ3vxWOs/OZhUxtlse1fCUAaUcfXFTwnF
+iC2mjKTcLbx8rC52Gxrp5KhpSuhEZkbmFOn6pGa0DcT62ThbePlEpcB06PGE
+kd5OeofUVzINeI3oDMS0uJGQXCeZPEH3GaOaxDzdueZMMSQf0McbO3QjYmG3
+Oou2I0q13Q3hPZAFlXV0y7y6xj1b7iPj4ErNCexdYTn7tm+TaabZDIb21YCX
+d5b4XuSda/W+zz2+K4jR/8LSTWaCtvqqW/PKYt6WEJcjiLVT6Wh6FPpJyTN3
+yR8TaRrpdPVpXaTdPH1XSOqFY6QKg7+h/RntFHxGawdki3spvg8pkkXa9YXe
+9K1TDgNpEpVmBwa9IaYpZ4RkvPAr2g8bcIm6sa8Okw4dIpgMkXsVPNwazM4M
+oF8JwcqACWIpOaWeQ3k4eIxLTxtBIdlUg2wqIrsUsekFjBT6cTnKPRIVK5ON
+ffc9NMlgujB4hMEQskSrCiYR6DJH6e+QlNSyz3Hl4TGudr11hBEZeYRRY/QI
+145w42nLMbIRo9d40Go2ajAS1aDO4HfcbC2DFsUz3MJ7EY+vSMp25QpjvyAe
+Oxz7C8oPiKuHYydQliTQNXp/lJZY2JNSvX1qUvsHmSTtmxXLNSqWw218QHmW
+SU9KUu/Xa3C/Hkr3CZ9igcr3eR3QwgrJL8h+hzo1tQ7VwrSFuxbu4UNS8ZGF
+Gcyug/mYw8fr6PTl84kPvb4mfBg+Mj66fHT7uFU33vZh+siS/h/DktczzQcA
+AA==
+""",
+ """
+androidx/test/Outer$InnerObject.class:
+H4sIAAAAAAAA/4VUS08TURT+7p0+ptMChSIUUBCpykNpQV1BTJRoHCzFCMEo
+q9t2hKHtjM7cEpas/AkuXLpwxULigkQTg7Dzf/g3jOdOR4vgI2nP+c655zXf
+uTNfv3/8DOAmbjGMCKfquXZ1Jy8tX+aXm9LycqbjWN5yecuqyDgYQ3pLbIt8
+XTgb+Z9ejSE2bzu2vM2gjU+spRBFzEAEcYaI3LR9htHif2rPMejSXZGe7Www
+9I5PFNt9Wl6KGCu63kZ+y5JlT9iOnxeO40ohbZdwyZWlZr1OUckTZXV0UuFN
+4W8uuFUrGM/Uto6+PaSRrZdNUafZzo0XTz/T3MQzhty/ulErUa5b1C7qyk3L
+Y+g5W4Vaz1fqATMGuKJDN0srq3dKC/dSGISRIOcQQ3ex5koKyy9ZUlSFFJTI
+G9sa7YUpkVACDKxG/h1bWQVC1RmG54e7wwbPcoOnD3cNriuQDLVuKFe6Uz9+
+ZWQPd2d5gd2N6/zobYyn+WImrQ3yQmRWT0cHI1lWYA+OX2uLiXSMvHHCjLBO
+OKGw6jbL1AyZP+wxjgmG+CrZ0zXJMPS46Ui7YZnOtu3bRNKdNnF0IVqL6Cra
+jlVqNsqWt6qIVPy5FVFfE56t7NDZsSJFpbYkXoR27nTtR8ITDYuG+K1JKrgC
+C3Xh+xaZxorb9CrWfVuVGAhLrJ0ZDjO0j0hA9YBaD+lrZMVId5CO0mk0sK6T
+lVcLUd7JA+j7BDimw2AgQ8dAqhWABJVSRZPk4UHyaJis9XS9D47a4VoYfrIz
+vXXoDvu2U3v2/pJKW0Jv2MkkzUn3T069QzSyN/UF/A2i2t7UIfiTyF4weIFk
+BDyuB8X6WglhMYX66M+IHagrTC8MAR3ZX1T0BwlA8hP40wMMfMD5/cChYZak
+4pFjEp3E6o2g3xR9b9RoDBeInuF1aCZGTFw06ekuEcSYiRwur4P5uIKr6zB8
+9Rv3EfORCUCfj3QAkiR/AL5kiZ3FBAAA
+""",
+ """
+androidx/test/Outer.class:
+H4sIAAAAAAAA/3VRwW7TQBB9u05ixzE0TSlNKE2BFmiKhNuKU6mQSgSSpZBK
+bRQJ5bRJVmUTx5bsTdRjTnwIf1BxqAQSiuDGRyHGbiAHileetzPz5s3u7M9f
+X74BeIFnDCsi6Eeh6l+4WsbaPRlrGZlgDMWBmAjXF8G5e9IdyJ42YTDkjlSg
+9CsGY6fWdpBFzkYGJkNGf1Axw2rjBr2XDNZRz08rbfCEbnnNs9Zxs/7GwS3Y
+eQreZthqhNG5O5C6GwkVxK4IglALrULaN0PdHPs+SS03hqEmMfed1KIvtKAY
+H00MuhFLTD4xYGBDil+oxNujXX+foTabOjYvc5sXZ1ObW4b14yMvz6YHfI8d
+ciPz2rT49085XuRJwQFLZGwvCGRU90VM1yukzvU8GKo33HV7QTexybD5X86f
+qT5kMFuUez4kyfXTcaDVSHrBRMWq68vjxQxoyPWwLxmWGiqQzfGoK6OWIA5D
+qRH2hN8WkUr8edBZHEVSsX0WjqOefKuSXGXep/1PF+zTY2TSCVaStyHcJi9H
+WCTktLKp95g8N5kzYXb3CtZlmn4yJwMlPCXrXBOQJynAQuFv8Rqxk6/wFfz9
+FZzPWLpMAwZ20nKOB/Rv0DkeEVYJa2mLLewSHpLMMgmXOjA8rHi442EVd2mL
+NQ9lVDpgMe5hvYNsDDvG/Ri5GBsxqr8BxLf8XAEDAAA=
+""",
+ """
+androidx/test/OuterComp$InnerClassComp$Companion.class:
+H4sIAAAAAAAA/5VTTW8SURQ9d4YyMGKlVC34/YGVGu0AcVdjohgTEmqT2rDp
+wjzgqQ+GN2bmTdMlK3+I/6ArExeGdOmPMt43oI0LE7u599x77rk3cx78+Pnt
+O4CnaBCaQo/iSI2OAyMTE+ylRsadaPqp3tWaUSiSJCttEFpF2gMRymNxJIJQ
+6A/B3mAsh8aDS8g/U1qZ5wS3sdUvYQV5Hzl4hJz5qBJCu3feYzuEVqM3iUyo
+dDA+mgZKs0SLMHgl34s0NJ1IJyZOhyaKd0U8kfHOVt+HY4+u14dn5LtpxhK2
+z7eNsPZbsCuNGAkjuOdMj1w2kGwo2gACTbh/rGzVZDRqEerzme87Vcd3yozm
+s8LpZ7c6n7WdJr30Cs7pl7xTduxsm+yGzf9zx8N1wsY/Zj3cJKz+LSAU/xhK
+8A5YsD0x/CqdaCQJl3pKyzfpdCDjAzEIuVPpRUMR9kWsbL1sls6WSn5L/22U
+xkP5Wlmutp9qo6ayrxLFwy+0jowwfC5Bix8jZx3i7NifBH/oPa4CaxnnlUdf
+UTjJ6Psc81nzMeocS4sBFOEDZWJ0YSl+wtlZiksnmf1WcHXRXAgydBGrzLl4
+wFWF2Ru4hduoZegO583s8F08zP4O7AVryodwu1jrotLFOi4zxJUu7944BCWo
+osZ8Aj/BtQT5X3gloppLAwAA
+""",
+ """
+androidx/test/OuterComp$InnerClassComp.class:
+H4sIAAAAAAAA/41UXVMURxQ9Pfs1Oywwi18IJJq4McuuukA0MYqJijEMATRi
+iGjy0OyOMLDMkJlZyrykfPInWJW8pCoPeeJBKwmkYlWK6Ft+UyqV0zPjomAs
+qqD73ru3zz197u35+98//gRwGl8LHJduw/ecxr1aaAdh7VortP0xb2W1ZLku
+raYMAuXmIATMJbkma03pLtSuzS/Z9TCHlEB21HGd8COBdNkanBVIlQdnC8gg
+ZyANXUB3FNIlf0FAWAUY6MhDQ4H54aITCJQn90bhvEDHgh1abTQWsgSMOn/z
+XNsNhwlZ91a/FaiSyd5Rj016/kJtyQ7nfem4QU26rhfK0PFoT3vhdKvZPK8u
+lDXI+6BAQRUpNey7stUMBe7u+QKWNblTwfN75lnAPuxXDPooaejNhL7jUoT9
+5cEXQOMo73RoZ+xyy2k2bD+HNw0cUW3pfRm//LxLF3S8xabK1VXbbQicLO+G
+310xQSfJYyipAu8IDKgmvC7xXZVYVoljr0+sqMRqAQN4Q1knKcCiDBbHvIYt
+UNw+abmhvaDuOBQPI6ethhEDw3iPN7K/ackm5+1A+RVduC1Qet0YcAbkfNOm
+shkvXLR9gZ7dKOQ1Wm8mr2Fob30tqUW6rJIDAYbLk8teSIza0tpKzeGFfFc2
+a1fiYRsjl9Bv1UPPn5L+MuWJn9oFA6NgzXwbTGBkj4O1TYBaX8Ql9TgvU9bn
+PKbsUDZkKElOW1lL8bsh1JJXC/iklxm/5yiPqmsNPsL1rftHDa1XMzRz677B
+P83UDU3Pcu/gnuLexbD+9IHey9TuEW1InBPdlzt7sqbWpw2lnv6U1cz0RN7M
+KW/82YPUxD5Tp711f0TXtTiJYcFwnrYxopsdfeleMSTGnz1M8WAhzngoaHfS
+7lL2jWIbXmf9vrSeMbOK84hQNzn0P3rlcF2g62XRBHI3mXRqme+//0bLDZ0V
+23LXnMDhkFzaHhzOYTyl3ZOOa0+3VuZt/6YaJDU/Xl02Z6XvKD8Jds6Esr48
+JVcTv7QT+7r05YpNZi8VKWyzs+kaM17Lr9tXHQVxOIGY3UWO70LjJxpcD6vO
+U4Ob9LLcD3DvUZ9q1Wn6mSj6Bb2rzNa4G5VN5Cv9v6HzcYQwy7ULagwqxKzy
+VAVf0jsYZ/O3bjUwtBQq5wsm/2PMmpoj7pnKr+hcb8Nlo2A1ginECQlMkeSe
+Hz6287B45QF+PAmrDgyTpeKUfwJtrn8Thx61D8Vk822y+YTsC7KYefRSrrj2
+8UTA4kD6u++hKwajlf4N9MeQt7imIBQCP11J+XPcFbWBJzgyt4mjPW9v4Lg6
+uYFBc3ADJzZw6tGOawwkjF5sj6BsxTaPWIOIwe84vVMGPTkvcAbvJzy+4q7a
+VapUf0YmvV79C9oPyKTWq1vQphTQCf7/qCLpuCe3ovalcvo/KObobytWaitW
+wll8yDpztHOK1AdR+XPIJVR7o6Ik9gSjc2ITH/+CscdRJIXb0dSp+focNyjy
+KK2L3O9E5WdIGbQFrrCvn9xBysJVC59aGIdFExMWPsMkEwJMYfoOzADdAa4F
+MKI1G6hIMUBPgH0BzkTBswFqAQYi++J/Kz3doBgJAAA=
+""",
+ """
+androidx/test/OuterComp$InnerObject.class:
+H4sIAAAAAAAA/41US08TURT+7p0+ptMC5SEUUHxQtFClBXVhICZIfAwpxQjB
+KKvbdoSh7QzO3BKWrPQfuHDpwhULiQsSTQzKzp/kwnjuMApCMCbtOd8597zm
+O3fm+89PXwDcwm2GYeHUPNeubRWk5cvCQkta3qzb3MiajmN5C5V1qyrjYAzp
+dbEpCg3hrBZ+ezWG2LTt2PIug5YbXU4hipiBCOIMEblm+wwjpf+oP8WgS3dR
+erazytCTGy0d9Tr0UsRwyfVWC+uWrHjCdvyCcBxXCmm7hMuuLLcaDYpKHiur
+o50Krwl/bdatWcGIpnbn4esfNLb1siUaNN+5XOnkc02NPmfI/qsbtRKVhkXt
+oq5cszyGrtNVqPV0tRGwY4ArSnSzvLg0U569n8IAjAQ5Bxk6S3VXUlhh3pKi
+JqSgRN7c1Gg/TImEEmBgdfJv2coqEqpNMLzY3x4yeIYbPL2/bXBdgWSodUO5
+0u36wSsjs789yYvsXlzn397FeJrPdae1AV6MTOrp6EAkw4rs0cEbbS6RjpE3
+TpgR1gknFFbdJpmaoe+MXcYxyhBfIt94XTIMPmk50m5aprNp+zYRNXNEHl2M
+w2V0lGzHKreaFctbUmQqDt2qaCwLz1Z26GxblKJanxcboZ09Wfux8ETTokH+
+apIKrsFsQ/i+Raax6La8qvXAViX6wxLLp4bDBO0kEtDdr1ZE+jpZMdJtpKN0
+Gg2sG2QV1FKUd2wP+i4BjvEwGMjRMZA6DECCSqmiSfLwIPlymKx1dXwIjo7C
+tTD8eGd6+9AZ9j1K7do5I5WhGz1hJ5M0J903ln+PaGQn/xX8LaLaTn4f/Glk
+Jxi8SDICHteDYr2HCWExhXrpz4gdqGtMLw0BHZk/VPQFCUDyM/izPfR/xPnd
+wKFhkqTikWMM7cTqzaBfnr49ajSGC0TP0Ao0ExdNXDLp6a4QxLCJLEZWwHxc
+xbUVGL765XzEfHQHoNdHOgBJkr8Ar2VkytEEAAA=
+""",
+ """
+androidx/test/OuterComp.class:
+H4sIAAAAAAAA/31RW2sTQRT+ZjbJbjaxTeMlibX10lqbCm5bfKpFqEVhIaZg
+Q0DyNEmGOslmV3YnoY958of4D4oPBQUJ+uaPEs9so0Wk7rDnO9fvzDnz4+fn
+rwCe4jFDRYT9OFL9U0/LRHtHYy3jw2j03gZjKA3ERHiBCE+8o+5A9rQNiyG3
+r0KlnzNYm/V2EVnkXGRgM2T0O5Uw1BpXcD5jcPZ7QVrtgpsSx28etw6ahy+L
+uAY3T84FhrVGFJ94A6m7sVBh4okwjLTQKiK9GenmOAiIaqkxjDSRea+lFn2h
+Bfn4aGLRZMyIvBFgYEPynypjbZPW32Goz6ZFl1e5y0uzqcsdy/n+gVdn012+
+zfa4lXlhO/zbxxwvcVOwywzNgh+GNEYgksTMwlBIHRd7Ydi4Yub1v8ts3KX5
+/pv7e9P3GewWxZ8MiX75zTjUaiT9cKIS1Q3kweVOaPGHUV8yLDZUKJvjUVfG
+LUE5DOVG1BNBW8TK2HNn8fJKkord42gc9+QrZWK1eZ/2P12wQ4+TSTdaM29F
+uE5WjrBEyOlkU+shWZ7ZO2F26xzOWRremCcDj+gAxYsE5IkKcFD4U1yhbPMV
+voC/PUfxExbPUoeFTZJlCt+jf4Xu8YBwlbCetljDFuEe0SwRcbkDy8d1Hzd8
+3MQtUlHxUUWtA5bgNpY7yCZwE9xJkEuwkmD1F4DlAzIZAwAA
+""",
+ """
+androidx/test/TestAbstract.class:
+H4sIAAAAAAAA/3VRy04CMRQ9t8AgIwriC/AR3Rh14ahxpzFBExMS1EQNG1eF
+mWgFOsm0EJd8i3/gysSFIS79KOPt6NbNyXnctqft1/f7B4AjrBHqUodJrMLn
+wEbGBncMjY6xiezaPIhQfpIjGfSlfgiuO0+RczME70RpZU8Jme2ddhE5eD6y
+yBOy9lEZwmrr/22PCXOtXmz7SgeXkZWhtJI9MRhluBQ5KDgAgXrsPyun9pmF
+B4SNydj3RVX4osxsMp7aqk7Gh2KfznKfL54oCzd3SG513p2617Pc6jwOI0Kp
+pXR0NRx0ouROdvrsVFpxV/bbMlFO/5n+bTxMutGFcqJ2M9RWDaK2MorThtax
+lVbF2mQ3IfjSf03dGzBWWQWpBnK7b5h6ZSJQY/RScx11xuLvAArw03wlxWWs
+pt9CmOaseI9MEzNNzDZRQpkp5pqoYP4eZLCARc4NfIMlA+8H7YuiztMBAAA=
+""",
+ """
+androidx/test/TestAbstractComp$Companion.class:
+H4sIAAAAAAAA/5VSTW/TQBB9u07jxARIWz4SPspXkNJK1E3FrQipBCFZSkGC
+Kpce0MZZYBN7jbzrqMec+CH8g56QOKCoR34UYtYJcENwmZ15b96M962///j6
+DcBjPGToCj3OMzU+Da00NjymcDgyNhex7Wfpx44LQqtM+2AMzYmYiTAR+n34
+ajSRsfXhMVSfKK3sUwavuz1sYA3VABX4DBX7QRmGncG/Ljlg6HUH08wmSoeT
+WRoqbWWuRRI+l+9EkVC7Jl0R2yw/EvlU5gfbwwDcLdvsxH/It2nJMuz+3zSG
+9V+CI2nFWFhBGE9nHhnGXKi7AAY2JfxUuWqPsnGPobOYBwFv8YA3KVvMa+ef
+vNZivs/32DO/xs8/V3mTu9595iZs/d0VHzcZ6r+tYfBdx+7Ukq/9bCwZLg+U
+li+LdCTzYzFKCNkYZLFIhiJXrl6BjUhrmfcTYYyk1wjeZEUeyxfKce3XhbYq
+lUNlFDUfap1ZYWmdQY9srbi70sndo9In36EqdJenc23nC2pnJX2XYrUEe7hH
+sbFsQB0B0GSUXViJH9HJV+LGWWmkE1xbgktBmV3EJeI83KcqKEW3cBttPCgX
+bqFT/sDkAfU2T+BFWI+wEWETVyjF1YhmXj8BM2ihTbxBYHDDoPoTIO6Wpv0C
+AAA=
+""",
+ """
+androidx/test/TestAbstractComp.class:
+H4sIAAAAAAAA/4VRXWsTQRQ9s5vPdWOT+pVYramtMc2D2xRBsEWoEWEhTUFL
+QPI0ScY6yWZWdiahj/kt/oPiQ0FBgo/+KPHuNrYPQn25Z+6dc889c+fX728/
+ADxHg2Gdq2EUyuGpZ4Q23jGFg742ER+YVjj5nAVjKI74jHsBVyfeUX8kBiYL
+myGzL5U0rxjs+nbXRRoZBylkGVLmk9QM1fb10nsMuf1BsBSpX0/eigNXMlRZ
+uAzNenscGur1RrOJJ5URkeKB90Z85NOAGhR1TgcmjA55NBbR3oXBmw4KWGHI
+X4oxNP7j8mrwnosSVvOwcIthsx1GJ95ImH7EpdIeVyo03BBNe53QdKZBQO8r
+/XV5KAwfcsOpZk1mNi2fxSEfBzCwMdVPZZzt0GnYZKgt5q5jlS3HKi7mjpWz
+crXyYl61d60d9pLZr9M/v2SsohWzd1mskY2dPxsbhrV3U2XkRPhqJrXsB+Lg
+yhz9TiscCoaVtlSiM530RXTMicOw2g4HPOjySMb5suj6SomoFXCtBTU778Np
+NBBvZXxXWc7p/jMltUFbSiVPq8RLI9ykLEN4h9AiTCfZFmVevADCdOMcubPk
++smSDDRRo+heEJCHQ5jDjcvmMpIVwv2Owgd2juJX3D5LKjaeUnSIVyDFEhmp
+J9qPsU34gup3SfFeD7aPso+Kj/tYoyMe+HiI9R6YxiNUe0hpOBobGhmN0h/c
+zA/4OwMAAA==
+""",
+ """
+androidx/test/TestClass.class:
+H4sIAAAAAAAA/3VRu04CQRQ9d5BFVpQFX+CrVgsXjZ3GRE1MSFATNTRWA7vR
+gWU2YQZCybf4B1YmFoZY+lHGO6utzcl53Jk5N/P1/f4B4BjbhHWpo2Gqoklo
+Y2PDB4bLRBpTABGCnhzLMJH6Kbzt9OKuLSBH8E6VVvaMkNvda5eQh+djDgXC
+nH1WhlBv/XPnCaHS6qc2UTq8jq2MpJXsicE4x3XIQdEBCNRnf6KcajCLDgk7
+s6nvi5rwRcBsNq3NpkeiQRf5zxdPBMJNHZE7W3APHvQtF7pMo5hQbikd34wG
+nXj4IDsJO9VW2pVJWw6V03+mf5+Oht34SjlRvxtpqwZxWxnF6bnWqZVWpdrg
+EIL3/evp1messQozDeT33zD/ykSgzuhl5hI2GEu/AyjCz/LNDNexlX0HYYGz
+0iNyTSw2sdREGQFTVJqoYvkRZLCCVc4NfIM1A+8HjoCWJ8sBAAA=
+""",
+ """
+androidx/test/TestClassComp$Companion.class:
+H4sIAAAAAAAA/5VSTW/TQBB9s07jxARIWz4SyjepaJGom4pbERIEIUVKQYIq
+lx7QJllgE3uNvJuox5z4IfyDnpA4oKhHfhRi1ilwQ3CZnXlv3oz3rb//+PoN
+wCNsEjalGeWZHh3HTlkXH3LoJNLaTpZ+bPkgjc5MCCLUx3Im40Sa9/GrwVgN
+XYiAUH6sjXZPCMHWdr+GFZQjlBASSu6DtoT7vX/asE9ob/UmmUu0icezNNbG
+qdzIJH6u3slp4jqZsS6fDl2WH8h8ovL97X4E4Tett4Z/yLdpwRJ2/m8aYfWX
+4EA5OZJOMibSWcBWkQ9VH0CgCePH2le7nI3ahNZiHkWiISJR52wxr5x+ChqL
++Z7YpWdhRZx+Lou68L175Cds/MWSEBuE6m9fCKGndyaOHe1kI0W42NNGvZym
+A5UfykHCyFovG8qkL3Pt6zOw1jVG5cVcxe8Qvcmm+VC90J5rvp4ap1PV11Zz
+81NjMicdr7Nos6clf1E+hX9O/t5bXMX+5nyuPPiCyklB3+ZYLsB7uMOxtmxA
+FRFQJ87OnYkf8inOxLWTwkUvuLIEl4IiO48LzAW4y1VUiK7jBpq8wC+8iVbx
+37IH3Fs/QtDFahdrXazjEqe43OWZV49AFg00mbeILK5ZlH8CxwhM7PQCAAA=
+""",
+ """
+androidx/test/TestClassComp.class:
+H4sIAAAAAAAA/31RXWsTQRQ9s5vPdWOT+pVYq9WmNu2D2xRBMEXQiLCQtqAl
+IHmaJGOdZDMrO7Ohj/kt/oPiQ0FBgo/+KPHuNrYPQl7umXvn3HPP3Pn95/tP
+AM+xy7DG1TAK5fDMM0Ib74RCO+Bat8PJlzwYQ3nEp9wLuDr1jvsjMTB52Ay5
+A6mkecVgN3a6LrLIOcggz5Axn6VmWO8s0W0xFA4GwUJhawmzngSuZKjycBma
+jc44NNTojaYTTyojIsUD7634xOPAtEOlTRQPTBgd8mgsotaltZsOSlhhKF6J
+MWwv83c9teWigtUiLNxi2OyE0ak3EqYfcam0x5UKDTdE095RaI7iIKCXVf5Z
+PBSGD7nhVLMmU5sWzpJQTAIY2JjqZzLJ9ug0bDLU5zPXsaqWY5XnM8cqWNX5
+bMPet/bYS2a/yf76mrPKVsLdZ4lCPjH9bGzoE9/HysiJ8NVUatkPxOtra/Ql
+7XAoGFY6UomjeNIX0QknDsNqJxzwoMsjmeSLousrJaJ0F4KanQ9hHA3EO5nc
+1RZzuv9NQZN2lEkfVktWRrhJWY7wDqFFmE2zOmVe8nzC7O4FCufp9daCDGp7
+StG9JKAIh7CAG1fNVaQLhPsDpY/sAuVvuH2eVmxsU3SIVyLFChlppNpPsEP4
+gup3SfFeD7aPqo+aj/tYoyMe+FjHwx6YxiNs9JDRcDQea+Q0Kn8BDq7wpy0D
+AAA=
+""",
+ """
+androidx/test/TestClassWithArg.class:
+H4sIAAAAAAAA/31QTWsTURQ9781nxsRM4leaaq3aRZtFJy3ulGIMCANRoZZ0
+kdVLZkhfM5mBeS+ly/wW124ERXAhwaU/SrwvDa5EeJx7z32Hcz9+/f7+A8Bz
+7DHsiDwpC5lcRzpVOjoj6GdCqXOpL3rl1ANjCC/FlYgykU+j9+PLdKI9WAzu
+S5lLfcJg78cHQwZr/2BYhQMvgA2fuCinDCyuIsCtCjiqJNUXUjHsDv7f9QW5
+T1PdMwZkGzM0BrNCZzKP3qZaJEILkvD5lUVrMAMVA6B2M6pfS8O6lCVHDP3V
+shnwFg94uFoG9HjoB9y3WqvlMe+y17WmG/I271o/P7o8tE8bf5lP6rbtO6Fr
+rI6ZaeCZWQ9nmnbpF0nKUB/IPH23mI/T8kyMM6o0B8VEZENRSsM3xeBDsSgn
+6RtpyNbpItdyng6lkvTby/NCCy2LXOGIDmWvV2mau1HGKXfgEj4mdkKcUww6
+31DpbH9F7fNas0toNECIJ4T3b1S4jbq5DGXGjQ5J/42NV2QORtHpfEHt0z9t
+qjeCjQ3H0zXu4BnFV+shHdwZwYpxN8a9mNo+oBStGFtoj8AUtvFwBE+hrvBI
+IVijqxAqNP4AZEwrjogCAAA=
+""",
+ """
+androidx/test/TestClassWithArgComp$Companion.class:
+H4sIAAAAAAAA/5VSTW8TMRB99qbZZAmQtnwkfLdNpRZBt6m4FSGVIKRIKUhQ
+hUMPyElM62TXi2wn6jEnfgj/oCckDijqkR+FGG8CHFEv45n35s14n/fnr+8/
+ADzDJsMToQcmU4Oz2Enr4iMKrURY+0G50wNz0srSzw0fhFaZDsEYqkMxEXEi
+9En8tjeUfRciYCg+V1q5FwzB1na3giUUIxQQMhTcqbIMO53LLNpnaG51RplL
+lI6HkzRW2kmjRRK/kp/EOHGtTFtnxn2XmUNhRtLsb3cjcL9wtdH/R35Mc9bv
+v9Q0huU/gkPpxEA4QRhPJwEZx3wo+wAGNiL8TPlql7JBk6Exm0YRr/GIVymb
+TUsXX4LabLrHd9nLsMQvvhZ5lfvePeYnrP/fmRB3Gcp/7WEIfdfOyJG/rWwg
+Ga53lJZvxmlPmiPRSwhZ6WR9kXSFUb5egJW21tLk4yW9SvQ+G5u+fK08V383
+1k6lsqusouYDrTMnHK2zaJK1Bf+9dHL/uHTth1TF3gA6lx5/Q+k8px9RLObg
+JtYoVuYNKCMCqoyyKwvxUzr5Qlw5z830gltzcC7Is6u4RlyAdaqiXHQP91HH
+Rr7wARr5z0weUG/1GEEby22stLGKG5TiZptm3j4Gs6ihTrxFZHHHovgbuLwQ
+4QkDAAA=
+""",
+ """
+androidx/test/TestClassWithArgComp.class:
+H4sIAAAAAAAA/41SW08TQRT+ZnvZ7VpkqYgFvCAXLRXZQnwSQoI1xk1KTZDU
+GJ6m7Vi23c6a3WnDI7/FZ1+IGhJNDPHRH2U8s63woAkmu+fMOXPO953L/Pz1
+9TuAJ9hgWOSyHYV++9hVIlbuAYlqwOP4ja+OdqNONey/N8EYnC4fcjfgsuO+
+anZFS5lIMWS3femrHYZ0yVttMKRKq408MjBtpGGRzaMOA/PysHEtBwN5ClVH
+fsywXLuaeYsYOkLtahCC9his7VYwply7On9ZCy79UJq4wbBRqvVCRflud9h3
+falEJHngPhfv+CBQ1VDGKhq0VBjt8agnoq1RLzdtTGOGIXcBxrD+H8Vfkm/l
+UcSsbn+OYakWRh23K1Qz4r6MXS5lqLiisNith6o+CAJqe+pPpXtC8TZXnHxG
+f5iitTEtclqARtsj/7GvrQqd2rTRl+cnBdsoGrbhnJ/Y9BmOZRtWunh+smBu
+GhX2lJnPJgpZx5gzKqkfH7KGk96furAsSplLWxknq/E2mWYxdX/rPcUwvz+Q
+yu8LTw792G8GYveyfFptNWwLhsmaL0V90G+K6IBTDEOhFrZ40OCRr+2xM+9J
+KaJkbIKS7dfhIGqJF76+mx3zNP5iwQbNMU39GpjVY6XyymRlSd8mXdAvjXSK
+7EzifUTWDkUbpO3yGXLl+S+YOE0Q1saZwAoek5wZReE6JvV86aTRaB1w6B9h
+uXrspDPlz5j4+E+Y/ChgDGNRUeY4uYhkcch/w/RbdoZbnzB/mnhSWE8IGb02
+I2nMTbBXUSFdJf8dQrx7iJSHex4WPNzHIh2x5GEZK4dgMR7g4SGsGJMxSjHs
+RGZjODGmYhR/A+R3KVn3AwAA
+""",
+ """
+androidx/test/TestGraph.class:
+H4sIAAAAAAAA/3VSTW/TQBB9s/mw4waalo8klO/2UDjgtuJGhVQqQJaMkWgU
+qeppE6/aTRwb2Zuox5z4IfyDikMlkFAEN34UYtZEcEB4pTfzZt887Yz84+fn
+rwCeYovQlmmcZzo+940qjN9jeJ3L92cOiNAayZn0E5me+m8HIzU0DiqE+r5O
+tXlOqGw/6jdRQ91DFQ6has50QeiG//F8RnD3h0nZ7UHYFjeIjnoH0eHLJq7A
+a3DxKmEzzPJTf6TMIJc6LXyZppmRRmecR5mJpknCVmvhODNs5r9RRsbSSK6J
+yazCk5GFhgUQaMz1c23ZDmfxLmFrMfc80RGeaHG2mLvfP4jOYr4nduiF44pv
+H+uiJax2j6yDYyd4MjaEjXfT1OiJCtKZLvQgUQd/n8bzH2axIqyGOlXRdDJQ
+eU+yhrAeZkOZ9GWuLV8WvaNsmg/VK21Jd2nc/8cWu7yUajlJ1+6I411mdY4t
+joJPrWT3mPl2Xo61x5dwL8rr+0sxuPUBY/O3AA3mgIuVP81tVttv5QvE8SWa
+n7B6URYEHpZ4B5vlb8O7Z4P1E1QCXAtwPcAN3OQU7QAddE9ABW5hg+8LeAVu
+F6j/Ai/P7yRzAgAA
+""",
+ """
+androidx/test/TestInterface.class:
+H4sIAAAAAAAA/32Oz0rDQBDGv9lo08Z/qVqoiK9g2tKbJy9CoCKoeMlpm2xl
+m3QD2Wnpsc/lQXr2ocSJ3p2Bb76Zgd/M1/fHJ4ApBoRr7YqmtsU2YeM5eRVJ
+HZtmoXMTggjxUm90Umn3njzNlybnEAGhPytrrqxLHg3rQrO+I6jVJhAstdJr
+BQQqZb61bTcSV4wJg/2uG6mhilQsbjHc7yZqRO1yQriZ/fOP3BBk2M5uSyZE
+L/W6yc2DrQzh6nnt2K7Mm/V2Xpl752rWbGvnO8LGAf5C4eJXz3EpdSy8Q8lO
+hiBFmKKboodILI5SHOMkA3mc4iyD8og9+j/Vk+x/PAEAAA==
+""",
+ """
+androidx/test/TestKt.class:
+H4sIAAAAAAAA/4VUbU/TUBR+bjvWrgzW8b6BiLzo5gsFfBc0ISQmjRMSJBhC
+YtJt11kYbdJ7R/jIb/EXqHwgkcQQP/qjjOc2xGkLug/3nvvc53l6zunpfvz8
++g3AAzxnGPKCZhT6zSNHciGdLVpeSQOMwd7zDj2n7QUtZ6O+xxuE6gyDLS7X
+2p4QbiCkFzT4Jn/PMF6p1tJGMW+ZYaYWRi1nj8t65PmBcLwgCKUn/ZDi9VCu
+d9ptYtmNlG3pStM8TORy0GAxlJMpvfXlh9WoFVtMX53ZBY0ePdq4Sj71P3Ee
+/SioRGyGMUrEDQIepRuUTmOjI3k026VTGsP+5eJkEilpHkMYVkmMMJgrjbYf
++PIFg16pblNxV1RgoMyQXYm5eUygZGEc1xgm/12xgesMmYpb3VaiGxamMJ0S
+JTM0MGthThGLtf1QUoLOay69pic9qls7ONRpHplacmoBA9tXgUaXR76KFihq
+LjK8Oz8uW+fHljamWZqpJ3ZtumgTQVtg3z9mTeKVM6Zm64RmCOzpglnbINAk
+MNcFLbtXPWWJpvyScgzcZ7C6NTEYqjfz+5Lmf7MTSP+Au8GhL/x6m692R5y6
+tRY2OUOh5gd8vXNQ59GWRxyGfNeNE896E3aiBn/pq7vSheV2yhCL9KYz1BMd
+ZfUZULce0ylLu0F7WU1kCqMBSWAZlNBDJw1P6DRBqPplvqD3U/wGnl5wFfNP
+XQl59KVVxaQqm1ANYDCtGk2qzL9UJsZIyWLVGuLRwMwZxndOMXmC3jNM7diF
+U8ycoHiGuTi+eYLRz79N+2NRBhZZjpCdjmd0tuh2jv7/HpL5shozPMIK7RuE
+36KmVHahu6i6uO3iDu66uId5Fw4WdsFU+5d2kRcwBXICPQJZgX6BglBgn8CQ
+wLDAgMDgL1Y9gwBpBQAA
+""",
+ """
+androidx/test/TestObject.class:
+H4sIAAAAAAAA/3VSTWvbQBB9s7ZlWXEbN/2InfQrH4ekhyoJvTUUktCCQFWh
+MYaQ09pa0rVlCaS1ydGn/pD+g9BDoIVi2lt/VOmscNpDqRbezHs789gZ9PPX
+l28AXmCb0JZpnGc6vvSNKozfZXjXH6qBqYMIraGcSj+R6YV/o1YIzqFOtXlF
+qOzs9pqowfFQRZ1QNR90QVgL/2f6kuAeDpKy3YOwPW4QnXaPopPXTdyC12Dx
+NmErzPILf6hMP5c6LXyZppmRRmecR5mJJknCVnfCUWbYzH+rjIylkayJ8bTC
+s5GFhgUQaMT6pbZsj7N4n7A9n3meaAtPtDibz9wfH0V7PjsQe3Rcd8X3T45o
+CVt7QNahbkd4PjKE9feT1OixCtKpLnQ/UUd/n8YLOMliRVgOdaqiybiv8q7k
+GsJKmA1k0pO5tnwheqfZJB+oN9qSzsK4948t9nkp1XKSjt0Rx8fMHI4tjoJP
+rWRPmPl2Xo61Z9dwr8rrp4tioImNEssCNNgKcLH0p3mVq+239BXi7BrNz1i+
+KgWBzRIfYav8cXj3bLByjkqAuwHuBbiPB5xiNUAbnXNQgTWs830Br8DDAs5v
+jU1b0HUCAAA=
"""
+ )
+ .bytecode
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
index 4af2ebc..ce9918d 100644
--- a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
@@ -17,28 +17,21 @@
package androidx.navigation.runtime.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.compiled
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestMode
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import org.junit.runners.JUnit4
-@RunWith(Parameterized::class)
-class WrongNavigateRouteDetectorTest(private val testFile: TestFile) : LintDetectorTest() {
+@RunWith(JUnit4::class)
+class WrongNavigateRouteDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = WrongNavigateRouteDetector()
override fun getIssues(): MutableList<Issue> =
mutableListOf(WrongNavigateRouteDetector.WrongNavigateRouteType)
- private companion object {
- @JvmStatic @Parameterized.Parameters public fun data() = listOf(SOURCECODE, BYTECODE)
- }
-
@Test
fun testEmptyConstructorNoError() {
lint()
@@ -47,7 +40,8 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -57,7 +51,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.skipTestModes(TestMode.FULLY_QUALIFIED)
.run()
@@ -72,7 +67,8 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -98,7 +94,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.skipTestModes(TestMode.FULLY_QUALIFIED)
.run()
@@ -113,7 +110,8 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -150,91 +148,92 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.skipTestModes(TestMode.FULLY_QUALIFIED)
.run()
.expect(
"""
-src/com/example/test.kt:7: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:8: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClass)
~~~~~~~~~
-src/com/example/test.kt:8: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:9: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClass::class)
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:9: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:10: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClassWithArg)
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:10: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:11: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClassWithArg::class)
~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:11: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:12: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestInterface)
~~~~~~~~~~~~~
-src/com/example/test.kt:12: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:13: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestInterface::class)
~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:13: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:14: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = InterfaceChildClass)
~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:14: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:15: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = InterfaceChildClass::class)
~~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:15: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:16: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestAbstract)
~~~~~~~~~~~~
-src/com/example/test.kt:16: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:17: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestAbstract::class)
~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:17: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:18: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = AbstractChildClass)
~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:18: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:19: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = AbstractChildClass::class)
~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:19: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:20: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = InterfaceChildClass::class)
~~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:20: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:21: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = Outer.InnerClass)
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:21: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:22: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = Outer.InnerClass::class)
~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:24: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:25: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClassComp)
~~~~~~~~~~~~~
-src/com/example/test.kt:25: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:26: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClassComp::class)
~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:26: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:27: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClassWithArgComp)
~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:27: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:28: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestClassWithArgComp::class)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:28: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:29: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = OuterComp.InnerClassComp)
~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:29: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:30: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = OuterComp.InnerClassComp::class)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:30: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:31: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = InterfaceChildClassComp)
~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:31: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:32: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = InterfaceChildClassComp::class)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:32: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:33: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = AbstractChildClassComp)
~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:33: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:34: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = AbstractChildClassComp::class)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:34: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:35: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestAbstractComp)
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:35: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+src/com/example/test.kt:36: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
navController.navigate(route = TestAbstractComp::class)
~~~~~~~~~~~~~~~~~~~~~~~
27 errors, 0 warnings
@@ -242,528 +241,3 @@
)
}
}
-
-private val SOURCECODE =
- kotlin(
- """
-
-package androidx.navigation
-
-public open class NavController {
-
- public fun navigate(resId: Int) {}
-
- public fun navigate(route: String) {}
-
- public fun <T : Any> navigate(route: T) {}
-}
-""" +
- TEST_CLASS
- )
- .indented()
-
-private val BYTECODE =
- compiled(
- "libs/StartDestinationLint.jar",
- SOURCECODE,
- 0xb1569275,
- """
- META-INF/main.kotlin_module:
- H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgUuMSTsxLKcrPTKnQy0ssy0xPLMnM
- zxPi90ssc87PKynKz8lJLfIu4RLl4k7Oz9VLrUjMLchJFWILSS0u8S5RYtBi
- AADcysPxVwAAAA==
- """,
- """
- androidx/navigation/AbstractChildClass.class:
- H4sIAAAAAAAA/41QTW9SQRQ9M+8DeAV54Belamv9CGUhtHGnaaQkJiTYRW1Y
- wGrgvdAJj/eSNwPpkt/i2o2JxsSFIS79UcY7j2pcsHAxZ+65c3LunfPz17fv
- AF7iGcNzEQdpIoPrViyWciq0TOJWZ6x0Kia6eyWjoBsJpXJgDAfbtJeh0n/0
- OVgM7msZS33KYDeGRwMGq3E0KMJBzoONPHGRThnYsAgPOwVwFEmqr6RiaPT/
- b5tXNGUa6o4xIvshQ6U/S3Qk49a7UItAaEESPl9a9E1moGAANHZG/WtpWJuq
- 4JjhbL2qerzGs7Needzf8Xjeqq1XJ7zNzkpV1+d13rZ+fHC5b19U/rI8qet2
- 3vFd43TCcLh1/X8Doq1oCf9cLLtJrNMkisL0xUxTAN0kCBnKfRmH54v5OEwv
- xTiiTrWfTEQ0EKk0/KbpvU8W6SR8Kw3ZvVjEWs7DgVSSXjtxnOhsssIxpWtn
- /66asKniVDtwCQ+InRLndHvNryg0976g9CnTPCY0GqCBQ8J7GxVuoWxipMq4
- Uerw6Wy8WiZdup3mZ5Q+brUpbgQ3NhxPMtzHU7rfZEs6uD2C1cOdHu72aOx9
- KlHrYRf1EZjCHh6MkFMoKzxU8BQeKbgKvkLlN2AWm1jVAgAA
- """,
- """
- androidx/navigation/AbstractChildClassComp$Companion.class:
- H4sIAAAAAAAA/51Sy27TQBQ9Y6dxYgKkLY+E9yNIbSXqpqrYBCGVIKRIaZEo
- yqYLNLGHdhJ7Bo0nUZdZ8SH8QVdILFDUJR+FuOME2AKb+zr33Ds+199/fP0G
- YA9PGPa4SoyWyVmk+FSecCu1ivaHuTU8tt1TmSbdlOd5V2cfW85wRQ0BGEN9
- xKc8Srk6id4MRyK2AXyG8nOppH3B4G9sDmpYQTlECQFDyZ7KnOFZ/38Wdhja
- G/2xtqlU0WiaRVJZYRRPo1fiA5+ktqsVTZjEVpsDbsbCdDYHITy3eL0V/wHf
- ZwXKsP1v0xhWfxEOhOUJt5xqXjb1SUjmTNUZMLAx1c+ky3YoStoMrfksDL2G
- F3p1iuazysUnvzGf7Xo77GVQ8S4+l72653p3mZuw9fcKBbjNUP0tEx3lkE/p
- 9dboNBVme2xJ+K5OBMPVvlTicJINhXnHhylV1vo65umAG+nyZbHWU0qYYoGg
- c4VHemJi8Vo6rPl2oqzMxEDmkpr3ldK2eF2ONmldcgKQ99zV6TvuUxY5Rciv
- bH1B5byAH5AtF8UOHpKtLRpQRQjUGUWXluSn5L0luXZeqOsINxbFBaGILuMK
- YT4eURYWpDu4iyYeFwvvoVX87aQB9daP4few2sNaD+u4RiGu92jmzWOwHA00
- Cc8R5riVo/wTjWwvPCoDAAA=
- """,
- """
- androidx/navigation/AbstractChildClassComp.class:
- H4sIAAAAAAAA/5VSS08TURT+7vQ1HYqUiljABwpiqcgUQlxYQoI1mialCyRN
- hNVtey2XTu+YubcNS36LazdEDYkmhrj0RxnPTCskygIXc849Z77znefPX1+/
- A9jAOkORq3bgy/axq/hAdriRvnK3m9oEvGUqh9JrVzyudcXvvU+BMSxehd8T
- 2lzERMgYQ3JTKmm2GOKF/eUGQ6yw3MgggZSDOGyyedBhYPsZOBhLw0KGoOZQ
- aoaV2vWrKlOmjjDbIRml2GewN1veKPXG9XkWQ8EVAVK4ybBWqHV9Qzzu0aDn
- SmVEoLjnvhTveN+jJhVx9FvGD3Z40BVBedjbLQdTmGZIX5AxPPuPZi6LKGeQ
- x0w4llmGhZofdNwjYZoBl0q7XCnfRDzarfum3vc8GsPkn4p3hOFtbjj5rN4g
- RqtmoUiHAjTyLvmPZWiV6NVeY3h9fpJzrLwVfecnjpUdcyw7nj8/mU+tWyX2
- nKVejOeSWWvWKsV+fEha2fju5IVlU8hs3E5kkyEdHdXSlS3/fSVUHlWTrfMB
- jdMEvueJYLVrGOZ2+8rInqiqgdSy6Ynty37pRip+WzBM1KQS9X6vKYI9ThiG
- XM1vca/BAxnaI2emqpQIogELCnbe+P2gJV7J8N/MKE/jnyxYo8HHaUAWZsI9
- UJ1PyEqSvkM6F54s6RjZici7QtYWoS3STvEM6eLcF4yfRgxPR5FAGaskp4co
- 3MBEuBB6hWy0P2TpG3K54Z5IJ4qfMf7xSprMEDCisamo1Cg4j2jTyHzD1Ft2
- htufMHcaeWJEHCZkdKZW1Fgp4i5Sw0CF/HeJ8d4BYlXcr2K+igd4SE8sVLGI
- RwdgGkt4fABbY0KjoOFoLGskNbIakxr53/A96KpcBAAA
- """,
- """
- androidx/navigation/AbstractChildObject.class:
- H4sIAAAAAAAA/41Sy27TQBQ9M3k5bqChPJpQHqVFgrDAbcWOCilEIFkyRqJR
- JNTV2B610zgzkj2JusyKD+EPKhaVQEIR7PgoxLUJD4kusOV759w5c67uGX/7
- /vEzgCe4z/BA6CQzKjn1tJipI2GV0V4/ym0mYjs4VmnyOjqRsW2AMWxeRB7K
- 3P460ECFob6vtLLPGCoPe6MWaqi7qKLBULXHKmfoBf/Z8ymDsx+npZoLXkg4
- fngw7IeDFy1cgtuk4mWG7cBkR96JtFEmlM49obWxpWruhcaG0zQlqSvB2FgS
- 815JKxJhBdX4ZFYhJ1gRmkUAAxtT/VQVaIdWyS41WMxdl3d4+S3mztd3vLOY
- 7/Ed9rzh8C/v67zNC+oew9aFw/3tEbVth2I2MNpmJk1l9nhsGTbeTLVVE+nr
- mcpVlMr+nyHIuYFJJMNqoLQMp5NIZkNBHIa1wMQiHYlMFXhZdA/MNIvlS1WA
- 7lJ49I8sdsm+ajlzt3CT8h1CdcptypzeWonuEvIKZyjXHp3DOSu3N5dkoId7
- FFs/CWiSFOBg5ffhdWIXz8on8LfnaH3A6llZ4Ngq421slz8k3RIJrB2i4uOq
- j2s+ruMGLbHuo4PuIViOm9ig/Rxujls56j8AJAtPiM0CAAA=
- """,
- """
- androidx/navigation/AbstractChildObjectComp.class:
- H4sIAAAAAAAA/5VSW2sTQRT+ZnLbbKNN66WJVau2FC/otsU3gxCDwsK6gg0B
- 6dNsdmin2czI7iT0MU/+EP9B8aGgIEHf/FHi2TVU0L64y57bfOc7nG/2x8/P
- XwE8xRbDI6Hj1Kj4xNNiqg6FVUZ73SizqRja3pFK4jfRsaTQjN/XwBi2Lmro
- y8yeNxXIEkO1o7SyzxlK9x8MGqig6qKMGkPZHqmM4XHwH7OfMTidYVIwuuA5
- jeOH+/1u2HvZwCW4dSpeZtgMTHroHUsbpULpzBNaG1swZ15obDhJEqJaCUbG
- Epn3WloRCyuoxsfTEqnCclPPDRjYiOonKs92KIp3acB85rq8xYtvPnO+f+Ct
- +WyP77AXNYd/+1jlTZ5D9xi2L1zwb61odDMU057RNjVJItMnI8uw/nairRpL
- X09VpqJEdv8sQgr2TCwZlgOlZTgZRzLtC8IwrAZmKJKBSFWeL4ruvpmkQ/lK
- 5Ul7QTz4hxa7JGG52LudK0r+NmVV8k3ynN5KkW1Q5uXqkK88PINzWhzfWYCB
- Du6SbfwGoE5UgIOl8+Y1QufP0hfwd2dofMLyaVHguFfYW9gsflC6KSJYPUDJ
- xxUfV31cw3UKseajhfYBWIYbWKfzDG6GmxmqvwBXhfge3QIAAA==
- """,
- """
- androidx/navigation/InterfaceChildClass.class:
- H4sIAAAAAAAA/41Qz2sTQRT+ZvJjk21qNqnWNPVXrdomBzct3pRiGxACsUJb
- ckhOk+yaTrOZhZ1J6DF/i2cvgiJ4kODRP0p8k4aCkIMs89775n37vTff7z8/
- fgJ4hT2GPaGCJJbBta/EVA6FkbHyW8qEyUcxCJuXMgqakdDaAWPwrsRU+JFQ
- Q/9D/yocGAcphp1VEhehNrcyDjIM2TdSSXPEkN7v1joMqf1apwAHeRdpuIRF
- MmRg3QIKWM+D4w5RzaXUDLX2f275msYMQ3NslUi/y1Bqj2ITSeW/D40IhBFE
- 4eNpit7PbMjbAJo7ovtraVGDquCA4WQ+K7u8whdnPnO5t+byXKoynx3yBjtZ
- L2c9XuWN1K9PWe6lz0q3KEfsajqX8bJW6ZBhd+X+/1hEa9EW3qmYNmNlkjiK
- wuTlyJAFzTgIGYptqcLTybgfJheiH9FNuR0PRNQRibR4eemex5NkEL6TFmyd
- TZSR47AjtaTusVKxWYzWOCB/0zQwS6dsDad3c6od5Cg+JXREmFN269+xVt/+
- huKXBWeXov0LeEYfsHnDgoeSdZIqq0bGk+7GUsu3BlPO1L+i+HmlTOGGsJTh
- eL6IO3hB+S317lLvXg+pFjZbuN9CBVtUotrCNh70wDQe4lEPjkZJ47FGQeOJ
- Rk6jrLHxF0rwRpzxAgAA
- """,
- """
- androidx/navigation/InterfaceChildClassComp$Companion.class:
- H4sIAAAAAAAA/51Sy27TQBQ9Y6d5mABpyyPhXQhSC6JuKhBIRUgQhGQpLRJU
- 2XSBJva0ncSeQeNJ1GVWfAh/0BUSCxR1yUch7jgB1mVzX+eee8fn+uev7z8A
- PMVDhmdcJUbL5CRUfCKPuJVahZGywhzyWHSPZZp0U57nXZ19bjvDFXVUwBga
- Qz7hYcrVUfh+MBSxrcBnKL+UStpXDP76Rr+OJZQDlFBhKNljmTM87/3Xxh2G
- znpvpG0qVTicZKF0DMXT8K045OPUdrXKrRnHVptdbkbC7Gz0A3hu82o7/gd+
- ygqUYfN80xiW/xB2heUJt5xqXjbxSUrmTM0ZMLAR1U+ky7YoSjoM7dk0CLym
- F3gNimbT6tkXvzmbbntb7E2l6p19LXsNz/VuMzfh8TkkquAmQ+2vTnSWPT6h
- 51uj01SYzZEl6bs6EQyXe1KJvXE2EGafD1KqrPR0zNM+N9Lli2I9UkqYYoGg
- gwUf9djE4p10WOvDWFmZib7MJTW/Vkrb4nk5OiR2ySlA3nN3pw+5S1noJCG/
- 9OgbqqcFfI9suSi+wBrZ+rwBNQRAg1F0YUF+Qt5bkOunhbyOcG1enBOK6CIu
- EebjPmVBQbqF22jhQbHwDtrFD08aUG/jAH6E5QgrEVZxhUJcjWjm9QOwHE20
- CM8R5LiRo/wbyZhkAy0DAAA=
- """,
- """
- androidx/navigation/InterfaceChildClassComp.class:
- H4sIAAAAAAAA/5VSXU8TQRQ9sy3dtizSFsVS/ABBLUXYghiNEBKs0TQpNUHS
- RHiatkPZdjtrdqYNj/wWn30hakg0McRHf5TxTqnwoInhYe+dc/fecz9//vr6
- HcAa1hgWuWyGgdc8ciXvey2uvUC6ZalFeMAbonTo+c2Sz5UqBd33NhhDqs37
- 3PW5bLlv6m3R0DYiDLP/otkVSl9Q2RhhiG140tObDNH83kKNIZJfqDmwkUgi
- iiRhHrYY2J4DB2MJWLhGrvrQUwxLlStUuk6pWkJvGTbKsccQ32j4w9xPrkA0
- bwSX5GHjBsNKvtIJNBG57X7X9UyM5L77Uhzwnq9LgVQ67DV0EG7zsCPC9fPu
- biYxiSxD4oKM4elV2rmsYt1BDtNmMrcY5ipB2HLbQtdD7knlcikDPSBSbjXQ
- 1Z7v0yDSf0reFpo3ueZks7r9CF0AMyJhBGjqHbIfeQYV6dVcYXh9dpxJWllr
- 8J0dJ63UaNKKR7NnxzP2qlVkz5n9YiwTS1k5qxj58SFmpaI76QsUp5BcND6S
- ihm6VVPvf6+EaqNSUlXep2HqMPB9ES53NMP0Tk9qryvKsu8pr+6Lrctm6UZK
- QVMwjFc8Kaq9bl2Eu5x8GDKVoMH9Gg89g4dGpyylCAfTFRScfBv0woZ45Zl/
- U8M8tb+yYIWmHqXqYqSnzBrovUTTipG+QzpjjpZ0hLCNOMllQpvkbZFOFk4x
- Wpj+gvETQhbcYSTwDEWSk+deSCFt9kEvw0brI96JIZdr1kR6pPAZ4x//SeOc
- Owxp4riOxDA4i8Gi4XzD5Dt2iqlPuH0ysESoNZOQDYrIUXOrA+5HeEy6RPa7
- xDizj0gZs2XcK2MO8/TE/TIe4OE+mEIeC/uIK6QVCgqOwqIyMKMwoZD7DYsK
- p/5yBAAA
- """,
- """
- androidx/navigation/InterfaceChildObject.class:
- H4sIAAAAAAAA/41STW/TQBB9u0kTxw00LV8J5asUUOkBtxU3KqQSgWQpGIlG
- kVBPG3tJN3F2JXsT9ZgTP4R/UHGoBBKK4MaPQsyaUA4gga2dmfd25u3O2N++
- f/wM4DEeMGwJnWRGJSeBFlM1EFYZHYTayuytiGX7WKXJq/5QxrYKxtAYiqkI
- UqEHwS+2xLDxN42uzO25ThVLDJV9pZV9ylDaetirowrPRxk1hrI9VjnDdud/
- 7/KEwduP00LOB3caXhgddg+i9vM6VlCvEdlg2OyYbBAMpe1nQuk8EFobW8jm
- QWRsNElTklrtjIwlseCltCIRVhDHx9MSjYg5U3MGDGxE/IlyaIeiZJcOmM98
- nzd5seYz7+s73pzP9vgOe1b1+Jf3Fd7gLnXP3eWfU6JzG5GYto22mUlTmT0a
- WYb11xNt1ViGeqpy1U/lwe8uaHZtk0iGlY7SMpqM+zLrCsphWOuYWKQ9kSmH
- F6R/aCZZLF8oB1oL4d4fstil+ZWp5Qqtlhso+TvUt8Nr5Dm99P0IbRAK3HDI
- L22fwT8ttu8ukoH72CRb/5mAZYpAhRfOi69RtnuWP4G/OcPFD1g9LQiOe4W9
- TRLuZ2W4RAKXj1AKcSXE1ZBKmxSiFeI61o/ActzATdrPUc9xK4f3Az/F2jnp
- AgAA
- """,
- """
- androidx/navigation/NavController.class:
- H4sIAAAAAAAA/41SW08TQRT+Zttut8utrYJQQeWiFFC2EH2xhERJjEtqNdL0
- hadpuynTbmeT3WnDY3+L/8AnjQ+G+OiPMp7ZNlBAo5vsuX/fnDNzfv769h3A
- c+wzrHLZCgPROnckH4g2VyKQTpUPjgKpwsD3vTANxpDt8AF3fC7bzvtGx2uq
- NBIM5oGQQh0yJIpb9WmkYNpIIs2QVGciYliv/JO9zGCNcx7hiu5WnSEVepHb
- YmAuw3yxcnX2iQqFbJd1zXolCNtOx1ONkAsZOVzKQMUHRE41UNW+75c1U9BX
- noUcw4NuoHwhnc6g5wipvFBy33GlZoxEM0rjDh3WPPOa3TH8Aw95z6NChs3J
- JkYXUP5TW9OYx4KNu7jHkL9dcGOaMZGeZvmg9vJ25rBYq8Xp/O0cQ64ynuid
- p3iLK04xozdI0NMyLTJagG6xS/Fzob0SWa09hvBiuGwbi4ZtZC+GtmFpg34r
- Sdqif9ZaWLwY7hsl9jr145NJyeOVbKJglJJrlnUxzKa2KbVvZs2C8XZUkD6e
- HRVQ1CKdmfCpqmTrk2nfdD812qdrS7DbVfT2R0GLVmCuIqRX7fcaXljjDd/T
- wwdN7td5KLQ/Dm587Eslep4rByISFLp8rVdXi8CQORFtyVU/JIh9EvTDpvdG
- aPzSGF8foSdAWIVBW6y/JHVLS01yhzxH9046tf0F1mcyDDwlacbBJJ6RnB4V
- IAObdA5TcUSDX8T1NP5NoBkDF0bJMVBbM5glqSnmKKcpyqR1VXonn/+KxetE
- JqwJovQlUZoolii/q23df3bcWAGJ/2G1/8q6THknrr5/nd1AKZbb2CNdoegK
- XcmDUyRcPHTxyKUbXiMT6y428PgULMITbJ5iKoIdoRjBjLRNxlaEXIRChJnY
- Lf4G70EG2roEAAA=
- """,
- """
- androidx/navigation/NavControllerKt.class:
- H4sIAAAAAAAA/41UW08TQRg9s1va7VJoy70F5Y6tFxbwLmhCmphsrCVBgiEk
- Jtt2rAvLbLIzbXjkt/gLVB5IJDHER3+U8duV2NCC0IedmTPnnD3z9Zv99fv7
- DwCP8JJh1hH1wHfrh5ZwWm7DUa4vrIrTKvlCBb7n8eCNSoAxZPaclmN5jmhY
- G9U9XiNUZxhscFXyHCltIZUjanyTf2SYLhTLl/lucfmXvUovLvtBw9rjqho4
- rpCWI4SvIpq0Kr6qND2PWJlal/nkNdYpGEgmocFkyHfGe++qT+tBIzIqXJfy
- nEwxRmtXmSzczCKFfqTDUBmGMQplC8GD7sJdFWmjqXgw1xZRpGH3covLA3UZ
- pDCE4TDQCIOxVvNc4apXDHqhuM1w679nSiDPEF+LFClMIGdiHLcY5m5SiQQm
- GWIFu7gdSqdNTGHmCmln5gTmTMyH9Gx531cU2XrLlVN3lEP10A5aOjU1Cx/J
- 8AEGth9ONNo8dMPZEs3qywwfzo7y5tmRqY1ppmboHaM2k80QQVtiPz/HDeLl
- Y4aW0QmNEdjTBuOZBIEGgck2aGZ6w7esMOSuPFQCDxnM9smozy9cucV9xTC+
- 2RTKPeC2aLnSrXp8vX1DqIQlv84Z0mVX8ErzoMqDLYc4DKm2LSee+c5vBjX+
- 2g33cueW212GWKZWiFGJdOTD+0PFe0qrOI0JGvNh+3Zh1EEdWAw59NBKwzNa
- TRAa/mLf0Psl+kOen3OBvgu6HFKEdKmynap0h2oAg92q0U5V9oLKwBgpWaQq
- IeoUzJ5ifOcEt4/Re4qpnUz6BLPHyJ5iPpovHGP06z/T/kjUB5PijJC5jhe0
- Nml3nr6pj8l8New6PMEajRuE36GiFHah2yjauGvjHu7beIBFGxaWdsHC8q/s
- IiVhSCQleiTiEv0SaRmCfRJDEsMSAxKDfwAPYTX9vQUAAA==
- """,
- """
- androidx/navigation/Outer$InnerClass.class:
- H4sIAAAAAAAA/41U31MbVRT+7m5+bJYENkBbfkRQiZiEtgvYai20CiiyGEIF
- h7HiyyVZw8Kyi7sbpr44PPVP6Iy+OOM4PvFQZxQcO+Ng++bf5Diem90mNVSG
- meSec8+e853vnnPu/euf3/8AcAPrDHnu1DzXqj3QHX5g1XlguY6+2ghML284
- jukt2Nz3k2AM2g4/4LrNnbq+urVjVoMkZIbErOVYwV2GWMEobjDIheJGGnEk
- VcSgMCiWQJnz6gzMSENFVwoS0uQfbFs+w3j5IgRmGLrqZmC0sCiNwaBW3b19
- 1zGdYIoAq+7+1wxF4nFRzLGy69X1HTPY8rjl+Dp3HDdoevt6xQ0qDdueEYdJ
- qMT5MkNapMjXzC95ww4YtgoXS2QY5c7azVyQYxp96BfZh6iUgbseeJZDx+8v
- FF+ADK10niudtvmGZddML4kRFaOiHf1t7MLzztxR8Bo1ku/vm06N4VrhLPTZ
- bBEyERxDXoC/wZATpT/P8U3hWBCOC+c7loTjRBo5vCK0a3T4be5vL7g1kyHb
- jjScwKyL802GA0gTpmNaxRTeohOZXzW4TTN2qfCS+n9Os39e+6n3fMs2qapx
- N9g2PYbesyhEprzrBrbl6CtmwGs84GST9g5kul9MLCmxgIZ/l+wPLLEjrlKN
- BvbH08MRVRqQVEk7PVTpJ2mKKikJkl0kZZLdytOHysDp4bQ0yeYzvQlNGpIm
- 5ac/JCQttpzSkmK39OyhvNynKaSTo6JIoROZGZlTpKvTitY1FBtgk2zp2SOZ
- AtOhxyNGeob0bqGvZVvwCtEZiilxLSG4TjNxgsH/HdgkFukutieL3ooKP1hw
- ncBzbdv0ru/SZYmFzespW45Zaextmd6nor6irG6V2xvcs8Q+Mg6vNZzA2jMN
- 58DyLTLNtXvDkFkPeHV3he9H3vlO73vc43smUftPWLpN0aStuu42vKq5aAmI
- wQhi40w6GiaJ3jJRgl7xfpGmkE6vAq3LtFuk7xJJtXSCVGn4V2R+pp2Ej2nt
- huj4CMWPIkWyTLvLoTd96xGzQZpApVGCRv8QUxcjQzJe+gWZoxZcomkcbcKk
- Q4cIJkvkngePdQazlwbQy0KwImCKWApOqSeQ7g+f4MrjVlBINtUim4rIrkRs
- LgFaCgMYjHKPR8XK5mLffAtFMJgtDR9jOISs0CqDCQS621H62yQFtdwTjN4/
- wau9rx9jXEQeo6gVj3H1GNcfdxwjFzF6gQeteqsG41ENmgx+w43OMihRPMNN
- vB3x+IKkaFe+NPET4rGjiT8hfYe4fDRxCmlFAF2l//fCEgt7Umm2T04qfyOb
- pH27YvlWxfK4hXcpzyrpSUHqnWYN7jVD6XrhIyxR+T5pAhpYI/kZ2W9Tp2Y2
- IRuYNXDHwF28RyreNzCH+U0wHwv4YBM9vvh96ENtrgkfmo+sj14ffT5uNo23
- fOg+cqT/CwA97z/6BwAA
- """,
- """
- androidx/navigation/Outer$InnerObject.class:
- H4sIAAAAAAAA/41US08TURT+7p0+plMe5SFvnxR5KVMQVxATJBqHlGKEYJTV
- bTvCwDCjM7cNS1bu3Lpw6cIVC4kLEk0MStz4o4jnTkdBCMakvec7Z84535nv
- 3Pbn8eevAKZxl2FIeNXAd6o7pifqzrqQju+ZSzVpB3nL8+xgqbxpV2QajCG3
- KerCdIW3bv6OagypWcdz5D0GbWR0tQlJpAwkkGZIyA0nZBgu/hfDDIMu/WUZ
- ON46Q+fIaPGErRGljMGiH6ybm7YsB8LxQlN4ni+jhqFZ8mWp5rqUlT3VVkcL
- Nd4Q4ca8X7WjIS3tx/TxGxrcflUTLk14aaR49s1mRp8z5P/FRlSi7NpEl/Tl
- hh0wtJ/vQtSzFTfSxwBXouhWaXllrjT/oAl9MDIU7GdoK275ktLMRVuKqpCC
- Cvl2XaMdMXVk1AEGtkXxHUd5BULVSYYXh7tXDN7DDZ473DW4rkA2trqhQrkW
- /ei10XO4O8UL7H5a59/fp3iOL3TktD5eSEzpuWRfoocV2KOjt9pCJpeiaJow
- I6wTziis2KaYmqH3wm2mMUp3pCTq874nA9917WBiSzL0P6l50tm2La/uhA5p
- NneiI92Sxl5ai45nl2rbZTtYUboqOf2KcFdF4Cg/DjYvS1HZWhQvYz9/tvdj
- EYhtm6b5i6QpuhHzrghDm1xj2a8FFfuho1r0xi1Wzw2HSVpPIlK+V22L7C3y
- UmSbySbpaTLybpNnqv2o6NgB9H0CHBNxMjBAj4GmRgIy1Eo1zVKER8XX42Kt
- vfVj9OgkXYvTTzOTzGiLeU9K2/cuKGXoQGfMZJHlZLvHxj8gmdgb/wb+Dklt
- b/wQ/GliLxq8QGcCPK1HzboaBXEzhbroy0gdqBtNvx8COnr+SNEdFQDZL+DP
- DtD7CQP7UUDDFJ1KR44xtJCqdyK+cforUqMxXCZ5rqxBs3DVwjWL3u4GQQxa
- yGNoDSzETQyvwQjVZyREKkRHBLpC5CKQpfMXu5HXfeAEAAA=
- """,
- """
- androidx/navigation/Outer.class:
- H4sIAAAAAAAA/4VRW2sTQRg9M5vLZhNtGi9NjK2XptpUcNviUy1CDQoLMYW2
- BCRPk2SIk2xmYXcS+pgnf4j/oPhQUJCgb/4o8dttNA9S3GG/M9/tfDNnfv76
- 8g3ACzxjqAjdDwPVP3e1mKqBMCrQ7vHEyDALxlAciqlwfaEH7nF3KHsmC4sh
- c6i0Mq8YrO16u4A0Mg5SyDKkzAcVMVSb17K+ZLAPe37S74DHTbbXOj07ajXe
- FHADTo6CNxk2m0E4cIfSdEOhdOQKrQOT8ERuKzCtie8T1WpzFBgic99JI/rC
- CIrx8dSi27HY5GIDBjai+LmKvV3a9fcY6vNZweFl7vDifOZw27J/fOTl+Wyf
- 77IDbqVeZ23+/VOGF3ncsM9iGsfTWoYNX0R0yXziXKnCULv2xrVlUxYPGLb+
- U/lH50ekfktMG4E2YeD7Mnw+ojnVk4k2aiw9PVWR6vryaCkM6d8I+pJhpam0
- bE3GXRmeCaphKDWDnvDbIlSxvwgWlieT1OycBpOwJ9+qOFdZzGn/MwV79EKp
- RNZK/GCENfIyhEVCTiudeFvkubH4hOmdS9gXSfrJohio4inZwlUBckQF2Mj/
- bV6j6vjLfwV/f4nCZ6xcJAEL22RLlH5I/zqd4zHhBmE9GbGJHcIDolkl4lIH
- lodbHm57uIO7tMWahzIqHbAI91DtIB3BiXA/QibCeoSN33FlSFUiAwAA
- """,
- """
- androidx/navigation/OuterComp$InnerClassComp$Companion.class:
- H4sIAAAAAAAA/51TTW/TQBB9a6d2YkJJUz4SoHwGSBHUTQUIqQgJgpAipa0E
- KJce0CZZyib2Gq3XUY858UP4Bz0hcUBRj/woxKwTqLgglcvMm3nzZr0z3h8/
- v30H8AhNhidcDXUih4eh4hN5wI1MVLiXGaHbSfyp0VGKUMTTNA+t4YpKfDCG
- yohPeBhxdRDu9UdiYHy4DN4zqaR5zuA213tlLMELUIDPUDAfZcrwtPt/R24z
- tJrdcWIiqcLRJA6lIoniUfhKfOBZZNqJSo3OBibRO1yPhd5e7wVw7NGrjcEJ
- +T7OWYaN03VjWPkt2BGGD7nhlHPiiUvDZNaUrAEDG1P+UNpok9CwxdCYTYPA
- qTmBUyE0mxaPP7u12XTL2WQv/aJz/MVzKo6t3WK2w4PTzMjHFYa1fyp8rDEs
- /y1jKP0ZLi1zl0/ozkYnUST0xtjQwtrJUDCc60oldrO4L/Q73o8oU+0mAx71
- uJY2XiTLJ90FrTl4m2R6IF5Ly9XfZMrIWPRkKqn4hVKJyb8wRYs2VLBjI+/Y
- v4Vuf4ui0M6R/NL9ryge5fRtsl6efIwG2fK8ACUEQIUROrMQPyTvLMTlo3wn
- VnBxnpwLcnQWy8S5uENRldiruIbrqOfoBvm7+cE3cS9/LzQL0lT24Xaw0kG1
- g1WcJ4gLHep9aR8sRQ114lMEKS6n8H4BDr3L2mwDAAA=
- """,
- """
- androidx/navigation/OuterComp$InnerClassComp.class:
- H4sIAAAAAAAA/5VUW0/cVhD+jvfmNQt4IRcCmyYt23S5BHNLSoE0XFKK6QIp
- pDSE9uGw64LB2NT2ovSlylN+QqT2pVIf+sRDorZQFamiyVt/U1V1jm2WW4S0
- 0u45M+OZb74zM+f889+ffwEYxNcM3dwuu45ZfqrZfMdc477p2Np8xTfcSWdr
- O6/bNkkW9zyhpsAY1A2+wzWL22va/OqGUfJTiDEkR03b9D9miBf0jiWGWKFj
- KYMEUgrikBlkUyCNu2sMTM9AQV0aEjLk76+bHkNPsRYiIwx1a4avVzEpnc6g
- lOibYxu230fAJWf7O4Y+4lMrdnvRcde0DcNfdblpexq3bccPojxtzvHnKpY1
- Ig6XVOgMVxgyIlW+bHzDK5bP4BZqS6jrxbM1HamRcwbNuCTYtFKpfWfRd02b
- ynKp0HECOrTS+a6etU1UTKtsuCm8o+CGaFfLafzCUffuyXiXms23tw27zHC7
- cB7+fMYInUi2Iy8SvM+QE225yPED4VgQjpMXO3YKx64McrgupNtUgHXurU86
- ZYMhexyp276xJs7YGw4pTaGGfgV9GKATGd9WuEVzeLnwll48YchfNBI0D3zV
- MqiyCcdfN1yGpvMoxGu0ZEW35G4t3c2LhdvkksKImOjipuMTkraxs6WZdCzX
- 5pb2IBy/SWLku5WS77iz3N2kIoUX8Z6CUVDmdBWMYaimITumQXUfw7i4wBNU
- 4iM2s4bPy9znRFHa2onRC8PEkhYL6Npvkv2pKTTqgFSmK7p7+OymIrVIiqQe
- PlPoJ6myIslJ2utoj9HeQGb59XO5hVwb+6VeNswaJ+qbkqrUKvXGXv+clNT4
- TFpNCW36zfPYTLMqk3z4rF+WpdCJzIzMaZKVflmta423sF42/eZFjAIzoccL
- RnI9yQ1CXshW4WXK3xqXE2pScO5n4iTXL6xaCg8ZGk6Xjl7NOb5DrfFdx7IM
- t2eTnom2hYrtm1uGbu+YnknzM348UzSi4QA3Fk3bmKtsrRruIzFjYrScEreW
- uGsKPTLWL/q8tDnLtyM9fxb7IXf5lkEUTyXJHNM0SFUWnYpbMqZMAXEtglg6
- R46ujESvOmi9JgaBSvKItCTtl2lvEq+7aDzpicD6BWlT5C3RrnTuI93Z9jvq
- XwUIS7Q2QEzFAGEOUtQAviTtSuhN3xrF/JAkUGncoNI/xNTEWNGe6PwN9btV
- uGRgHAxgMqFDBJMlckfB7WeD2VsD6F0lWBHQRywFp/QBpOW2fVx9WQ0Kyaar
- ZNMR2RNlUdNooXKFuW9FBczm4t//AFkwGO1s20NbCPmY1hiYQKBXLUo/TLug
- ljvAjeV93Gx6bw+3ROQeOtSOPXTvoeflmWPkIkYn28OobNkqj7AGAYM/MHi2
- DHIUz3AHdyMeX9Eu2pXv7PoFifhu19+QfkQittt1CGlWAHXT/ydhiYc9eRy0
- L5aS/0U2RfpxxfLViuUxhI8ozzLJKUHqwyD9MFIR1ZYgKRE7wOgy28f9XzH5
- KrDE8CSYOjFfn2OBijxK0hjtK0H6RaIMkhkeUF8/WUFMx5SOT3VMQycRMzo+
- Q5EcPMxibgWqh0YP8x6UYE16wpL10OSh2cOdwDjkQfOQC+Sx/wHugknKUQkA
- AA==
- """,
- """
- androidx/navigation/OuterComp$InnerObject.class:
- H4sIAAAAAAAA/41US08TURT+7p0+plMe5SFPxQeolCpTUFcQE2w0DinFCMEo
- q0s7wsB0BmduG5as/AkuXLrQDQuJCxJNTJWdP8p47jAKQiQm7T2POef7znzn
- tj9+fv4K4C7uMeSFVwt8p7ZjeqLprAvp+J652JB2UPLr22OW59nB4tqmXZVp
- MIbcpmgK0xXeuvk7qzGkZh3PkfcZtPH8ShuSSBlIIM2QkBtOyFAo/zfLDIMu
- /SUZON46Q+94vnzMeJSlitGyH6ybm7ZcC4TjhabwPF9GoKFZ8WWl4bpUlT0B
- q6ODgDdEuFHya3Y0qKV5H4qzNLz9qiFcmvLCePn0283kXzCMncdGVGLNtYku
- 6csNO2DoPotC1LNVN9LIAFfC6FZlaXmuUnrYhiEYGUoOM3SVt3xJZeaCLUVN
- SEGNvN7UaFdMHRl1gIFtUX7HUVGRvNoUw8vW7ojBB7jBc61dg+vKycZWN1Qq
- 16EfvjYGWrvTvMgepHX+/V2K5/h8T04b4sXEtJ5LDiUGWJE9PnyjzWdyKcqm
- yWfk6+RnlK/Yppma4dK5G00jT3elIpol35OB77p2MLklGYafNjzp1G3Lazqh
- Q7rNHWtJt+VoN51lx7MrjfqaHSwrbZWkflW4KyJwVBwn25ekqG4tiO04HjuN
- /UQEom7TRH+RtEW3ouSKMLQpNJb8RlC1HzkKYjCGWDkzHKZoRYlI/UG1MbK3
- KEqRbSebpKfJKLpNkal2pLITB9D3yeGYjItBQCadbUcFyBCUAs1ShkfNV+Nm
- rbvzY/TouFyLy08yk8zoinmPW7v3/tHK0IPemMkiy8n2TxTeI5nYK3wDf4uk
- tldogT9L7EWDF+lMgKf1CKzvqCEGU14ffRm9FNStpt8QOToG/kjRHzUA2S/g
- zw8w+AkX96OEhmk6lY4cE+ggVe9EfAX6W1Kj0Q0jeUZWoVm4bOGKRW93jVyM
- WhjD9VWwEDdwcxVGqD7jIVIheiKnL0QucrJ0/gKE9w8Q7AQAAA==
- """,
- """
- androidx/navigation/OuterComp.class:
- H4sIAAAAAAAA/41RW2sTQRT+ZjaXzWZt03hpYk2rtmpTxW2LT7UINSgsxC20
- JSB5miRLnGQzK7uT0Mc8+UP8B8WHgoIEffNHiWe30SJCcYc935zbd+ac8+Pn
- 568AnuExQ02oXhTK3qmjxET2hZahcg7H2o8a4eh9HoyhNBAT4QRC9Z3DzsDv
- 6jwMhty+VFK/YDA26y0bWeQsZJBnyOh3MmZYa17J/JzB3O8GKYcFniSarnd8
- cuA1Xtm4BqtAxgWG9WYY9Z2BrzuRkCp2hFKhTrlixwu1Nw4ColpqDkNNZM4b
- X4ue0IJsfDQxqEuWiEIiwMCGZD+VibZNt94OQ302tS1e4RYvzaYWNw3z+wde
- mU13+Tbb40bmZd7k3z7meIknCbssoVlwlaI2AhHHSS8MxdRwMR2GJ1d2vvF3
- ch5r9Ij/yPg9+3u0EU9MGqHSURgEfvR0SDVXjsZKy5HvqomMZSfwDy4HRTtp
- hD2fYbEple+NRx0/OhEUw1Buhl0RtEQkE31utC9f6FOydRyOo67/Wia+6rxO
- 658q2KGNZdIxV5MFEm6QliMsEXI62VR7QJqTLIMwu3UO8yx1P5wH02rwiKR9
- EYACUQEmin+Slyk6+YpfwN+ew/6ExbPUYGCTZJncd+mv0TvuE64S1tMS69gi
- 3COaJSIut2G4uO7ihoubuEVXLLuooNoGi3EbK21kY1gx7sTIxajFWP0Fraxj
- CzoDAAA=
- """,
- """
- androidx/navigation/TestAbstract.class:
- H4sIAAAAAAAA/4VRy0oDMRQ9SduxjlWnPusL1IWvhaPiThFUEApVQaUbV2kn
- aOw0gUlaXPZb/ANXggspLv0o8WZ07+ZwHjfh5Obr+/0DwCFWGFaFTjKjkudY
- i756EE4ZHd9J605b1mWi7UbAGKIn0RdxKvRDfN16kt4tMATHSit3wlDY2m5W
- UEIQoogRhqJ7VJZhvfHf5UcM1UbHuFTp+FI6kQgnyOPdfoEKMg+jHsDAOuQ/
- K6/2iCX71H04CENe4yGPiA0H5Y3acHDA99hZ6fMl4BH3cwfMn46uRP/caJeZ
- NJXZbsdRyXOTSIbJhtLyqtdtyexOtFJyphqmLdKmyJTXf2Z4a3pZW14oLxZu
- etqprmwqqyg91dq4/HG2uAZOO/ir7FdCWCMV5xoo7byh/EqEY4EwyM1NLBJW
- fgcwijDPl3Kcx3L+VwxjlFXuUahjvI6JOiYREUW1jilM34NZzGCWcovQYs4i
- +AEAejLS6AEAAA==
- """,
- """
- androidx/navigation/TestAbstractComp$Companion.class:
- H4sIAAAAAAAA/5VSy24TMRQ99qR5DAHSlkfCuyVILRKdpGJFEVIJQoqUFolW
- 2XSBnIkpTmY8yHaiLrPiQ/iDrpBYoKhLPgpxPQmwpZv7Ovfcax/756/vPwA8
- xxOGHaGHJlPDs0iLqToVTmU6OpbW7Q+sMyJ2nSz93PRGaIJKYAy1kZiKKBH6
- NHo3GMnYlRAwFF8qrdwrhmBru1/FCoohCigxFNwnZRlavcut2mNob/XGmUuU
- jkbTNFLaSaNFEr2RH8UkoXZNvEnsMnMgzFiave1+CO5Xrjfjf+CHNEfprpeb
- xrD6h3AgnRgKJ6jG02lA4jFvKt6AgY2pfqZ81qJo2GZozmdhyOs85DWK5rPy
- xZegPp/t8hZ7XSrzi69FXuO+d5f5Cc3/0aaEuwyVvwLRQxyKKZ3bmSxJpNkZ
- OxK7kw0lw/We0vJwkg6kORaDhCprvSwWSV8Y5fNlsdrVWppOIqyV9EThUTYx
- sXyrPNZ4P9FOpbKvrKLmfa0zl5/Lok0qF/zVyXP/0nSDh5RFXgvyK0+/oXye
- w4/IFvPiC2yQrS4aUEEI1BhFV5bkZ+T5klw9z3X1hFuL4oKQR1dxjbAAm5SF
- Oeke7qOBx/nCB2jmf5s0oN7aCYIuVrtY62IdNyjEzS7NvH0CZlFHg3CL0OKO
- RfE3/dAjtxgDAAA=
- """,
- """
- androidx/navigation/TestAbstractComp.class:
- H4sIAAAAAAAA/41RW2sTQRg9s5vrurFJvSXWS2trTPvQbYsgNEWoESGQpqAl
- IHmaJGOdZDMrM5PQx/4W/0HxoaAgwUd/lPjtNrYPvuTlO/Ndzvku8/vP958A
- XmKLYYOrgY7k4CxQfCpPuZWRCk6EsYc9YzXv20Y0/pIFYygO+ZQHIVenwXFv
- KPo2C5chcyCVtK8Z3Npmx0caGQ8pZBlS9rM0DNXWIg3qDLmDfjiX2l6EshEb
- riiVhc+wW2uNIksKwXA6DqSyQiseBm/FJz4JiaCIOenbSB9xPRK6fjXsbQ8F
- LDHkr8UYdhaa+KZ93UcJy3k4uMOw3or0aTAUtqe5VCbgSkU2UTBBO7LtSRjS
- rqV/sx4Jywfccoo546lLn8Jik48NGNiI4mcy9nboNdile87Ofc8pO55TnJ17
- Ts7JVcuz81V3z9lh+8x9k/71NeMUnbh6j8UaxTaf0vpWR2Eo9PbIMqy8nygr
- x6KpptLIXigOb6akj2tEA8Gw1JJKtCfjntAnnGoYlltRn4cdrmXsz4N+Uymh
- GyE3RhDZ+xBNdF+8k3GuMu/T+a9Lao3OlUp2rMTXI1wnL0N4j9AhTCfeBnlB
- fAnC9NYlchdJ+vm8GNhHlax/VYA8PMIcbl2Ty0huCf8HCh/ZJYrfcPciibh4
- QdajugIplmiQWqL9DJuEryh+nxQfdOE2UW6i0sRDrNATj5p4jCddMIOnWO0i
- ZeAZrBlkDEp/AVeTkqlcAwAA
- """,
- """
- androidx/navigation/TestClass.class:
- H4sIAAAAAAAA/31Ry0oDMRQ9N7VTHauO7/reqgtHxZ0iaEEoVAWVblylnaCx
- 0wQmaXHZb/EPXAkupLj0o8Q7o2s3h/O4CecmX9/vHwCOsEHYkCbJrE6eYyMH
- +kF6bU18p5yvp9K5CogQPcmBjFNpHuLr9pPq+ApKhOBEG+1PCaXtnVYVZQQh
- xlAhjPlH7QhbzX9vPibMNrvWp9rEl8rLRHrJnugNSlyNcpjIAQTqsv+sc7XP
- LDkgbI6GYShqIhQRs9GwNhoein06L3++BCIS+dQh5WejKzmoW+Mzm6Yq2+t6
- 7le3iSLMNLVRV/1eW2V3sp2yM9e0HZm2ZKZz/WeGt7afddSFzsXKTd943VMt
- 7TSnZ8ZYX+zlcADB6/8Vzl+DscYqLjRQ3n3D+CsTgRXGoDCXscpY/R3ABMIi
- XytwGevFHxEmOaveo9TAVAPTDcwgYorZBuYwfw9yWMAi5w6hw5JD8AOsqUxn
- 4AEAAA==
- """,
- """
- androidx/navigation/TestClassComp$Companion.class:
- H4sIAAAAAAAA/5VSy24TMRQ99qR5DAHSlkfCuxCkFminqdgVIUEQUqS0SKXK
- pgvkJKY4mbGR7URdZsWH8AddIbFAUZd8FOJ6UmCJurmPc++5987x/Pz1/QeA
- 53jM8FTooTVqeJJoMVXHwiujk0PpfDsVzrVN9rkZjNCEl8AYaiMxFUkq9HHy
- rj+SA19CxFB8obTyLxmi9Y1eFUsoxiigxFDwn5Rj2OxeYM8uQ2u9OzY+VToZ
- TbNEaS+tFmnyRn4Uk9S3jXbeTgbe2D1hx9LubvRi8LBvtTn4V/yQ5VWGrYtN
- Y1j+Q9iTXgyFF4TxbBqRbCyYSjBgYGPCT1TItikathia81kc8zqPeY2i+ax8
- 9iWqz2c7fJu9LpX52dcir/HQu8PChLX/ClPCbYbKX3XoCfbFlI721qSptFtj
- TzK3zVAyXO0qLfcnWV/aQ9FPCVnpmoFIe8KqkJ+D1Y7W0uYLJD1O/N5M7EC+
- VaHWOJhorzLZU05R8yutjc+PcmiRxIXw3eR5eGM6/z5lSRCC/NKTbyif5uUH
- ZIs5+AxrZKuLBlQQAzVG0aVz8iZ5fk6unuaiBsKNBbgg5NFlXKFahIeUxTnp
- Du6igUf5wnto5r80aUC9tSNEHSx3sNLBKq5RiOsdmnnzCMyhjgbVHWKHWw7F
- 3/peHcMPAwAA
- """,
- """
- androidx/navigation/TestClassComp.class:
- H4sIAAAAAAAA/4VRXWsTQRQ9s5vPdWOT+pVYP1obta0f2xRBsEXQiBBII2gp
- SJ8myVgn2czIzCT0sb/Ff1B8KChI8NEfJd7dxvbBh7zcM/fMuWfuvfP7z/ef
- AJ5hg2GFq77Rsn8UKT6Rh9xJraI9YV0z5tY29ehLHoyhPOATHsVcHUbvugPR
- c3n4DLkdqaR7yeCvre+HyCIXIIM8Q8Z9lpZhtT3XfZuhsNOLZz6P5urrSeCK
- +DxChsZae6gdlUeDySiSygmjeBy9EZ/4OHZNrawz457TZpeboTDbZ21eDlDC
- AkPx3IzhyfxeL97eDlHBYhEeriRTanMYDYTrGi6VjbhS2qXlNupo1xnHMU1Z
- +dfornC8zx0nzhtNfPoIloRiEsDAhsQfySTbpFO/wVCfHoeBV/UCrzw9DryC
- V50eL/tb3iZ7wfzX2V9fc17ZS7RbLHEod/iEJndGx7EwT4eOYen9WDk5Ei01
- kVZ2Y/Hqokf6rabuC4aFtlSiMx51hdnjpGFYbOsej/e5kUk+I8OWUsKkSxFU
- HHzQY9MTb2VyV5u9s//fK2jQsjLphLVkd4SrlOUIrxF6hNk0q1MWJXsgzG6c
- onCSXt+fiYHHeEAxPBOgiICwgEvnxVWkm0T4A6WP7BTlb7h6kjI+HlIMSFci
- xwo1spZ638M64XPir5PjjQP4LVRbqLVwE0t0xK0WbuPOAZjFXSwfIGMRWKxY
- 5CwqfwEoeIqnTgMAAA==
- """,
- """
- androidx/navigation/TestClassWithArg.class:
- H4sIAAAAAAAA/41QTW/TQBScXSdOYhLihK805ZsKtTngtOIGqgiRkCyFIpUq
- HHLaxJa7jbOWvJuox/wWzlyQQEgcUMSRH4V461ScOCDZ897sjuf5za/f338A
- eI49hj2hojyT0WWgxEomwshMBWexNsNUaP1BmvNBnlTAGPwLsRJBKlQSvJte
- xDNTgcPgvpRKmmOG0n54MGZw9g/GdZRR8VBClbjIEwYW1uHhWg0cdZKac6kZ
- no7+Z/YLmpHEZmBtyDxkaI3mmUmlCt7GRkTCCJLwxcqhlZiFmgXQ0DmdX0rL
- +tRFhwzDzbrt8Q73uL9Ze/Rwv+rxqtPZrI94n71utF2fd3nf+fnR5X7ptPWX
- VUndLVXLvmutjpgd4J+I1TBTJs/SNM6fzQ2tNsyimKE5kio+WS6mcX4mpimd
- tEfZTKRjkUvLrw6999kyn8VvpCU7p0tl5CIeSy3pdqBUZopINA4pt1KxU9vG
- SB2nvgyX8AGxY+Kcqtf7hlpv9ysanwvNQ0KrAXbwiPD2VoXraNqIqLNulCh8
- erdegU2Oarn3BY1P/7SpbwVXNhyPC7yPJ1RfFT9Zxo0JnBA3Q9wKaewdatEJ
- 6fvuBExjF3cnqGg0Ne5peAW6Gr5G6w+psmgInQIAAA==
- """,
- """
- androidx/navigation/TestClassWithArgComp$Companion.class:
- H4sIAAAAAAAA/5VSS29SQRT+Zi6FckWlrQ/wVR+YUBO5hXRXY1IxJiS0Jtrg
- ogszwEgH7p0xcwfSJSt/iP+gKxMXhnTpjzKeuaBu7ea8vvOdM/PN/Pz1/QeA
- PTxlaAk9tEYNzyItZmoknDI6Opapa8ciTT8od3pgR22TfK55IzTBBTCG8ljM
- RBQLPYre9sdy4AoIGPIvlFbuJUNQ3+mVsIZ8iBwKDDl3qlKGve7l1+0zNOvd
- iXGx0tF4lkRKO2m1iKPX8pOYxq5tdOrsdOCMPRR2Iu3+Ti8E92u3aoN/4Mck
- Qxkal5vGsPGHcCidGAonqMaTWUAiMm+K3oCBTah+pny2S9GwyVBbzMOQV3jI
- yxQt5usXX4LKYt7iu+xVYZ1ffM3zMve9LeYn1P9XnwLuMhT/ikQPciRmdHZn
- TRxL25g4Er1thpLheldpeTRN+tIei35Mlc2uGYi4J6zy+apY6mgtbbZH0lOF
- 783UDuQb5bHqu6l2KpE9lSpqPtDauOxsKZqkdM5fnzz3L0632KYs8nqQX3v2
- DevnGfyQbD4rNvCIbGnZgCJCoMwourIiPyfPV+TSeaatJ9xaFpeELLqKa4QF
- eExZmJHu4T6qeJItfIBa9s9JA+otnyDoYKODzQ62cINC3OzQzNsnYCkqqBKe
- IkxxJ0X+N9Gij4okAwAA
- """,
- """
- androidx/navigation/TestClassWithArgComp.class:
- H4sIAAAAAAAA/41S308TQRD+9vrrehY5KmIBf6CglqpcITwJIcEa4yWlJkhq
- DE/bdi3bXvfM3rbhkb/FZ1+IGhJNDPHRP8o4d634oA8kdzM7szPfzHyzP399
- /Q5gE+sMZa46OpSdY0/xkexyI0PlHYjI1AIeRW+kOdrV3Vo4eJ8DY3B7fMS9
- gKuu96rVE22TQ4ohuy2VNDsM6bK/2mRIlVebBWSQc5CGTTbXXQbmF+DgSh4W
- ChRqjmTEUKlftv4W1ekKsxtDUQGfwd5uB5PCG5dFWYkFV3SdwzWG9XK9HxpC
- 8XqjgSeVEVrxwHsu3vFhYGqhiowetk2o97juC701nuu6g1nMMeQvwBg2Lz3I
- 3xa2CihhPiZkgWG5Huqu1xOmpblUkceVCk2CEnmN0DSGQUAUzPzpd08Y3uGG
- k88ajFK0ThaLfCxAZPfJfyxjq0qnDm365flJ0bFKlmO55ycOfZZrO5adLp2f
- LOU2rCp7ynLPpopZ11qwqqkfH7KWm96fubBsSllI2xk3G+NtsLiK2+AjIsno
- MAiEXusbhsX9oTJyIHw1kpFsBWL37xy09VrYEQzTdalEYzhoCX3AKYahWA/b
- PGhyLWN74iz4Sgmd8Cco2XkdDnVbvJDx3fykTvOfKlgnQtM0uIX5mF/qs0JW
- lvRN0sX4EZJOkZ1JvI/I2qFoi7RTOUO+svgFU6cJwuNJJrCGJyTnxlG4iumY
- aDrFaLQXuPSPsbyYf9KZymdMffwvTGEcMIGxqancJLmEZIMofMPsW3aGG5+w
- eJp4UpQbF2T0+KxkMC/BXkWVdI38twjx9iFSPu74WPJxF/foiGUfK7h/CBbh
- AR4ewo4wHaEcwUlkNoIbYSZC6Tedt709GAQAAA==
- """,
- """
- androidx/navigation/TestGraph.class:
- H4sIAAAAAAAA/31SQWsTQRT+ZpJsNttoY6s2sdaq7UE9uG3xZhFqUFmIK9gQ
- kJ4m2SGdZDMju5PQY07+EP9B8VBQKKHe/FHimzXoQXAW3nvfN998zHuzP35+
- vQTwDLsMW0InmVHJWajFTA2FVUaHXZnbN5n4eFoFY2iMxEyEqdDD8F1/JAe2
- ihKDd6i0si8YSo8e9+qowAtQRpWhbE9VzrDd+a/zcwb/cJAWHgG4O+hH8XH3
- KG6/quMaghqR1xl2OiYbhiNp+5lQOg+F1sYWXnkYGxtP05SsbnTGxpJZ+FZa
- kQgriOOTWYm6ZC7UXAADGxN/phzaoyrZZ9hdzIOAN3nAG1Qt5v73T7y5mB/w
- Pfay6vOrzx5vcKc9YM6hEYtZ22ibmTSV2dOxZdh8P9VWTWSkZypX/VQe/b0j
- jaNtEsmw2lFaxtNJX2ZdQRqGtY4ZiLQnMuXwkgyOzTQbyNfKgdbSuPePLfZp
- OuWipZYbFuV7hDx3QcqcvkqBtgmFrnHKlScX8M+L7ftLMbCOBxTrvwWokRXg
- Y+XP4Q1Su7XyDfzDBepfsHpeEBwPi7iFneJfokcgg7UTlCKsR7gZ4RZuU4mN
- CE20TsBy3MEm7ecIctzN4f0CEKx6togCAAA=
- """,
- """
- androidx/navigation/TestInterface.class:
- H4sIAAAAAAAA/4WOz0rDQBDGv9lo08Z/qVqoR/Fu2tKbJykIgaqg4iWnbbIt
- 22x3IbsNPfa5PEjPPpR0Ux/AGfjmmxn4zfz8fn0DGKNHuOW6qIwsNonmtVxw
- J41OPoR1qXaimvNchCBCvOQ1TxTXi+R1thS5CxEQutPSOCV18iwcL7jjDwS2
- qgMPp0Y6jYBApZ9vZNMNvCuGhN5u245Yn0Us9m7e321HbEDNckS4m/77lb/k
- wfELrydGu8ooJar70hGid7OucvEklSDcvK21kyvxKa2cKfGotXEHmG35UzjC
- XzBcHfQS174OPfjYZytDkCJM0U7RQeQtTlKc4iwDWZzjIgOziC26e5qGvyhR
- AQAA
- """,
- """
- androidx/navigation/TestObject.class:
- H4sIAAAAAAAA/32Sz2sTQRTHvzNJNptttLH+aGK1VtuDenDb4s0i1KCwEFew
- IVB6mmSHOMlmBnYnS485+Yf4HxQPBQUJevOPEt+sUQ+Cu/De+37nzYeZt/v9
- x6cvAJ5ij2Fb6CQzKjkPtSjUWFhldNiXuX0znMiRrYMxtCaiEGEq9Dj87VYY
- vCOllX3OUHn4aNBEDV6AKuoMVftO5Qw7vf+jnzH4R6O0hATgbqcfxSf947j7
- sokrCBpkXmXY7ZlsHE6kHWZC6TwUWhtbwvIwNjaepymhrvWmxhIsfC2tSIQV
- 5PFZUaF7MhcaLoCBTck/V07tU5UcMOwtF0HA2zzgLaqWC//be95eLg75PntR
- 9/nXDx5vcdd7yByhFYuia7TNTJrK7MnUMmy9nWurZjLShcrVMJXHf89I8+ia
- RDKs95SW8Xw2lFlfUA/DRs+MRDoQmXJ6ZQYnZp6N5CvlRGcFHvyDxQFNp1pe
- qeOGRXmblOcOSJnTWyvVPVKhuzjl2uNL+Bfl8s6qGbiJ+xSbvxrQIBTgY+3P
- 5k3qds/aZ/DTSzQ/Yv2iNDgelPEudsu/iT4CATbOUIlwPcKNiNC3qMRmhDY6
- Z2A5bmOL1nMEOe7k8H4CjO1ti4oCAAA=
- """
- )
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongStartDestinationTypeDetectorTest.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongStartDestinationTypeDetectorTest.kt
index eaaf466..7b31a26 100644
--- a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongStartDestinationTypeDetectorTest.kt
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongStartDestinationTypeDetectorTest.kt
@@ -17,22 +17,15 @@
package androidx.navigation.runtime.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.compiled
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
-import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestMode
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import org.junit.runners.JUnit4
-@RunWith(Parameterized::class)
-class WrongStartDestinationTypeDetectorTest(private val testFile: TestFile) : LintDetectorTest() {
-
- private companion object {
- @JvmStatic @Parameterized.Parameters public fun data() = listOf(SOURCECODE, BYTECODE)
- }
+@RunWith(JUnit4::class)
+class WrongStartDestinationTypeDetectorTest : LintDetectorTest() {
@Test
fun testEmptyConstructorNoError() {
@@ -42,7 +35,10 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.navigation.TestNavHost
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -56,7 +52,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.skipTestModes(TestMode.FULLY_QUALIFIED)
.run()
@@ -71,7 +68,9 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -93,7 +92,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.run()
.expectClean()
@@ -107,7 +107,9 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -124,7 +126,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.run()
.expectClean()
@@ -138,7 +141,9 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.NavController
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navController = NavController()
@@ -160,84 +165,85 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.run()
.expect(
"""
-src/com/example/test.kt:7: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:9: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestClass(...)?
If the class TestClass does not contain arguments,
you can also pass in its KClass reference TestClass::class [WrongStartDestinationType]
navController.createGraph(startDestination = TestClass) {}
~~~~~~~~~
-src/com/example/test.kt:8: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:10: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestClassWithArg(...)?
If the class TestClassWithArg does not contain arguments,
you can also pass in its KClass reference TestClassWithArg::class [WrongStartDestinationType]
navController.createGraph(startDestination = TestClassWithArg) {}
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:9: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:11: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor InnerClass(...)?
If the class InnerClass does not contain arguments,
you can also pass in its KClass reference InnerClass::class [WrongStartDestinationType]
navController.createGraph(startDestination = Outer.InnerClass) {}
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:10: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor InterfaceChildClass(...)?
If the class InterfaceChildClass does not contain arguments,
you can also pass in its KClass reference InterfaceChildClass::class [WrongStartDestinationType]
navController.createGraph(startDestination = InterfaceChildClass) {}
~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:11: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor AbstractChildClass(...)?
If the class AbstractChildClass does not contain arguments,
you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
navController.createGraph(startDestination = AbstractChildClass) {}
~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:14: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestInterface(...)?
If the class TestInterface does not contain arguments,
you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
navController.createGraph(startDestination = TestInterface)
~~~~~~~~~~~~~
-src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:15: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestAbstract(...)?
If the class TestAbstract does not contain arguments,
you can also pass in its KClass reference TestAbstract::class [WrongStartDestinationType]
navController.createGraph(startDestination = TestAbstract)
~~~~~~~~~~~~
-src/com/example/test.kt:15: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:17: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navController.createGraph(startDestination = TestClassComp)
~~~~~~~~~~~~~
-src/com/example/test.kt:16: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:18: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navController.createGraph(startDestination = TestClassWithArgComp)
~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:17: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:19: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navController.createGraph(startDestination = OuterComp.InnerClassComp)
~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:18: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:20: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navController.createGraph(startDestination = InterfaceChildClassComp)
~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:19: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:21: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navController.createGraph(startDestination = AbstractChildClassComp)
~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:20: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:22: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
@@ -256,7 +262,9 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.TestNavHost
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navHost = TestNavHost()
@@ -278,7 +286,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.run()
.expectClean()
@@ -292,7 +301,9 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.TestNavHost
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navHost = TestNavHost()
@@ -310,7 +321,8 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.run()
.expectClean()
@@ -324,7 +336,9 @@
"""
package com.example
- import androidx.navigation.*
+ import androidx.navigation.TestNavHost
+ import androidx.navigation.createGraph
+ import androidx.test.*
fun createGraph() {
val navHost = TestNavHost()
@@ -345,84 +359,85 @@
"""
)
.indented(),
- testFile,
+ *NAVIGATION_STUBS,
+ TEST_CODE
)
.run()
.expect(
"""
-src/com/example/test.kt:7: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:9: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestClass(...)?
If the class TestClass does not contain arguments,
you can also pass in its KClass reference TestClass::class [WrongStartDestinationType]
navHost.createGraph(startDestination = TestClass) {}
~~~~~~~~~
-src/com/example/test.kt:8: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:10: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestClassWithArg(...)?
If the class TestClassWithArg does not contain arguments,
you can also pass in its KClass reference TestClassWithArg::class [WrongStartDestinationType]
navHost.createGraph(startDestination = TestClassWithArg) {}
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:9: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:11: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor InnerClass(...)?
If the class InnerClass does not contain arguments,
you can also pass in its KClass reference InnerClass::class [WrongStartDestinationType]
navHost.createGraph(startDestination = Outer.InnerClass) {}
~~~~~~~~~~~~~~~~
-src/com/example/test.kt:10: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor InterfaceChildClass(...)?
If the class InterfaceChildClass does not contain arguments,
you can also pass in its KClass reference InterfaceChildClass::class [WrongStartDestinationType]
navHost.createGraph(startDestination = InterfaceChildClass) {}
~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:11: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor AbstractChildClass(...)?
If the class AbstractChildClass does not contain arguments,
you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
navHost.createGraph(startDestination = AbstractChildClass) {}
~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:14: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestInterface(...)?
If the class TestInterface does not contain arguments,
you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
navHost.createGraph(startDestination = TestInterface)
~~~~~~~~~~~~~
-src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:15: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor TestAbstract(...)?
If the class TestAbstract does not contain arguments,
you can also pass in its KClass reference TestAbstract::class [WrongStartDestinationType]
navHost.createGraph(startDestination = TestAbstract)
~~~~~~~~~~~~
-src/com/example/test.kt:14: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:16: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navHost.createGraph(startDestination = TestClassComp) {}
~~~~~~~~~~~~~
-src/com/example/test.kt:15: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:17: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navHost.createGraph(startDestination = TestClassWithArgComp) {}
~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:16: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:18: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navHost.createGraph(startDestination = OuterComp.InnerClassComp) {}
~~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:17: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:19: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navHost.createGraph(startDestination = InterfaceChildClassComp) {}
~~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:18: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:20: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
navHost.createGraph(startDestination = AbstractChildClassComp) {}
~~~~~~~~~~~~~~~~~~~~~~
-src/com/example/test.kt:19: Error: StartDestination should not be a simple class name reference.
+src/com/example/test.kt:21: Error: StartDestination should not be a simple class name reference.
Did you mean to call its constructor Companion(...)?
If the class Companion does not contain arguments,
you can also pass in its KClass reference Companion::class [WrongStartDestinationType]
@@ -438,600 +453,3 @@
override fun getIssues(): MutableList<Issue> =
mutableListOf(WrongStartDestinationTypeDetector.WrongStartDestinationType)
}
-
-private val SOURCECODE =
- kotlin(
- """
-package androidx.navigation
-
-import kotlin.reflect.KClass
-import kotlin.reflect.KType
-
-public open class NavDestination
-
-// NavGraph
-public open class NavGraph: NavDestination() {
- public fun <T : Any> setStartDestination(startDestRoute: T) {}
-}
-
-// NavController
-public open class NavController
-
-public inline fun NavController.createGraph(
- startDestination: Any,
- route: KClass<*>? = null,
-): NavGraph { return NavGraph() }
-
-// NavHost
-public interface NavHost
-public class TestNavHost: NavHost
-
-public inline fun NavHost.createGraph(
- startDestination: Any,
- route: KClass<*>? = null,
-): NavGraph { return NavGraph() }
-""" +
- TEST_CLASS
- )
- .indented()
-
-// Stub
-private val BYTECODE =
- compiled(
- "libs/StartDestinationLint.jar",
- SOURCECODE,
- 0x8e62b385,
- """
- META-INF/main.kotlin_module:
- H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgUucSTsxLKcrPTKnQy0ssy0xPLMnM
- zxMS8Essc0ktLsnMA/O9S7hEubiT83P1UisScwtyUoXYQoCy3iVKDFoMAGXO
- +shYAAAA
- """,
- """
- androidx/navigation/AbstractChildClass.class:
- H4sIAAAAAAAA/41QTW8SURQ9780wwBRkwC9K/ajVNpSF0MaN0TRSjAkJdtE2
- LGD1YCb0hWEmmfcgXfJbXLsx0Zi4MMSlP8p431CNCxYu3rn33Hdy7sfPX9++
- A3iBfYYDEflJLP3rZiQWciK0jKNme6R0Isa6cyVDvxMKpbJgDLubtJeB0n/0
- WVgMzmsZSX3CYNcHh30Gq37YLyCDrAsbOeIimTCwQQEutvLgKJBUX0nFUO/9
- 3zSvqMsk0G1jRPYDhnJvGutQRs33gRa+0IIkfLawaE1mIG8A1HZK9WtpWIsy
- /4jhdLWsuLzK07dautzbcnnOqq6Wx7zFTosVx+M13rJ+fHC4Z5+X/7IcqWt2
- LuM5xumYYW/j+P8eiKaiIcpnYvGWqjJKFc+nmi7Qif2AodSTUXA2n42C5FKM
- QqpUevFYhH2RSMNviu5FPE/GwTtpyPb5PNJyFvSlkvTbjqJYp8YKR3ReO128
- Yq5NGac8A4dwl9gJcU7RbXxFvrHzBcVPqeYJodEAL7FHeG+twi2UzB0pM260
- CTx6a6+mOS/FTOMzih832hTWghsbjqcpPsYzim/SITO4PYTVxZ0u7nap7X1K
- Ue1iG7UhmMIOHgyRVSgpPFRwFR4pOAqeQvk3MHA2w9YCAAA=
- """,
- """
- androidx/navigation/AbstractChildClassComp$Companion.class:
- H4sIAAAAAAAA/51Sy24TMRQ99qR5DAHSlkfC+xGkthKdpqrYFCGVVEiR0iIB
- yqYL5MyY1smMB42dqMuu+BD+oCskFijqko9CXDsBtsDm+t5z7rnXczzff3z9
- BmAHTxh2hE6KXCWnkRZTdSysynW0NzS2ELHtnqg06abCmG6efWy7IDQ1VMAY
- GiMxFVEq9HH0ejiSsa0gYCg/V1rZFwzB2vqgjiWUQ5RQYSjZE2UYnvX/Z+Eu
- Q2etP85tqnQ0mmaR0lYWWqTRvvwgJqnt5pomTGKbFweiGMtid30QgrvFq+34
- D/k+8yzD5r9NY1j+JTiQViTCCsJ4Ng3ISOZCzQUwsDHhp8pVW5QlHYb27CwM
- eZOHvEHZ7Kx68Slozs62+RZ7Wanyi89l3uCud5u5CRt/71AFtxlqv22iWx6K
- 6b40Vmkv2xxbcr6bJ5Lhal9peTjJhrJ4J4YpISv9PBbpQBTK1Quw3tNaFn6D
- pPcK3+aTIpavlONabybaqkwOlFHUvKd1bv0egw6ZXXIO0Mnds9OH3KcqcpbQ
- ubTxBdVzTz+gWPZgHw8p1ucNqCEEGoyySwvxUzr5Qlw/9/Y6wY05OBf47DKu
- EBfgEVWhF93BXbTw2C+8h7b/3ckD6m0cIehhuYeVHlZxjVJc79HMm0dgBk20
- iDcIDW4ZlH8CofOnlCsDAAA=
- """,
- """
- androidx/navigation/AbstractChildClassComp.class:
- H4sIAAAAAAAA/5VSzU8TQRT/zW4/lyJtRSzgBwpiqcgWQjwIIcESTZPSA5Im
- wmnarmXodtbsTBuO/C2evRA1JJoY4tE/yvhmWyFRDnqYefPe/N7vff74+eUb
- gHWsMZS4bIeBaJ+4kg9Eh2sRSHe7qXTIW7pyJPx2xedKVYLeuyQYw8J1+H1P
- 6UufCGkzJDaFFHqLIVY8WGow2MWlRgZxJB3EkCKdhx0GdpCBg7E0LGQIqo+E
- Yliu/XtWGxSp4+ltQ0YhDhhSmy1/FHr933kWzMUlAZK4ybBarHUDTTzu8aDn
- Cqm9UHLf3fHe8r5PRUri6Ld0EO7ysOuFG8PabjmYxBRD+pKM4dl/FHOVxEYG
- BUybtswwzNeCsOMee7oZciGVy6UMdMSj3Hqg633fpzbkfme862ne5pqTzeoN
- bBo1M1faXKCWd8l+IoxWpld7leHVxWnesQpWdC5OHSs75lipWOHidC65ZpXZ
- c5Z8MZ5PZK0Zq2x/f5+wsrG93KWWIpeZWCqeTRg6WqrFa0v+c0soPcomV+eD
- HfoRMkKtdDXD7F5fatHzqnIglGj63vZVwbQklaDtMUzUhPTq/V7TC/c5YRjy
- taDF/QYPhdFHxkxVSi+MOuyRs/M66Ict76Uwf9OjOI2/omCVOh+jDlmYNoOg
- RJ+QliB5h2Te7CxJm/R4ZF0mbYvQFkmndI50afYzxs8ihqcjT6CGFbqnhijc
- wISZCL0MG7UCWTpDLtcMimS89AnjH66lyQwBI5oUJZUcORcQjRqZr5h8w85x
- +yNmzyKLTcQmIKM9taLCyhF3iQoGKmS/S4z3DmFXcb+KuSoe4CE9MV/FAh4d
- giks4vEhUgoTCkUFR2FJIaGQVcgpFH4BxvFHnF0EAAA=
- """,
- """
- androidx/navigation/AbstractChildObject.class:
- H4sIAAAAAAAA/41Sy2oUQRQ9VfPq6Yzm4SMzxkdMhBgXdhJcaRDGUaGhbcEM
- A5JV9YOkMj3V0F0zZDkrP8Q/CC4CCjLozo8Sb5XjA8zCbvreuueeOtX3UN++
- f/wM4BHuMWwJlRS5TE49JSbySGiZK68blboQse4dyyx5HZ2ksW6AMaxfRO6n
- pf61oYEKQ31fKqmfMlTubw9aqKHuoooGQ1Ufy5JhO/jPM58wOPtxZtVccCPh
- +OFBvxv2XrRwCW6TwMsMm0FeHHknqY4KIVXpCaVybVVLL8x1OM4ykloOhrkm
- Me9VqkUitCCMjyYVcoKZ0DQBDGxI+Kk01Q6tkl06YDZ1Xd7m9ptNna/veHs2
- 3eM77FnD4V/e1/kSN9Q9ho0Lh/vbI/MroZg8J0gq23441Axrb8ZKy1Hqq4ks
- ZZSl3T9TkHW9PEkZFgOp0nA8itKiL4jDsBLkscgGopCmnoPuQT4u4vSlNEVn
- Ljz4Rxa75F/VDt0xdlK+TVWd8hJlTm/NVneo8ow1lGsPzuGc2fb6nAw8xl2K
- rZ8ENEkKcLDwe/Mqsc2z8An87TlaH7B4ZgGODRtvYdPeSPKGBFYOUfFxxcdV
- H9dwnZZY9dFG5xCsxA2sUb+EW+JmifoP1gZuws4CAAA=
- """,
- """
- androidx/navigation/AbstractChildObjectComp.class:
- H4sIAAAAAAAA/5VSW2sTQRg9M0k2m220tV6aWO8t4gXdtvhmEWJUWNiu0IaA
- 9Gk2u7TTbGZldxL6mCd/iP+g+FBQkKBv/ijxmzFU0L64y36XM+c7s3OYHz8/
- fwXwDOsMj4VKilwmx74SE3kgtMyV34lLXYiB7h7KLHkbH6VU5qP3dTCG9fMG
- emmpz4Yss8LgbEsl9QuGyoOH/SZqcDxUUWeo6kNZMjwJ/2Pv5wzu9iCzih64
- kXGDaK/Xibqvm7gAr0HgRYa1MC8O/KNUx4WQqvSFUrm2yqUf5ToaZxlJXQqH
- uSYxfyfVIhFaEMZHkwq5wkxomAAGNiT8WJpug6pkkzaYTT2Pt7j9ZlP3+wfe
- mk23+AZ7WXf5t48OX+KGusVw/9wD/u2V+Z1ITF4RLJWlPB1qhtXdsdJylAZq
- IksZZ2nnz0nIwm6epAyLoVRpNB7FadETxGFYDvOByPqikKafg95ePi4G6Rtp
- mvZcuP+PLDbJw6o9eNtYSvkWdQ7lJcqc3prtblPnG3so1x6dwj2xy3fmZGAH
- dyk2fxPQICnAxcLZ8AqxzbPwBfzdKZqfsHhiAY57Nt7Emr2h5A0JLO+jEuBy
- gCsBruIalVgJ0EJ7H6zEdazSegmvxI0Szi+zsivd3gIAAA==
- """,
- """
- androidx/navigation/InterfaceChildClass.class:
- H4sIAAAAAAAA/41Qz28SQRT+ZhZY2FJZqFZK/VWrtnBwaaMnTWNbY0KCbVIb
- DnAaYKVTltlkZyA98rd49mKiMfFgiEf/KOMbSpqYcPAw773vvW++9+P3nx8/
- AbzADsOOUP0klv2rQImJHAgjYxU0lAmTj6IXHl/IqH8cCa1dMAb/UkxEEAk1
- CE67l2HPuHAYtpZJnIfa3Mi4SDNkXkslzQFDarddbTE4u9VWHi5yHlLwCItk
- wMDaeeSxmgPHLaKaC6kZqs3/nPIVtRmE5tAqkX6bodgcxiaSKngfGtEXRhCF
- jyYO7c+syVkD6juk/JW0qE5Rf4/haDYtebzM52829bi/4vGsU55N93mdHa2W
- Mj6v8Lrz61OG+6mz4g3KEruSyqb9jFXaZ9heOv8/J6KxaIriiZi8pbRUc8rz
- oaEbHMf9kKHQlCo8GY+6YXIuuhFlSs24J6KWSKTFi6T3IR4nvfCdtGDjbKyM
- HIUtqSVVD5WKzVxYY48OnKKOGXole3FanFPsIkv2MaEDwpy8V/uOldrmNxS+
- zDnbZO0v4CWekF2/ZsFH0Z6SIqtGu5Du2kIrsBcmn659ReHzUpn8NWEhw/F0
- brfwjPwbqt2m2p0OnAbWG7jbQBkbFKLSwCbudcA07uNBB65GUeOhRl7jkUZW
- o6Sx9hcxw6Dy8gIAAA==
- """,
- """
- androidx/navigation/InterfaceChildClassComp$Companion.class:
- H4sIAAAAAAAA/51STW/TQBB9u07zYQJNWz4SvgtBakHUTQXiUIQEqZAspUUC
- lEsPaGNv203sNfJuoh5z4ofwD3pC4oCiHvlRiFknwLlcZmfemzezfuufv77/
- APAMjxieCx3nmYpPAy0m6lhYlekg1FbmRyKS3ROVxN1EGNPN0s9tF4SmjgoY
- Q2MoJiJIhD4O3g2GMrIVeAzll0or+4rB29js17GEso8SKgwle6IMw4vef23c
- Zehs9EaZTZQOhpM0UE6hRRLsySMxTmw308bm48hm+b7IRzLf3ez74G7zWjv6
- R35KC5Zh62LTGFb+CPalFbGwgjCeTjyykrlQcwEMbET4qXLVNmVxh6E9m/o+
- b3KfNyibTavnX7zmbLrDt9mbSpWffy3zBne9O8xNeHIBiyq4xVD76xNd80BM
- 9qSxShe6rZEl77tZLBmWe0rLg3E6kPlHMUgIWe1lkUj6IleuXoD1UGuZFxsk
- vZj/IRvnkXyrHNd6P9ZWpbKvjKLm11pntthj0CG3S84COrl7ePqSe1QFzhM6
- lx5/Q/WsoO9TLBdgiHWK9XkDavCBBqPs0kL8lE6+ENfPCn+d4PocnAuK7DKu
- EOfhAVV+IbqNO2jhYbHwLtrFH08eUG/jEF6IlRCrIdZwlVJcC2nmjUMwgyZa
- xBv4BjcNyr8BSdfj0S4DAAA=
- """,
- """
- androidx/navigation/InterfaceChildClassComp.class:
- H4sIAAAAAAAA/5VSW08TURD+zrb0RpG2KJbiBQS1FGELYkyEkGCNZpNSEyRN
- hKfT9lBOuz1rdk8bHvktPvtC1JBoYoiP/ijjnKXCgySGh52Zb3bmm8uZX7+/
- /QCwhjWGRa5avidbR7biA9nmWnrKdpQW/gFvisqhdFsVlwdBxet9iIMxZDp8
- wG2Xq7b9ttERTR1HhGH2KppdEegLqjhGGGIbUkm9yRAt7i3UGSLFhXoacSRT
- iCJFmPttBraXRhpjSVi4QaH6UAYMS9VrdLpOpdpCbxk2qrHHkNhousPaz65B
- NG8EVxQRxy2GlWK162kisjuDni1NjuKu/Uoc8L6rK54KtN9vas/f5n5X+Ovn
- 091OYRJ5huQFGcPz64xz2cV6GgVMm83cYZiren7b7gjd8LlUgc2V8nRIFNg1
- T9f6rkuLyP5teVto3uKak8/qDSJ0AcyIpBGgrXfJfyQNKpPVWmF4c3acS1l5
- K/zOjlNWZjRlJaL5s+OZ+KpVZi9Y/OVYLpaxClY58vNjzMpEd7IXKEEphWhi
- JBMzdKum3/9eCfVGrWRrfPCK3FKFIctdzTC901da9oSjBjKQDVdsXU5LR1Lx
- WoJhvCqVqPV7DeHvcophyFW9Jnfr3JcGD51pRynhh+sVlJx65/X9pngtzb+p
- YZ36P1WwQmuPUnsx0lPmHcheonXFSN8jnTNXSzpCOI4EyWVCmxRtkU6VTjFa
- mv6K8RNCFuxhJuCgTHLyPAoZZM2DkGXYaBnEOzHkss07kR4pfcH4pytp0ucB
- Q5oEbiI5TM4jfGmkv2PyPTvF1GfcPQk9ERrNFGRhEwUabjXkfoKnpCvkv0+M
- M/uIOJh18MDBHObJxEMHj/B4HyxAEQv7SATIBigFSAdYDAzMBZgIUPgDTG80
- x3MEAAA=
- """,
- """
- androidx/navigation/InterfaceChildObject.class:
- H4sIAAAAAAAA/41SS2/TQBD+dpMmjmtoWl4J5VXaotIDbivEhQqpBJAsBSPR
- KBLqaRMv6SbOWrI3Vo858UP4BxWHSiChCG78KMSsCeUAEtjaeXwz83lm1t++
- f/wM4CHuMWwJHaWJik58LXI1EEYl2g+0kelb0ZetYxVHr3pD2TdVMIb6UOTC
- j4Ue+L/QEsPa3zg6MjPnPFUsMFT2lVbmCUNp637XQxWOizJqDGVzrDKG7fb/
- 9vKYwdnvxwWdC245nCA87ByEreceluDVCKwzrLeTdOAPpemlQunMF1onpqDN
- /DAx4SSOiWq5PUoMkfkvpRGRMIIwPs5LtCJmRc0KMLAR4SfKejtkRbv0gdnU
- dXmDF2c2db6+443ZdI/vsKdVh395X+F1blP3bC//3JLtJRT5M8KULuIPRoZh
- 9fVEGzWWgc5VpnqxPPg9Bi2vlUSSYamttAwn455MO4JyGFbaSV/EXZEq689B
- 9zCZpH35QlmnOSfu/kGLXVpgmWau0GnajZK+Q4Nbf4U0p5cukLw18ny7HdIL
- 22dwT4vw3Xky8AjrJL2fCVgkC1R44bz4GmXbZ/ET+JszXPyA5dMC4Ngo5G1s
- Fn8rwyUiuHyEUoArAa4GVNogE80A17F6BJbhBm5SPIOX4VYG5wfApo4N6gIA
- AA==
- """,
- """
- androidx/navigation/NavController.class:
- H4sIAAAAAAAA/4VRu0oDQRQ9d2I2ukZNfMYXGGzUwlWxUwSNCIGooJLGapId
- 4pjNDOxOgmW+xT+wEiwkWPpR4t01vc3hPGbuHO58/3x8AjjGJqEqTRhbHb4E
- Rg50RzptTXAjBzVrXGyjSMUFEKH0LAcyiKTpBLetZ9V2BeQI3qk22p0Rcju7
- zSLy8HxMoECYcE86IWw3/p1+Qig3utZF2gTXyslQOsme6A1yXJFSmEoBBOqy
- /6JTdcAsPCRsjYa+LyrCFyVmo+HkcmU0PBIHdJH/evVESaTnjii9XeZnL1Xi
- tMla7Hcd16zZUBHmGtqom36vpeIH2YrYmW/YtoyaMtapHpv+ve3HbXWlU7F6
- 1zdO91RTJ5rTc2OsywYnqELwFsad06UwVlgFmQbye++YfGMisMroZeYs1hiL
- fwcwBT/L1zNcwUb2XYRpzoqPyNUxU8dsHXMoMUW5jnksPIISLGKJ8wR+guUE
- 3i+86bUs6wEAAA==
- """,
- """
- androidx/navigation/NavDestination.class:
- H4sIAAAAAAAA/4VRO08CQRD+ZoEDTpSHiuAjUWOhFh4SO42Jj5iQICZqaKwW
- 7oLLYy/hFkLJb/EfWJlYGGLpjzLOnTRWNl++x+zMZPbr+/0DwAm2CLtSu0Nf
- uRNHy7HqSKN87TTk+NoLjNKRTIIIua4cS6cvdce5a3W9tkkiRrDOlFbmnBDb
- P2hmkIBlI44kIW6eVUDYq//f/pSQr/d801faufWMdKWR7InBOMZLUgjpEECg
- HvsTFaoKM/eYsD2b2rYoCVvkmM2mqWJpNq2KCl0mPl8skRNhXZXC1/m/c496
- hve88l2PkK0r7TVGg5Y3fJStPjuFut+W/aYcqlDPTfvBHw3b3o0KRfl+pI0a
- eE0VKE4vtPZN1DjADgSfYb5zeBXGEisn0kDi8A2pVyYCZUYrMi2sM2Z+C5CG
- HeUbEa5hM/owwgJnmSfEalisYamGLHJMka+hgOUnUIAVrHIewA5QDGD9AKYj
- 0APtAQAA
- """,
- """
- androidx/navigation/NavDestinationKt.class:
- H4sIAAAAAAAA/61WbVPbRhB+zsbYCGOECW8GDAGHGGgQEJI0hZJSaILKS1Kg
- pJS26WGEEQgpo5OZdDrT5lP/Q7/2FyTlQzplppPJt/ZHdbqnCgw2BibDB5/2
- 9naffXZv787//PvnXwDGsMGQ4fam65ibLzSb75t57pmOrS3y/RlDeKbtT+e8
- KBiDusP3uWZxO6893tgxcqQNM9TmXIN7xiOXP99msLLzFfCmHdtzHcsy3PH5
- UqDx+V3Hs0xbc40ti+ba3LTFhRjvrwTmRxtncK4y3MTA5MURe+cdN6/tGN6G
- y01baNy2Hc+3Etqi4y0WLIusMudZkQnfsAwyq57wtk0xGYPCkA447ezvaabt
- Ga7NLU2nJMjfzIko4gxNuW0jtxuEecJdvmeQIcPN7Bk5FjXLEiQ/3r8aRwL1
- Cuqg0m4Kj7veiV2OIcnQcV76UVyTnE3b9CYZwlkJ2IwWBU1oJcCMmdnKnOoG
- pjM0ZGSOp/W9l9g1hmR5UgwR1yl4BkNLhZZhaDwRKrNpbPGC5TH8fKWNqZdb
- Xtg5HWWFeFYYHTsmuFWR4KwjvCs8M+bVBLrUaek8NxTDD1eU9PvsR2Pe8Hxv
- 3aajYOeMJWOL4Xr2bMcVOihHHabmyty6LnCKYwCDNQjhA4ZUaeCnprc95eZ9
- oOxF8QNjotGSqwTSdzmIOIYxIkmNMrQSKd22Dbe8JJUoPaaD6GaKTkSpyTwb
- 4mxCZQBx3MU9SehDhthEzgpums5zs4liXMGEvIHOfMpKs46CEKuy+v+X1ycK
- HmCqgmspvyimFcxI84ajPlwwPL7JPU65h/b2w/SoMjnUyAF0Ae5KIUSLL0wp
- DZO0OcLw99uXo8rbl0qoNaSEYuGzv/STJioNR6atodQNNZ4KDdcNhIYTo9Vq
- PcnqaCIWUhtSsSS5trLh5Oy7X2In7BrPs+tpIHxaZO9+q45RnFQV2YRJW0XK
- SFFZrUZJGSNlTVGpqLUyIWqf9EWnjYrRVrHGUawwKMVCU4FP/wMZ2qUbsn2p
- YHvmnqHb+6Yw6QWdKr6qtKXTzia9C/Xzpm0sFvY2DHdFvrLyGXFy3Frlrinn
- gbJm2cwTdMElOVOKe/yyngpQt+zx3O4Cfx5AxIt8DVpWlp2CmzMemnKtLYBc
- LSOKEWrvKmqFMFLyTqC6fEWzavrG6JuSR7JMR6eiRBdDGyI0q8Iazb4LMJsH
- krV/oGEw2UhjePIQTWtv0PZKtiC+DrwTaMQ6yQPkkSCcFNohW7MZHeiUXUtS
- EmmylFIXusn3Gx8heorBt/RrDAeTo7EGUGtwHT0kS2I/kluEvunOyE+/IsIW
- KhIMUxJyZDGfadLPR6VoSYJWiUmRdfMJ1mn0BqzTx6zTAWtZocx7VaijYoX6
- zq3QjctX6ObVVKiNorX73dB8gnVphfoqViiLfvpKIh3+ClD1O2698qke1UPa
- n8yrDUPQyr1ul3p1l3iN4U651/1Sr56SPv8ILUGppv18gN5DTFBRPj7ArUM8
- WFPr3+DTA9w+xIwvf3aA+6+PQRNBERSi00zgYTyjuUKrM/gSq0Tre3/rnoLT
- N0/6h7Qfj9YR1jGrQ9fxOeZ0zGNBxyIer4MJPMEX67gmMCAwKJAV6BcYppMt
- MCSgCdwVuCcwJnBHICKwJNApkBRYFugS6Bbo+w//HMZ8gQ0AAA==
- """,
- """
- androidx/navigation/NavGraph.class:
- H4sIAAAAAAAA/31SXU8TQRQ9s6XbbUFaQBAq+AEoBZStxCdLSPwIWlOr0qYv
- PE3bSZl+zJqdacNjf4v/wCeND6bx0R9lvLOtCkHcZO6de+65d87OnR8/v34D
- 8Bh7DKtcNcNANs98xQeyxY0MlF/mg5ch/3CaAGNYv4LxQmgjVRQmEGNwD6SS
- 5pAhltuuzSAON4UpJBimzKnUDLdK/zuqwLCghakYHppznRkWc6U2H3C/y1XL
- f1tvi4YpbNdI+EH1yeXMYa5ajdIbpSBs+W1h6iGXSvtcqcBELbVfDky53+3S
- kbP693nHQd8ID2nS2QlMVyq/Pej5UhkRKt71i8qE1EY2dAJzJKpxKhqdSZ93
- POQ9QUSGrX+IPYdUbJNWwV7PAq6nMI9FhvnLJQxzpYmKN8LwJjecMKc3iNHY
- mDVJa8DAOoSfSRvladd8xPB+NMymnGVnvDxaGSc1GpKzxnO8peXRcN/Js2fx
- 7x9dSr5ey8SyTn5q3fNGw0x8x8m7+24mkXVejQmebbzPsHnVAM/Ni2RaVVX6
- g4uJvY6hl/A8aAqGdEkqUe736iKs8npX2DsIGrxb46G08QRMVmSLivsh7TeP
- +8rIniiqgdSS0n8u/enfwTKkKkE/bIgjaetXJjW1ccU5Iu7CobdpP4fk0lMl
- u0WRb8WTj+98hvcpSufIuhGYxDbZmTGBohT5OUwTEouKC8R2yCd25zNfsHSx
- 3CW6LV8aUybldpfGDcrvROxr2LWYFTEbAQ8iex8PyR8Rukwnr5wgVkS2iJtF
- rGKNtrhVxG3cOQGzv7Z+gqRGSmNDw9WY1tjUuBfZtMbML5qRw8f+AwAA
- """,
- """
- androidx/navigation/NavHost.class:
- H4sIAAAAAAAA/31OTUvDQBB9s9F+xK9ELVTEv2Da4s2TUMRAVVDwktO2Wcs2
- 6S50t6HH/i4P0nN/lDiJd2fgzZt5w5vZ/3x9A7hDj3AtTb6yOt8kRlZ6Lr22
- JnmR1ZN1vg0iRAtZyaSUZp68ThdqxtOAEE8K60ttkmflZS69vCeIZRWwLdXQ
- rQEEKni+0XU3YJYPCb3dthOKvghFxOyzv9uOxIBqcUS4mfzzD99gy5i7sXJe
- m0a8LTwhfLfr1Uw96lIRrt7Wxuul+tBOT0v1YIz1zapr8RUc4C8ELho8xyXX
- ITsfcrYyBCnaKTopugiZ4ijFMU4ykMMpzjIIh8gh/gUZbPE0RgEAAA==
- """,
- """
- androidx/navigation/Outer$InnerClass.class:
- H4sIAAAAAAAA/41U30/bVhT+rp0fjgngAG35kbXdyFgS2jqwdusK7QZ0DLMQ
- OpjQOvZySbxgCDazHdS9TDz1T6i0vUyapj3x0EobTKtUsfZtf9M07VzbTbrQ
- IST7nnOPz/nOd88513/988czANexypDjds11rNoD3eZ7Vp37lmPry03fdHOG
- bZvuXIN7XhKMQdvie1xvcLuuL29smVU/CZkhMW3Zln+HIZY3CmsMcr6wlkYc
- SRUxKAyKJVBm3DoDM9JQ0ZWChDT5+5uWxzBWPguBKYauuukbLSxKYzCoVWdn
- 17FN258gwKqz+y1DgXicFXO07Lh1fcv0N1xu2Z7ObdvxA29Przh+pdloTInD
- JFTifJ4hLVLkaubXvNnwGTbyZ0tkGOXO2k2dkWMa/RgQ2YeplL6z6ruWTccf
- yBdegQytdJ4LnbbZptWomW4SF1VcEu0YaGPnX3bmtoI3qZF8d9e0awxX8yeh
- T2aLkIngKHIC/G2GrCj9aY7vCMe8cJw73bEoHMfTyOINoV2lw29yb3POqZkM
- mXakYftmXZyvFA4gTZiOSRUTeJdOZH7T5A2asXP519T/S5r909pPvecbDZOq
- Gnf8TdNl6DuJQmTK247fsGx9yfR5jfucbNLOnkz3i4klJRbQ8G+T/YEldsRV
- qtHA/ny8f1GVBiVV0o73VXokTVElJUGyi6RMskd5/lAZPN6flEpstrsvoUnD
- Ukl+/lNC0mKLKS0pdgsvHsqL/ZpCOjkqihQ6kZmROUW6OqloXcOxQVZiCy8e
- yRSYDj0eMdK7Se8R+kqmBa8QneGYEtcSguskEycY+t+BTWKe7mJ7sqgqFb53
- 1/R8yw7crm3TbYmF3estW7ZZae5smO7nosCirk6VN9a4a4l9ZBxZadq+tWMa
- 9p7lWWSaaTeHoXvV59XtJb4beec6ve9xl++YxO0/Yek2R5O26qrTdKvmvCUg
- hiKItRPpaJok+pmJGvSJHxhpCun0W6B1kXbz9F0iqRaPkCqO/IbuJ7ST8Cmt
- PRAt1ym+hBTJMu3Oh970rVcMB2kClaoGjd4QUxczQzJe/BXdBy24RGAsBTDp
- 0CGCyRC5l8GjncHstQH0ayFYETBBLAWn1FNI90eOcOFxKygkm2qRTUVklyI2
- 5wAthUEMRbnHomJlsrHvvociGEwXRw4xEkJWaJXBBAJd7ij9LZKCWvYpLt0/
- wuW+tw4xJiIPUdAKh7hyiGuPO46RjRi9woNWvVWDsagGAYPfcb2zDEoUz3AD
- 70U8viIp2pUrjv+CeOxg/E9IPyAuH4wfQ1oSQFfo/VFYYmFPKkH75KTyNzJJ
- 2rcrlmtVLIeb+IDyLJOeFKTeD2pwLwil+4VPsEDl+ywANLBC8guy36JOTa1D
- NjBt4LaBO/iQVHxkYAaz62Ae5nB3Hb2eeD72oAZrwoPmIeOhz0O/hxuB8aYH
- 3UOW9H8BSGQIivsHAAA=
- """,
- """
- androidx/navigation/Outer$InnerObject.class:
- H4sIAAAAAAAA/41US08TURT+7p0+plMe5SFPxQdFXsoUxBXEBFHjkFKMJRhl
- dWlHGGhndOa2YcnKnVsXLl24YiFxQaKJqRI3/ijiudNREKIxac/5zrnnNd+5
- Mz+OP30BMIvbDCPCLfueU941XVF3NoV0PNdcqUnbz1qua/srG9t2SSbBGDLb
- oi7MinA3zV9ejSEx77iOvMOgjY2vtSCOhIEYkgwxueUEDKP5/+owx6BLryh9
- x91k6B4bz590a3opYjjv+Zvmti03fOG4gSlc15NhwcAseLJQq1QoKn2qrI42
- Krwlgq1Fr2yHQ1ra99nj1zS4/bImKjThhbH82SebG3/GkP1XN2olNio2tYt7
- csv2GTrPV6HW86VKyI8BrkjRrUJxdaGweL8FAzBS5Bxk6MjveJLCzGVbirKQ
- ghJ5ta7RjpgSKSXAwHbIv+soK0eoPM3wvLE3ZPA+bvBMY8/gugLpSOuGcmXa
- 9KNXRl9jb4bn2N2kzr+9S/AMX+rKaAM8F5vRM/GBWB/LsYdHb7SlVCZB3iRh
- RlgnnFJYdZthaob+v24ziXF6lIKo37MD6bjhydSOZBh8XHOlU7Utt+4EDpG2
- cEIkXZPmYtrzjmsXatUN219VxCo+vZKorAnfUXbkbC1KUdpZFi8iO3u29iPh
- i6pN4/zRpCW8EosVEQQ2mUbRq/kl+4GjSvRHJdbODYdp2k8spL5frYv0DbIS
- pFtJx+k0Hlo3yTLVgpR34hD6AQGOqSgYFGCSbGkGIEWlVNE0eXiYfDVK1jrb
- P4RHJ+FaFH66M72L6Ij6nqR27v8llaEL3VEnizQn3Tsx+R7x2P7kV/C3iGv7
- kw3wJ7H9cPAcyRh4Ug+L9TQTomIK9dCfETtQV5peIAI6+n5T0RsmAOnP4E8P
- 0f8RFw9Ch4YZkopHjgm0Eau3wn6T9C1SozFcInqG1qFZuGzhikVPd40ghi1k
- MbIOFuA6RtdhBOo3FiARoCsEPQEyIUiT/AmvDPuL4QQAAA==
- """,
- """
- androidx/navigation/Outer.class:
- H4sIAAAAAAAA/4VRW2sTQRT+ZjaXzSbaNF6aWFsvTbWp4rbFp1qEGhUW0hRs
- CUieJskQJ9nMwu4k9DFP/hD/QfGhoCBB3/xR4tltNA9S3GHPN+f2nTnn/Pz1
- 5RuA53jCUBG6Fwaqd+ZqMVF9YVSg3eOxkWEWjKE4EBPh+kL33ePOQHZNFhZD
- 5kBpZV4yWFu1VgFpZBykkGVImQ8qYlhtXMn6gsE+6PpJvgMeJ9le8+T0sFl/
- U8A1ODkyXmfYaARh3x1I0wmF0pErtA5MwhO5zcA0x75PVMuNYWCIzD2SRvSE
- EWTjo4lF3bFY5GIBBjYk+5mKtR269XYZarNpweFl7vDibOpw27J/fOTl2XSP
- 77B9bqVeZW3+/VOGF3mcsMdiGsfTWoZ1X0TUZD5RLqfCUL2y4+oiKYt7DJv/
- ifwz5wfUXlNMXsvIKJ1EPRtSodV3Y23USHp6oiLV8eXhYjK0gHrQkwxLDaVl
- czzqyPBUUAxDqRF0hd8SoYr1ubGweJqkZOckGIdd+VbFvsq8TuufKtilFaWS
- uVbijRFWScsQFgk5nXSibZLmxtMnTG9fwD5P3I/mwcBTPCZZuAxAjqgAG/m/
- ySsUHX/5r+DvL1D4jKXzxGBhi2SJ3PfpX6N3PCRcJ6wlJTawTbhPNMtEXGrD
- 8nDDw00Pt3CbrljxUEalDRbhDlbbSEdwItyNkImwFmH9NzJGDiwjAwAA
- """,
- """
- androidx/navigation/OuterComp$InnerClassComp$Companion.class:
- H4sIAAAAAAAA/51TTW/TQBB9a6dxYkJJUz4SoJRCgBRB3VQIIRUhQapKkdJW
- giqXHtAmWcom9hp511GPOfFD+Ac9IXFAUY/8KMSsE6i4IJXLzJt582a0M/aP
- n9++A3iGBsNzrgZJLAcngeJjecyNjFVwkBqRtOLoU72tFKGQa52F1nBFJR4Y
- Q3nIxzwIuToODnpD0TceXIb8S6mkecXgNta7JSwg7yMHjyFnPkrN8KLzfyO3
- GZqNzig2oVTBcBwFUpFE8TDYER94GppWrLRJ0r6Jkz2ejESyvd714djRy/X+
- Ofk+yliGjYt1Y1j6LdgThg+44ZRzorFLy2TWFK0BAxtR/kTaaJPQoMlQn058
- 36k6vlMmNJ0Uzj671elky9lkb7yCc/Yl75QdW7vFbIcnF9mRh1sMK/9UeFhh
- WPxbxlD8s1x62z4f7whtpMqkGyNDF2vFA8FwpSOV2E+jnkgOeS+kTKUT93nY
- 5Ym08TxZOm8v6M7+uzhN+mJXWq72NlVGRqIrtaTi10rFJpuj0aQT5ezeyDv2
- c6Hn36MosIskv/D4KwqnGX2fbD5L7qJOtjQrQBE+UGaELs3FT8k7c3HpNDuK
- FVyfJWeCDF3GInEuHlBUIfY27mAVtQzdJf8wG7yGR9kPQ7sgTfkIbhtLbVTa
- WMZVgrjWpt43jsA0qqgRr+Fr3NTI/wIu2QAobQMAAA==
- """,
- """
- androidx/navigation/OuterComp$InnerClassComp.class:
- H4sIAAAAAAAA/5VUW08bVxD+zvq2XgysIRcHSJMWNzWGsECTlAJpuJelXFJI
- aQjtw8HemgV7l+6urfSlylN+QqT2pVIf+sRDorZQFamiyVt/U1V1zu5ibhES
- kn3OzOzMN9+ZmXP++e/PvwDcwdcMPdwqOrZZfKpZvGaWuGfalrZY9Qxnwq5s
- Z3XLIqnMXVeoCTAGdZPXuFbmVklbXN80Cl4CEYb4iGmZ3icM0ZzetcIQyXWt
- pBBDQkEUMoNsCqQxp8TA9BQUNCQhIUX+3obpMvTOXYTIMENDyfD0Oial0xmU
- An2zLcPy+gm4YG9/x9BPfC6K3TlnOyVt0/DWHW5arsYty/b8KFdbsL2Fark8
- LA4XV+gMVxhSIlW2aHzDq2WPwcldLKGuz52u6fAFOafQikuCTRuV2rOXPce0
- qCyXcl3HoAMrne/qadt41SwXDSeBdxTcEO3KnMTPHXbvvox3qdl8e9uwigy3
- c2fhz2YM0YlkJ7IiwfsMHaIt5zl+IBxzwnHifMe8cOxOoQPXhXSbCrDB3Y0J
- u2gwpI8idcszSuKMfcGQ0hRqGFDQjw/pRMa3VV6mObyce0svnjBkzxsJmge+
- XjaosjHb2zAchpazKMRrpFAOb8m9i3Q3KxZukUsCw2Ki57Zsj5C0zVpFM+lY
- jsXL2mQwfhPEyHOqBc925rmzRUUKLuJ9BSOgzMk6GMPghYbsiAbVfRRj4gKP
- U4kP2cwbHi9yjxNFqVKL0AvDxJIUC+jab5H9qSk06oBUpCu6c/DspiJlJEVS
- D54p9JNUWZHkOO0NtEdobyKz/Pq5nCHX5gGpjw2x5vHGlrgqtUl9kdc/xyU1
- OptUE0KbefM8MtuqyiQfPBuQZSlwIjMjc5JkZUBWG9qiGdbHZt68iFBgKvB4
- wUhuJLlJyEvpOrxM+duickyNC84DTJzk+rlVS+AhQ9PJ0lGVFnht0nA90/Ld
- e7fonWhfqlqeWTF0q2a6Jg3Q2NFQ0YwGE9w8Z1rGQrWybjiPxJCJ2bILvLzC
- HVPoobFx2eOFrXm+HerZ09gPucMrBnE8kSR1xNMgVVm2q07BmDYFxLUQYuUM
- ObozEj3roPWamASqySPS4rRfpr1FPO+i86THfOsXpE2Tt0S7kt9DMt/+Oxpf
- +QgrtDZBjMUkYU5R1CS+JO1K4E3fmsUAkSRQqZJQ6R9gamKuaI/lf0PjTh0u
- 7hunfJhU4BDCpIncYXDn6WD21gB6WAlWBPQTS8EpuQ9ptX0PV1/WgwKyyTrZ
- ZEj2WFnUJDJUriD3rbCA6Y7o9z9AFgxG8u27aA8gH9MaARMI9KyF6YdoF9Q6
- 9nFjdQ83W97bxS0RuYsutWsXPbvofXnqGB0ho+PtYVS2dJ1HUAOfwR+4c7oM
- chjPcBf3Qh5f0S7alc13/4JYdKf7b0g/IhbZ6T6ANC+Aeuj/k7BEg5489tsX
- Scj/Ip0g/ahi2XrFshjEx5RnleSEIPWRn34IiZBqxk9KxPYxssr28OBXTLzy
- LRE88adOzNfnWKIij5A0Svuan36ZKINkRpMVw9QaIjqmdXyqYwY6iZjV8Rnm
- yMHFPBbWoLpodrHoQvHXuCssaRctLlpd3PWNgy40Fx2+PPo/FBJ91lIJAAA=
- """,
- """
- androidx/navigation/OuterComp$InnerObject.class:
- H4sIAAAAAAAA/41US08TURT+7p0+plMe5SFPxQeoLVWmoK4gJoAah5RihGCU
- 1aUdy0A7gzO3DUtW/gQXLl3ohoXEBYkmpsrOH2U8dxgFIRKT9t7vnHvO+c58
- 5878+Pn5K4C7uMeQE27F95zKjumKplMV0vFcc6khbX/eq2+PWa5r+0vrm3ZZ
- JsEYMpuiKcyacKvmb6/GkJhxXEfeZ9CyudU2xJEwEEOSISY3nIAhX/xvlmkG
- XXrL0nfcKkNvNlc8ZjzyUsRo0fOr5qYt133huIEpXNeTYdHALHmy1KjVKCp9
- oqyODiq8IYKNea9ih41amvuhMEPN268aokZdXsgWTz/ddO4Fw9h5bEQl1ms2
- 0cU9uWH7DN1nqxD1TLkWamSAK2F0q7S8Mluaf9iGIRgpcg4zdBW3PElh5qIt
- RUVIQYm83tRoVkwtKbWAgW2Rf8dRVoFQZZLhZWt3xOAD3OCZ1q7BdQXS0a4b
- ypXp0A9fGwOt3SleYHNJnX9/l+AZvtCT0YZ4ITalZ+JDsQFWYI8P32gLqUyC
- vEnCjLBOOKWwYptiqodL5040iRw9Tkk0H9iBdNzwdGJLMgw/bbjSqduW23QC
- h4SbPRaTrsvRcDqLjmuXGvV1219R4ipNvbKorQrfUXbkbF+Wory1KLYje+x0
- 7SfCF3WbWvqLpC28FvM1EQQ2mcay1/DL9iNHlRiMSqyeaQ6TNKNYKP+gGhnt
- t8hK0N5Oe5xO46F1myxTDUl5xw+g7xPgmIiCgTk6BtqOApCiUqpomjw8TL4a
- JWvdnR/Do+NwLQo/yUzvJLoi3uPU7r1/pDL0oDdismjntPeP598jHtvLfwN/
- i7i2l2+BP4vthY0XaI2BJ/WwWN9RQlRMoT76M1IH6lrTS0RAx8AfKfrDBCD9
- Bfz5AQY/4eJ+6NAwRavSkWMcHaTqnZAvT98l1RpdMZJnZA2ahcsWrlj0dNcI
- YtTCGK6vgQW4gZtrMAL1ywZIBOgJQV+ATAjStP4Cqe39ku0EAAA=
- """,
- """
- androidx/navigation/OuterComp.class:
- H4sIAAAAAAAA/41RW2sTQRT+ZjaXzSa2abw0sbZVW7Wp4rbFp1qEGBUWYgq2
- BCRPk2SJk2xmZWcS+pgnf4j/oPhQUJCgb/4o8ew2WkQo7rDnm3P7zpxzfvz8
- /BXAEzxkWBWqF4Wyd+IqMZF9YWSo3MOx8aN6OHqfBWMoDsREuIFQffewM/C7
- JguLIXMglTTPGKytaquANDIOUsgypMw7qRnWG5cyP2WwD7pBwuGAx4m21zw6
- rjXrLwu4AidHxgWGjUYY9d2BbzqRkEq7QqnQJFzabYamOQ4ColpqDENDZO5r
- 34ieMIJsfDSxqEsWi1wswMCGZD+RsbZDt94uQ3U2LTi8zB1enE0dblv29w+8
- PJvu8R22z63U86zNv33M8CKPE/ZYTLPgKUVtBELruBeGfGI4nw7Do0s73/w7
- OYt1esR/ZPye/R1qtykmL3xtpEoiHw+p6MqbsTJy5HtqIrXsBH7tYlK0lHrY
- 8xkWG1L5zfGo40fHgmIYSo2wK4KWiGSsz42Fiyf6lOwcheOo67+Ssa8yr9P6
- pwp2aWWpZM6VeIOEm6RlCIuEnE460e6R5sbbIExvn8E+Tdz358FADQ9IFs4D
- kCMqwEb+T/IyRcdf/gv42zMUPmHxNDFY2CJZIvdt+lfpHXcJ1wirSYkNbBPu
- E80SEZfasDxc9XDNw3XcoCuWPZRRaYNp3MRKG2kNR+OWRkZjVWPtF1d0yO47
- AwAA
- """,
- """
- androidx/navigation/TestAbstract.class:
- H4sIAAAAAAAA/4VRy0oDMRQ9SduxHau29dX6AHUh6sLR4kJQhKoIA7WCSjeu
- 0s6gsdMMTNList/iH7gSXEhx6UeJN2P3bg7nkdwcbr5/Pj4BHGGdYUOoIIll
- 8OIpMZSPwshYefehNo2ONonomikwhtKzGAovEurRu+k8h9bNMDinUklzxpDZ
- 2W0XkYPjIosphqx5kpphq/nf8BOGcrMXm0gq7zo0IhBGkMf7wwwVZBYKFsDA
- euS/SKsOiAWH1H08cl1e5S4vERuP8tvV8ajOD9h57uvV4SVuz9WZvV1uieEl
- PSxVWmK/Z6jlRRyEDHNNqcLWoN8Jk3vRicipNOOuiNoikVZPTPcuHiTd8Epa
- UbsdKCP7YVtqSWlDqdikg3V2E5yWMOlsd0JYJeWlGsjtvSP/RoSjRuik5jFW
- CIt/B1CAm+arKS5jLf0shmnKig/I+JjxMetjDiWiKPuoYP4BTGMBi5RruBpL
- Gs4vELgJXekBAAA=
- """,
- """
- androidx/navigation/TestAbstractComp$Companion.class:
- H4sIAAAAAAAA/5VSy27TQBQ9M07zMAHclkfCmzZILRJ1UrErQiqpkCLSIkGV
- TRdo4gxlEnuMPBOry6z4EP6gKyQWKOqSj0LccQJs6ea+zj33+p7xz1/ffwB4
- jicMO0KPslSNzkItcnUqrEp1eCyN3R8am4nIdtPkc8sZoQmqgDEEY5GLMBb6
- NHw7HMvIVuAxlF8orexLBm9re1DHCso+SqgwlOwnZRja/cut2mPobPUnqY2V
- Dsd5EiptZaZFHB7Ij2IaU7sm3jSyaXYosonM9rYHPrhbud6K/oEfkgKlWy83
- jWH1D+FQWjESVlCNJ7lH4jFnas6AgU2ofqZc1qZo1GFozWe+zxvc5wFF81n1
- 4ovXmM92eZu9qlT5xdcyD7jr3WVuQut/tKngLkPtr0D0fUciP6AmpQvCzsSS
- 2t10JBmu95WWR9NkKLNjMYypstZPIxEPRKZcvizWe1rLrBsLYyS9kf8+nWaR
- fK0c1nw31VYlcqCMouZ9rVNb7DHokMwldzt57p6aTnhIWejEIL/y9Buq5wX8
- iGy5KL7BY7L1RQNq8IGAUXRlSX5Gni/J9fNCWEe4tSguCEV0FdcI87BBmV+Q
- 7uE+mtgsFj5Aq/i5SQPqDU7g9bDaw1oP67hBIW72aObtEzCDBpqEG/gGdwzK
- vwGgwqXcGQMAAA==
- """,
- """
- androidx/navigation/TestAbstractComp.class:
- H4sIAAAAAAAA/41RW28SQRT+ZpfrulioN7BeWotI+9CljYmJNCaVxoRIMdGG
- xPA0wIgDy6zZGUgf+S3+g8aHJpoY4qM/ynh2i+2DL7ycb87tO9858/vP958A
- nmOXoczVIAzk4MxTfCaH3MhAeadCm6OeNiHvm0Yw+ZIGY8iP+Ix7PldD711v
- JPomDZshdSiVNK8Y7OpOx0USKQcJpBkS5rPUDJXWKgPqDJnDvr+k2lulpRwZ
- riiVhsuwX22NA0MM3mg28aQyIlTc947FJz71qUFR57RvgvCEh2MR1i/F3nSQ
- wxpD9oqMobaS4uvxdRcFrGdh4RbDdisIh95ImF7IpdIeVyowMYP22oFpT32f
- di3803oiDB9wwylmTWY2fQqLTDYyYGBjip/JyKvRa7BP91zMXccqWo6VX8wd
- K2NlKsXFfNM+sGrsJbNfJ399TVl5K6o+YBFHoc1nxyReqljG3tgwbLyfKiMn
- oqlmUsueL46uZdLPNYKBYFhrSSXa00lPhKecahjWW0Gf+x0eyshfBt2mUiJs
- +FxrQc3Oh2Aa9sUbGeVKyzmd/6YktuheiXjJUnQ+wm3yUoR3CC3CZOyVyfOi
- UxAmdy+QOY/TT5fFwFtUyLqXBcjCIczgxlVzEfEx4f5A7iO7QP4bbp/HERvP
- yDpUlyPGAgmpxtxPsEP4guJ3ifFeF3YTxSZKTdzHBj3xoImHeNQF03iMzS4S
- Go7GlkZKo/AX8BZ2A10DAAA=
- """,
- """
- androidx/navigation/TestClass.class:
- H4sIAAAAAAAA/31Ru04CQRQ9d4BFVlTAF/hs1cJFYqcxUYwJCWKihsZqYDc4
- sMwmzEAs+Rb/wMrEwhBLP8p4d6W2OTmPO/femfn++fgEcIpdwq7U/ihS/oun
- 5UT1pFWR9h4DY+uhNCYLIhT6ciK9UOqed9fpB12bRYrgnCut7AUhdXDYziMD
- x0UaWULaPitD2G/+2/mMUGwOIhsq7d0GVvrSSvbEcJLi1SiGXAwg0ID9FxWr
- KjP/hLA3m7quKAtXFJjNpuXZtCaqdJX5enVEQcRVNYrPFltycs0zlU7mHw8s
- L1iP/ICw0lQ6aI2HnWD0KDshO6Vm1JVhW45UrOem+xCNR93gRsWicj/WVg2D
- tjKK00utI5s0NjiB4PvPN46fg7HMyks0kDl6x8IbE4EKo5OYB9hizP8VIAc3
- ybcT3MRO8kmERc7yT0g1sNTAcgMrKDBFsYESVp9ABmtY59zANdgwcH4BpKME
- iuEBAAA=
- """,
- """
- androidx/navigation/TestClassComp$Companion.class:
- H4sIAAAAAAAA/5VSTW8TMRB99qb5WAJsWz4SvtsGqQW121TcCkiQCilSWiSo
- cukBOYkpTna9aO1EPebED+Ef9ITEAUU98qMQYyfAEfUynnnz3oz3eX/++v4D
- wDM8Zngq9CDP1OAs1mKiToVVmY6PpbGtRBjTytLPDReEJrwExhANxUTEidCn
- 8dveUPZtCQFD8bnSyr5kCDa3ulUsoRiigBJDwX5ShmG7c4k9+wzNzc4os4nS
- 8XCSxkpbmWuRxAfyoxgntpVpY/Nx32b5ochHMt/f6obgbt9qo/+v+SH1XYad
- y01jWP4jOJRWDIQVhPF0EpBtzIWKC2BgI8LPlKt2KRs0GRqzaRjyGg95RNls
- Wr74EtRm0z2+y16Xyvzia5FH3HH3mJuw9l9jSrjLUPnrDl3uSEwOiKG0Z++M
- LPncygaS4XpHaXk0TnsyPxa9hJCVTtYXSVfkytULsNrWWuZ+g6TXCd9n47wv
- 3yjXq78ba6tS2VVGEfmV1pn1ewya5HHBfTid3D0y3f8hVbFzgs6lJ99QPvft
- RxSLHnyBNYrVOQEVhEDEKLuyEG/TyRfi6rl31QluzcG5wGdXcY16AdapCr3o
- Hu6jjg2/8AEa/p8mD4gbnSBoY7mNlTZWcYNS3GzTzNsnYAY11KlvEBrcMSj+
- BuBNy2UQAwAA
- """,
- """
- androidx/navigation/TestClassComp.class:
- H4sIAAAAAAAA/4VRXWsTQRQ9s5vPdWOT+pVYta2NmlZ0myIIpgqaIiykEbQE
- JE+TZIyTbGZlZxL6mN/iPyg+FBQk+OiPEu9uY/vgQ1/umXvn3HPP3Pn95/tP
- AM+ww7DJ1SAK5eDYU3wmh9zIUHlHQptmwLVuhpMvWTCG4ojPuBdwNfTe9Uai
- b7KwGTL7UknzisGubXdcpJFxkEKWIWU+S82w1bpUvcGQ2+8HS53Hl/KrceCK
- 6lm4DPVaaxwaavdGs4knlRGR4oF3ID7xaWCaodImmvZNGB3yaCyixpnNqw4K
- WGHIn4sxPLnc68XshosSVvOwcC1+ZRgNvZEwvYhLpT2uVGiSdu21Q9OeBgG9
- svTP6KEwfMANp5o1mdn0ESwO+TiAgY2pfizjbJdOgzpDdTF3HatsOVZxMXes
- nFVezDfsPWuXvWD2m/SvrxmraMXcPRYrlNp8dkC+pUpMPB0bhrX3U2XkRPhq
- JrXsBeL1hUn6rmY4EAwrLalEezrpieiIE4dhtRX2edDhkYzzZdH1lRJRshVB
- zc6HcBr1xVsZ31WWczr/TUGdtpVKnliJl0e4RVmG8AahRZhOsiplXrwIwvTO
- KXInyfWDJRl4iYcU3TMC8nAIc7hy3lxGskq4P1D4yE5R/IbrJ0nFxiOKDvEK
- pFgiI7VE+z62CZ9T/SYp3urC9lH2UfFxG2t0xB0fd3GvC6axjo0uUhqOxqZG
- RqP0F+8+XexPAwAA
- """,
- """
- androidx/navigation/TestClassWithArg.class:
- H4sIAAAAAAAA/41QTWsTURQ9781kkoyJmcSvNFWrtpQ2Cyct7pRijAgDsUIt
- cZHVSzKkr5m8gXkvocv8FtduBEVwIcGlP0q8b1JcuRBmzr3nvsO5H79+f/8B
- 4Bn2GPaEmmSpnFyFSizlVBiZqvA81qaXCK0/SHPRzaZFMIbgUixFmAg1Dd+N
- LuOxKcJh8F5IJc0Jg3sQHQ4YnIPDQQUFFH24KBEX2ZSBRRX4uFEGR4Wk5kJq
- hv3+//R+Tj2mselaGzKPGOr9WWoSqcK3sRETYQRJ+Hzp0ErMQtkCqOmM6lfS
- sg5lkyOG3nrV8HmT+zxYr3z6eFDyeclprlfHvMNeVRtewFu84/z86PHAPav/
- ZSVSt9xSIfCs1TGzDeqnYvmaxpUqH/3pzNBuvXQSM9T6UsWni/kozs7FKKFK
- o5+ORTIQmbT8uui/TxfZOH4jLdk6Wygj5/FAakmvXaVSkxtrHNHh3Hyphr0j
- ZZzyAjzCHWInxDlFv/0N5fb2V1Q/55pHhFYDtPGY8O5GhZuo2RtRZt1oEwT0
- b7xCezqKhfYXVD/906ayEVzbcDzJ8SF2Kb7Mhyzg1hBOhNsR7kTU9h6laEbY
- QmsIprGN+0MUNWoaDzT8HD2NQKP+B9uGEJmeAgAA
- """,
- """
- androidx/navigation/TestClassWithArgComp$Companion.class:
- H4sIAAAAAAAA/5VSy24TMRQ99qR5DAHSlkfC+xGkFIlOE3VXBCqpkCKlRaJV
- WHSBnMS0TmY8yHaiLrPiQ/iDrpBYoKhLPgpxPQmwpZvre8+5597x8fz89f0H
- gG08Y2gJPTSpGp5FWkzViXAq1dGRtK4dC2s/KHe6a07aafK57oPQRBfAGCoj
- MRVRLPRJ9K4/kgNXQMCQf6m0cq8YgsZGr4wV5EPkUGDIuVNlGba7l1+3w9Bs
- dMepi5WORtMkUtpJo0Uc7clPYhK7dqqtM5OBS82+MGNpdjZ6Ibhfu14f/CM/
- JhnLsHm5aQyrfwT70omhcIIwnkwDMpH5UPIBDGxM+Jny1RZlwyZDfT4LQ17l
- Ia9QNp8VL74E1fmsxbfYm0KRX3zN8wr3vS3mJzT+158C7jKU/ppE33ggpnvU
- qHQm2hw7cr2dDiXD9a7S8mCS9KU5Ev2YkLVuOhBxTxjl6yVY7mgtTbZI0luF
- h+nEDORb5bna+4l2KpE9ZRU172qdumyPRZOszvn708n9k9M1HlIVeUPoXHn+
- DcXzjH5EMZ+Br/GYYnnRgBJCoMIou7IUv6CTL8Xl88xcL7i1ABeCLLuKa8QF
- eEJVmInu4T5qeJotfIB69qOTB9RbOUbQwWoHax2s4waluNmhmbePwSyqqBFv
- EVrcscj/BkiAeOUlAwAA
- """,
- """
- androidx/navigation/TestClassWithArgComp.class:
- H4sIAAAAAAAA/41SW08TQRT+ZnvZ7VpkWxELeEFBLVXZQngSgsEa4yalJkhq
- DE/TdizTbmfN7rThkd/isy9EDYkmhvjojzKe3VZ40AeS3XPmnDnn+85lfv3+
- 9gPABtYYylx1wkB2jlzFR7LLtQyUuy8iXfN5FL2V+nAn7NaCwQcTjMHp8RF3
- fa667utWT7S1iRRDdksqqbcZ0mVvpcmQKq8088jAtJGGRTYPuwzMy8PGlRwM
- 5ClUH8qIoVK/LP8m8XSF3omhiMBjsLba/oR4/bIoy7Hgiq5NXGNYK9f7gSYU
- tzcauFJpESruuy/Eez70dS1QkQ6HbR2Euzzsi3Bz3Nd1GzOYZcidgzFsXLqR
- ixI28yhhLh7IPMNSPQi7bk/oVsililyuVKATlMhtBLox9H0aQeFvvbtC8w7X
- nHzGYJSidbJY5GIBGnaf/Ecytqp06tCmX50dF22jZNiGc3Zs02c4lm1Y6dLZ
- 8aK5blTZU2Y+nypmHWPeqKZ+fswaTnqvcG5ZlDKftjJONsZbZzFLocFHL6hF
- qZJCV/uaYWFvqLQcCE+NZCRbvti5aITWXgs6gmG6LpVoDActEe5zimEo1oM2
- 95s8lLE9ceY9pUSYDFBQsv0mGIZt8VLGd3MTnuY/LFijiaapcwNz8YCp0ApZ
- WdI3SRfjV0g6RXYm8T4ia5uiDdJ25RS5ysJXTJ0kCI8nmcAzPCE5O47CVUzH
- k6ZTjEajgEP/GMuNF0A6U/mCqU//hcmPAyYwFhVlTpJLSFaI/HfMvGOnuPEZ
- CyeJJ4XVhJDR6zOSxtwEewVV0jXy3yLE2wdIebjjYdHDXdyjI5Y8LOP+AViE
- B3h4ACvCdIRyBDuR2QhOhEKE0h8DM7acGQQAAA==
- """,
- """
- androidx/navigation/TestGraph.class:
- H4sIAAAAAAAA/31SXWsTQRQ9M0k2m220af1oYq1Vmwf1wW2Lbxah1g8W1hVs
- CJQ+TbJDOslmVnYnSx/z5A/xHxQfCgoS9M0fJd5Zgz4IzsC995w5c5h7mR8/
- P38F8ARdhi2h4yxV8bmvRaFGwqhU+z2Zm9eZeH9WB2NojUUh/ETokf92MJZD
- U0eFwTlQWplnDJUHD/tN1OB4qKLOUDVnKmfYDv/r/JTBPRgmpYcHbi+6QXTc
- O4yOXjZxBV6DyKsMO2GajfyxNINMKJ37QuvUlF65H6UmmiUJWa2Fk9SQmf9G
- GhELI4jj06JCXTIbGjaAgU2IP1cW7VIV7zF0F3PP423u8RZVi7n7/QNvL+b7
- fJc9r7v820eHt7jV7jPrsBaJ4gU1oXT5iMcTw7D5bqaNmspAFypXg0Qe/n0k
- zeMojSXDaqi0jGbTgcx6gjQM62E6FElfZMriJekdp7NsKF8pCzpL4/4/ttij
- 8VTLnjp2WpTvEHIotyhz2rUSbRPybeeUa48u4V6Ux3eXYqCLexSbvwVokBXg
- YuXP5Q1S27XyBfzkEs1PWL0oCY77ZdzCTvmZaDZksH6KSoBrAa4HuIGbVGIj
- QBudU7Act7BJ5zm8HLdzOL8ATyx+j4kCAAA=
- """,
- """
- androidx/navigation/TestInterface.class:
- H4sIAAAAAAAA/4VOTUvDQBB9s9GmjV+JWqhH8W7a0psnQYRAVVDxktM22ZZt
- 0g10t6HH/i4P0rM/SpzEH+AMvHkz83gz3z+fXwAm6BOupcnXlc63sZG1Xkin
- KxO/K+sS49R6LjPlgwjhUtYyLqVZxC+zpcqcD48QTYvKldrET8rJXDp5RxCr
- 2mNzaqDXAAhU8Hyrm27ILB8R+vtdNxADEYiQ2Xyw343FkJrlmHAz/fcrvsTG
- 0bOsH3isTSu5LRwheKs260w96lIRrl43xumV+tBWz0p1b0zlWqnt8C0c4C8E
- Llo8xyXXETsfcnZSeAn8BN0EPQRMcZTgGCcpyOIUZymERWgR/QKEKxfgUgEA
- AA==
- """,
- """
- androidx/navigation/TestNavHost.class:
- H4sIAAAAAAAA/4VRu04CQRQ9d3koKyrgA/BF7NTCBWKnMfERIwlqooaGamA3
- OLLMJsxAKPkW/8DKxMIQSz/KeHcxxsLEYk7O487MnTsfn69vAA5QIpSEcvuB
- dEeOEkPZEUYGyrn3tLkWw8tAmxkQIfMohsLxheo4N61Hr81ujLD+19afbQlC
- 8kgqaY4JsZ3dRhozmLURR4oQNw9SE7br/1x+SMjWu4HxpXKuPCNcYQR7Vm8Y
- 4/4phFQIIFCX/ZEMVZmZWyFsTsa2bRWsaE3Ghcm4apXpNPH+lLQyVlhU5aI/
- e/h1P9Nz7kiqKNnvGm7/LHA9wmJdKu960Gt5/XvR8tnJ1YO28BuiL0P9bdp3
- waDf9i5kKIq3A2Vkz2tILTk9USow0cEaFVg8HZ7a9D3huBjXWDmRBhJ7L7Cf
- mVhYZ0xGZh4bjOlpAeaYhflmhEVsRf9MmOdsoYlYDYs1ZGrIIscUSzUsY6UJ
- 0lhFnnONtEZBY/YLEThWOyQCAAA=
- """,
- """
- androidx/navigation/TestObject.class:
- H4sIAAAAAAAA/31S0WoTQRQ9M0k2m220aW1tYrVWW0R96LbFN4tQq8LCuoIN
- AenTJDvESTazsDtZ+pgnP8Q/KH0oKEjQNz9KvLNGfRCcgXvvOXPmMPcy3398
- +gLgCXYZtoSOs1TF574WhRoKo1Ltd2Vu3vRHcmDqYAytkSiEnwg99H+zFQbn
- SGllnjFUHj7qNVGD46GKOkPVvFc5w3b4f+unDO7RIClNPHB70w2i0+5xdPKy
- iWvwGkReZ9gJ02zoj6TpZ0Lp3Bdap6Y0y/0oNdE0SchqJRynhsz819KIWBhB
- HJ8UFeqT2dCwAQxsTPy5smifqviAYXc+8zze5h5vUTWfud8+8PZ8dsj32fO6
- y79+dHiLW+0hsw4rkSheUBdKl4/YGxuGzbdTbdREBrpQueon8vjvI2kgJ2ks
- GZZDpWU0nfRl1hWkYVgN04FIeiJTFi9I7zSdZgP5SlnQWRj3/rHFAY2nWvbU
- sdOivEXIodyizGnXSnSXkG87p1x7fAX3ojzeXoiBB7hHsflLgAZZAS6W/lze
- ILVdS5/B312heYnli5LguF/GO9gpvxPNhgxWz1AJcCPAWoB13KQSGwHa6JyB
- 5biFTTrP4eW4ncP5CW9bciiLAgAA
- """
- )
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index 2bfc291..1c5792b 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -27,9 +27,9 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.konan.target.Family
+
plugins {
id("AndroidXPlugin")
- id("com.android.library")
}
androidXMultiplatform {
@@ -39,7 +39,14 @@
ios()
watchos()
tvos()
- android()
+ androidLibrary {
+ namespace = "androidx.paging.common"
+ withAndroidTestOnDeviceBuilder {
+ it.compilationName = "instrumentedTest"
+ it.defaultSourceSetName = "androidInstrumentedTest"
+ it.sourceSetTreeName = "test"
+ }
+ }
defaultPlatform(PlatformIdentifier.JVM)
@@ -150,7 +157,3 @@
metalavaK2UastEnabled = false
samples(project(":paging:paging-samples"))
}
-
-android {
- namespace "androidx.paging.common"
-}
diff --git a/paging/paging-common/lint-baseline.xml b/paging/paging-common/lint-baseline.xml
new file mode 100644
index 0000000..c360e17
--- /dev/null
+++ b/paging/paging-common/lint-baseline.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
+
+ <issue
+ id="NewApi"
+ message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
+ errorLine1=" @OptIn(ExperimentalStdlibApi::class) repeat(result.size) { _hints.removeFirst() }"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
+ errorLine1=" @OptIn(ExperimentalStdlibApi::class) repeat(result.size) { _hints.removeFirst() }"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
+ errorLine1=" repeat(data.size) { removeFirst() }"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/commonTest/kotlin/androidx/paging/TestUtils.kt"/>
+ </issue>
+
+ <issue
+ id="BanThreadSleep"
+ message="Uses Thread.sleep()"
+ errorLine1=" Thread.sleep(1000)"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/commonJvmAndroidTest/kotlin/androidx/paging/PagedListTest.kt"/>
+ </issue>
+
+ <issue
+ id="BanThreadSleep"
+ message="Uses Thread.sleep()"
+ errorLine1=" @Suppress("BlockingMethodInNonBlockingContext") Thread.sleep(100)"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/commonJvmAndroidTest/kotlin/androidx/paging/SingleRunnerTest.kt"/>
+ </issue>
+
+</issues>
diff --git a/paging/paging-testing/build.gradle b/paging/paging-testing/build.gradle
index 9212202..50837cb 100644
--- a/paging/paging-testing/build.gradle
+++ b/paging/paging-testing/build.gradle
@@ -29,7 +29,6 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
}
androidXMultiplatform {
@@ -39,7 +38,14 @@
ios()
watchos()
tvos()
- android()
+ androidLibrary {
+ namespace = "androidx.paging.testing"
+ withAndroidTestOnDeviceBuilder {
+ it.compilationName = "instrumentedTest"
+ it.defaultSourceSetName = "androidInstrumentedTest"
+ it.sourceSetTreeName = "test"
+ }
+ }
defaultPlatform(PlatformIdentifier.ANDROID)
@@ -131,6 +137,3 @@
metalavaK2UastEnabled = false
}
-android {
- namespace "androidx.paging.testing"
-}
diff --git a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt
index 4e97003..22f92bc 100644
--- a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt
+++ b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt
@@ -58,7 +58,8 @@
val scenario =
launchFragmentInContainer<MockPdfViewerFragment>(
- themeResId = androidx.appcompat.R.style.Theme_AppCompat_DayNight_NoActionBar,
+ themeResId =
+ com.google.android.material.R.style.Theme_Material3_DayNight_NoActionBar,
initialState = Lifecycle.State.INITIALIZED
)
scenario.moveToState(nextState)
@@ -109,9 +110,9 @@
onView(withId(R.id.parent_pdf_container))
.perform(selectionViewActions.longClickAndDragRight())
onView(withId(R.id.parent_pdf_container)).check(selectionViewActions.stopHandleMoved())
+ scenario.close()
}
- @Test
fun testPdfViewerFragment_isTextSearchActive_toggleMenu() {
val scenario =
scenarioLoadDocument(
@@ -155,9 +156,9 @@
onView(withId(R.id.close_btn)).perform(click())
onView(withId(R.id.find_query_box))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+ scenario.close()
}
- @Test
fun testPdfViewerFragment_setDocumentUri_passwordProtected_portrait() {
val scenario =
scenarioLoadDocument(
@@ -188,6 +189,7 @@
// Swipe actions
onView(withId(R.id.parent_pdf_container)).perform(swipeUp())
onView(withId(R.id.parent_pdf_container)).perform(swipeDown())
+ scenario.close()
}
@Test
@@ -214,6 +216,7 @@
"Incorrect exception returned ${fragment.documentError?.message}"
)
}
+ scenario.close()
}
companion object {
@@ -221,7 +224,7 @@
private const val TEST_PROTECTED_DOCUMENT_FILE = "sample-protected.pdf"
private const val TEST_CORRUPTED_DOCUMENT_FILE = "corrupted.pdf"
private const val PROTECTED_DOCUMENT_PASSWORD = "abcd1234"
- private const val DELAY_TIME_MS = 1000L
+ private const val DELAY_TIME_MS = 500L
private const val SEARCH_QUERY = "ipsum"
}
}
diff --git a/pdf/integration-tests/testapp/src/main/AndroidManifest.xml b/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
index 309210b..e77d742 100644
--- a/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -24,6 +24,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
+ android:name=".PdfSampleApplication"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
diff --git a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/PdfSampleApplication.kt b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/PdfSampleApplication.kt
new file mode 100644
index 0000000..5696ffb
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/PdfSampleApplication.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.testapp
+
+import android.app.Application
+import com.google.android.material.color.DynamicColors
+
+class PdfSampleApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ // Apply dynamic colors to activities if available
+ DynamicColors.applyToActivitiesIfAvailable(this)
+ }
+}
diff --git a/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml b/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 3cf2d58..badec27 100644
--- a/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -40,9 +40,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/launch_string"
- android:textColor="@color/google_white"
- app:backgroundTint="@color/google_grey"
- app:strokeColor="@color/google_white"
app:strokeWidth="1dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
@@ -55,9 +52,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/search_string"
- android:textColor="@color/google_white"
- app:backgroundTint="@color/google_grey"
- app:strokeColor="@color/google_white"
app:strokeWidth="1dp"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
diff --git a/pdf/pdf-viewer-fragment/build.gradle b/pdf/pdf-viewer-fragment/build.gradle
index f412d40..8ca6798 100644
--- a/pdf/pdf-viewer-fragment/build.gradle
+++ b/pdf/pdf-viewer-fragment/build.gradle
@@ -33,6 +33,9 @@
api(libs.kotlinStdlib)
api(project(":pdf:pdf-viewer"))
api("androidx.core:core:1.13.1")
+
+ implementation("androidx.fragment:fragment-ktx:1.8.1")
+ implementation("com.google.android.material:material:1.11.0")
}
android {
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 82b13ec..7d1f8a9 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -147,6 +147,7 @@
private var shouldRedrawOnDocumentLoaded = false
private var isAnnotationIntentResolvable = false
private var documentLoaded = false
+ private var isSearchMenuAdjusted = false
/**
* The URI of the PDF document to display defaulting to `null`.
@@ -246,7 +247,6 @@
fastScrollView = pdfViewer?.findViewById(R.id.fast_scroll_view)
loadingView = pdfViewer?.findViewById(R.id.loadingView)
paginatedView = fastScrollView?.findViewById(R.id.pdf_view)
- paginationModel = paginatedView!!.paginationModel
zoomView = pdfViewer?.findViewById(R.id.zoom_view)
findInFileView = pdfViewer?.findViewById(R.id.search)
findInFileView!!.setPaginatedView(paginatedView!!)
@@ -306,11 +306,21 @@
paginatedView?.isConfigurationChanged = true
}
- // Need to adjust the view only after the layout phase is completed for the views to
- // accurately calculate the height of the view
+ /**
+ * Need to adjust the view only after the layout phase is completed for the views to
+ * accurately calculate the height of the view. The condition for visibility and
+ * [isSearchMenuAdjusted] guarantees that the listener is only invoked once after layout
+ * change.
+ */
findInFileView?.let { view ->
view.viewTreeObserver?.addOnGlobalLayoutListener {
- activity?.let { adjustInsetsForSearchMenu(view, requireActivity()) }
+ if (view.visibility == View.VISIBLE) {
+ if (!isSearchMenuAdjusted) {
+ activity?.let { adjustInsetsForSearchMenu(view, it) }
+ } else {
+ isSearchMenuAdjusted = false
+ }
+ }
}
}
@@ -365,6 +375,8 @@
findInFileView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = menuMargin
}
+
+ isSearchMenuAdjusted = true
}
/** Called after this viewer enters the screen and becomes visible. */
@@ -540,6 +552,7 @@
SingleTapHandler(
requireContext(),
annotationButton!!,
+ paginatedView!!,
findInFileView!!,
zoomView!!,
selectionModel,
@@ -561,7 +574,7 @@
}
private fun refreshContentAndModels(pdfLoader: PdfLoader) {
- paginationModel = paginatedView!!.initPaginationModelAndPageRangeHandler(requireActivity())
+ paginationModel = paginatedView!!.model
paginatedView?.setPdfLoader(pdfLoader)
findInFileView?.setPdfLoader(pdfLoader)
@@ -692,10 +705,7 @@
private fun detachViewsAndObservers() {
zoomScrollObserver?.let { zoomView?.zoomScroll()?.removeObserver(it) }
- paginatedView?.let { view ->
- view.removeAllViews()
- paginationModel?.removeObserver(view)
- }
+ paginatedView?.let { view -> view.removeAllViews() }
}
override fun onDestroyView() {
@@ -740,6 +750,7 @@
}
if (pdfLoader != null) {
pdfLoaderCallbacks?.uri = fileUri
+ paginatedView?.resetModels()
destroyContentModel()
}
detachViewsAndObservers()
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 4fd9f06..78bb274 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -24,23 +24,22 @@
}
dependencies {
- api(libs.guavaAndroid)
api(libs.kotlinCoroutinesCore)
- api("androidx.fragment:fragment-ktx:1.8.1")
- api("com.google.android.material:material:1.6.0")
implementation(libs.kotlinStdlib)
implementation("androidx.exifinterface:exifinterface:1.3.2")
+ implementation("androidx.core:core:1.13.0")
+ implementation("androidx.annotation:annotation:1.7.0")
+ implementation("com.google.android.material:material:1.11.0")
+ implementation("com.google.errorprone:error_prone_annotations:2.30.0")
testImplementation(project(":pdf:pdf-viewer-fragment"))
testImplementation(libs.junit)
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
- testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
- testImplementation(libs.guavaTestlib)
testImplementation(libs.testExtTruth)
testImplementation(libs.testExtJunitKtx)
testImplementation("androidx.fragment:fragment-testing:1.7.1")
diff --git a/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/widget/FastScrollViewIntegrationTest.kt b/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/widget/FastScrollViewIntegrationTest.kt
index f15fb9f..0ab7ef0 100644
--- a/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/widget/FastScrollViewIntegrationTest.kt
+++ b/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/widget/FastScrollViewIntegrationTest.kt
@@ -55,13 +55,12 @@
// Start by adding a PaginatedView with 10 50x50 pages
paginatedView =
PaginatedView(activity).apply {
- initPaginationModelAndPageRangeHandler(activity).apply {
+ model.apply {
initialize(10)
for (i in 0..9) {
addPage(i, Dimensions(50, 50))
}
}
- model = paginationModel
}
// Add a ZoomView to host the PaginatedView
zoomView =
@@ -73,7 +72,7 @@
fastScrollView =
FastScrollView(activity).apply {
layoutParams = ViewGroup.LayoutParams(100, 400)
- setPaginationModel(paginatedView.paginationModel)
+ setPaginationModel(paginatedView.model)
addView(zoomView)
}
activity.setContentView(fastScrollView)
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java
index 0021531..5f471cb 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/UiFutureValues.java
@@ -24,10 +24,10 @@
import androidx.pdf.data.FutureValues.SettableFutureValue;
import androidx.pdf.util.ThreadUtils;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Helpers to create {@link FutureValue}s that are ready to be used for UI operations: their
@@ -58,8 +58,16 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class UiFutureValues {
private static final String TAG = UiFutureValues.class.getSimpleName();
+
private static final Executor DEFAULT_EXECUTOR = Executors.newFixedThreadPool(4,
- new ThreadFactoryBuilder().setNameFormat("PdfViewer-" + TAG + "-%d").build());
+ new ThreadFactory() {
+ private final AtomicInteger mCount = new AtomicInteger(1);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "PdfViewer-" + TAG + "-" + mCount.getAndIncrement());
+ }
+ });
private static Executor sExecutor = DEFAULT_EXECUTOR;
private UiFutureValues() {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
index 4a24ed8..aae1aa6 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
@@ -31,6 +31,7 @@
import android.widget.TextView.OnEditorActionListener;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.pdf.R;
import androidx.pdf.util.Accessibility;
@@ -43,7 +44,6 @@
import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import javax.annotation.Nullable;
/**
* A View that has a search query box, find-next and find-previous button, useful for finding
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
index 44ac3b1..f020ed4 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
@@ -27,8 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-
-import com.google.common.base.Preconditions;
+import androidx.core.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
index e7eae7f..f8e6af0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
@@ -26,8 +26,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-
-import com.google.common.base.Preconditions;
+import androidx.core.util.Preconditions;
/**
* Represents the content associated with the destination where a goto link is directing.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java
index be8682c..984cebb 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java
@@ -37,6 +37,7 @@
import androidx.pdf.viewer.PageMosaicView;
import androidx.pdf.viewer.PageViewFactory;
import androidx.pdf.viewer.PaginatedView;
+import androidx.pdf.viewer.PdfSelectionHandles;
import java.util.Objects;
@@ -174,10 +175,39 @@
*/
@Override
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
- PageSelection pageSelection = mSelectionModel.selection().get();
- Rect bounds = pageSelection.getRects().get(0);
+ Rect bounds = getBoundsToPlaceMenu();
outRect.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
}
+
+ private Rect getBoundsToPlaceMenu() {
+ PageSelection pageSelection = mSelectionModel.mSelection.get();
+ int selectionPage = pageSelection.getPage();
+ PdfSelectionHandles mSelectionHandles = mPaginatedView.getSelectionHandles();
+ Rect startHandlerect = new Rect();
+ mSelectionHandles.getStartHandle().getGlobalVisibleRect(startHandlerect);
+
+ Rect stopHandleRect = new Rect();
+ mSelectionHandles.getStopHandle().getGlobalVisibleRect(stopHandleRect);
+
+ int screenWidth = mPaginatedView.getResources().getDisplayMetrics().widthPixels;
+ int screenHeight = mPaginatedView.getResources().getDisplayMetrics().heightPixels;
+
+ if (pageSelection.getRects().size() == 1 || startHandlerect.intersect(0, 0, screenWidth,
+ screenHeight)) {
+ return pageSelection.getRects().getFirst();
+ } else if (stopHandleRect.intersect(0, 0, screenWidth, screenHeight)) {
+ return pageSelection.getRects().getLast();
+ } else {
+ // Center of the view in page coordinates
+ int viewCentreX = mPaginatedView.getViewArea().centerX()
+ * mPaginatedView.getModel().getPageSize(selectionPage).getWidth()
+ / mPaginatedView.getModel().getWidth();
+ int viewCentreY = mPaginatedView.getViewArea().centerY()
+ - mPaginatedView.getModel().getPageLocation(selectionPage,
+ mPaginatedView.getViewArea()).top;
+ return new Rect(viewCentreX, viewCentreY, viewCentreX, viewCentreY);
+ }
+ }
}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ThreadUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ThreadUtils.java
index b006b77..02a6bec 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ThreadUtils.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ThreadUtils.java
@@ -22,11 +22,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
/** Thread-related utilities. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class ThreadUtils {
@@ -35,11 +30,6 @@
public static final Handler UI_THREAD_HANDLER = new Handler(Looper.getMainLooper());
- /** The executor for background tasks. Makes it easier to test if it's sequential. */
- private static final Executor BACKGROUND_EXECUTOR =
- Executors.newSingleThreadExecutor(
- new ThreadFactoryBuilder().setNameFormat("PdfViewerThreadUtils-%d").build());
-
/**
* Checks if the running thread is the UI thread.
*
@@ -61,11 +51,6 @@
}
}
- /** Runs the given {@link Runnable} on a background thread. */
- public static void runInBackground(@NonNull Runnable r) {
- BACKGROUND_EXECUTOR.execute(r);
- }
-
/**
* Posts the given runnable on the UI thread, to be started after the given delay (milliseconds)
*/
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
deleted file mode 100644
index 6575393..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.pdf.viewer;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-/**
- * Base class for views that will base their display on the {@link PaginationModel}.
- *
- * <p>Provides consistent {@link #onMeasure(int, int)} and {@link #onLayout(boolean, int, int, int,
- * int)} behavior and requests a layout each time a new page is added to the model.
- *
- * <p>Subclasses must implement {@link #layoutChild(int)} in order to position their actual views.
- * Subclasses can override {@link #onViewAreaChanged()} if they need to perform updates when this
- * happens.
- *
- * <p>Padding will not be acknowledged. If views must implement padding they need to measure
- * themselves but should be aware they will diverge from the coordinates of other views using the
- * {@link PaginationModel}.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public abstract class AbstractPaginatedView extends ViewGroup implements PaginationModelObserver {
-
- @Nullable
- private PaginationModel mModel;
-
- public AbstractPaginatedView(@NonNull Context context) {
- super(context);
- }
-
- public AbstractPaginatedView(@NonNull Context context, @NonNull AttributeSet attrs) {
- super(context, attrs);
- }
-
- public AbstractPaginatedView(@NonNull Context context, @Nullable AttributeSet attrs,
- int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public void setModel(@Nullable PaginationModel model) {
- this.mModel = model;
- }
-
- // This class does not produce a model but rather renders a model generated elsewhere to a view.
- // Any classes wishing to obtain the model should do so from the owner/manager.
- @Nullable
- public PaginationModel getModel() {
- return mModel;
- }
-
- protected boolean isInitialized() {
- return mModel != null;
- }
-
- /**
- * Measures this view in relation to the {@link #mModel} then asks all child views to measure
- * themselves.
- *
- * <p>If the {@link #mModel} is not initialized, this view has nothing to display and will
- * measure (0, 0). Otherwise, view will measure ({@link #mModel}'s width, {@link #mModel}'s
- * estimated height).
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int width = 0;
- int estimatedHeight = 0;
-
- if (isInitialized()) {
- width = mModel.getWidth();
- estimatedHeight = mModel.getEstimatedFullHeight();
- }
-
- setMeasuredDimension(width, estimatedHeight);
- measureChildren(widthMeasureSpec, heightMeasureSpec);
- }
-
- /**
- * Provides consistent layout behavior for subclasses.
- *
- * <p>Does not perform a layout if there aren't any child views. Otherwise asks the
- * subclasses to
- * layout each child by index.
- */
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- int count = getChildCount();
- if (count == 0) {
- return;
- }
-
- for (int i = 0; i < count; i++) {
- layoutChild(i);
- }
- }
-
- /**
- * Lays out the child at {@code index}.
- *
- * <p>Subclasses should use the {@link #mModel} to determine top and bottom values.
- */
- protected abstract void layoutChild(int index);
-
- /** Requests a layout because this view has to grow now to accommodate the new page(s). */
- @Override
- public void onPageAdded() {
- requestLayout();
- }
-
- /**
- * Implementation of PaginationModelObserver, is no-op at this level.
- *
- * <p>Will be called each time the viewArea of the model is changed. Should be overridden by any
- * subclasses that wish to perform actions when this occurs.
- */
- @Override
- public void onViewAreaChanged() {
- }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java
index 83247d4..0dc9a7d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java
@@ -84,7 +84,6 @@
mPageLinksView.setPageUrlLinks(links);
}
- @NonNull
@Override
public void setPageGotoLinks(@Nullable List<GotoLink> links) {
mPageView.setPageGotoLinks(links);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
index 8936f4e..ea3ece28 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
@@ -56,11 +56,6 @@
mMaxPage = maxPage;
}
- @NonNull
- public PaginationModel getPaginationModel() {
- return mPaginationModel;
- }
-
/**
* Returns the page currently roughly centered in the view.
*/
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 0ab2bfe..3cdd35d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -21,10 +21,12 @@
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
import androidx.pdf.ViewState;
import androidx.pdf.data.Range;
import androidx.pdf.util.PaginationUtils;
@@ -45,14 +47,11 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings("WrongCall")
-public class PaginatedView extends AbstractPaginatedView {
-
- private static final String TAG = PaginatedView.class.getSimpleName();
-
+public class PaginatedView extends ViewGroup implements PaginationModelObserver {
/** Maps the current child views to pages. */
private final SparseArray<PageView> mPageViews = new SparseArray<>();
- private PaginationModel mPaginationModel;
+ private PaginationModel mModel;
private PageRangeHandler mPageRangeHandler;
@@ -68,6 +67,9 @@
private boolean mIsConfigurationChanged = false;
+ /** The current viewport in content coordinates */
+ private final Rect mViewArea = new Rect();
+
public PaginatedView(@NonNull Context context) {
this(context, null);
}
@@ -78,20 +80,112 @@
public PaginatedView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
-
+ mModel = new PaginationModel(context);
+ mPageRangeHandler = new PageRangeHandler(mModel);
}
- /** Instantiate PaginationModel and PageRangeHandler */
- @NonNull
- public PaginationModel initPaginationModelAndPageRangeHandler(@NonNull Context context) {
- mPaginationModel = new PaginationModel(context);
- mPageRangeHandler = new PageRangeHandler(mPaginationModel);
- return mPaginationModel;
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mModel.addObserver(this);
+ }
+
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mPageRangeHandler != null) {
+ mPageRangeHandler.setVisiblePages(null);
+ }
+ mModel.removeObserver(this);
+ }
+
+ @VisibleForTesting
+ public void setModel(@NonNull PaginationModel model) {
+ mModel = model;
}
@NonNull
- public PaginationModel getPaginationModel() {
- return mPaginationModel;
+ public PaginationModel getModel() {
+ return mModel;
+ }
+
+ @NonNull
+ public PaginationModel resetModels() {
+ mModel = new PaginationModel(getContext());
+ mPageRangeHandler = new PageRangeHandler(mModel);
+ return mModel;
+ }
+
+ /** Requests a layout because this view has to grow now to accommodate the new page(s). */
+ @Override
+ public void onPageAdded() {
+ requestLayout();
+ }
+
+ protected boolean isInitialized() {
+ return mModel != null;
+ }
+
+ /**
+ * Measures this view in relation to the {@link #mModel} then asks all child views to measure
+ * themselves.
+ *
+ * <p>If the {@link #mModel} is not initialized, this view has nothing to display and will
+ * measure (0, 0). Otherwise, view will measure ({@link #mModel}'s width, {@link #mModel}'s
+ * estimated height).
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = 0;
+ int estimatedHeight = 0;
+
+ if (isInitialized()) {
+ width = mModel.getWidth();
+ estimatedHeight = mModel.getEstimatedFullHeight();
+ }
+
+ setMeasuredDimension(width, estimatedHeight);
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * Provides consistent layout behavior for subclasses.
+ *
+ * <p>Does not perform a layout if there aren't any child views. Otherwise asks the
+ * subclasses to
+ * layout each child by index.
+ */
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int count = getChildCount();
+ if (count == 0) {
+ return;
+ }
+
+ for (int i = 0; i < count; i++) {
+ layoutChild(i);
+ }
+ }
+
+ /**
+ * Returns the current viewport in content coordinates
+ */
+ @NonNull
+ public Rect getViewArea() {
+ return mViewArea;
+ }
+
+ /**
+ * Updates the current viewport
+ *
+ * @param viewArea the viewport in content coordinates
+ */
+ public void setViewArea(@NonNull Rect viewArea) {
+ if (!viewArea.equals(this.mViewArea)) {
+ this.mViewArea.set(viewArea);
+ onViewAreaChanged();
+ }
}
@NonNull
@@ -120,7 +214,7 @@
@NonNull
public PdfSelectionHandles getSelectionHandles() {
- return mSelectionHandles;
+ return mSelectionHandles;
}
public void setSelectionHandles(@NonNull PdfSelectionHandles selectionHandles) {
@@ -243,10 +337,10 @@
*
* @param index the index of the child view in this ViewGroup
*/
- @Override
- protected void layoutChild(int index) {
+ private void layoutChild(int index) {
int pageNum = mPageViews.keyAt(index);
- Rect pageCoordinates = getModel().getPageLocation(pageNum);
+ Rect viewArea = getViewArea();
+ Rect pageCoordinates = getModel().getPageLocation(pageNum, viewArea);
PageView child = (PageView) getChildAt(index);
child
@@ -257,7 +351,6 @@
pageCoordinates.right,
pageCoordinates.bottom);
- Rect viewArea = getModel().getViewArea();
child
.getPageView()
.setViewArea(
@@ -275,38 +368,13 @@
}
/** Perform a layout when the viewArea of the {@code model} has changed. */
- @Override
- public void onViewAreaChanged() {
+ private void onViewAreaChanged() {
// We can't wait for the next layout pass, the pages will be drawn before.
// We could still optimize to skip the next layoutChild() calls for the pages that have been
// laid out already for this viewArea.
onLayout(false, getLeft(), getTop(), getRight(), getBottom());
}
- @Override
- public void onWindowFocusChanged(boolean hasWindowFocus) {
- super.onWindowFocusChanged(hasWindowFocus);
- if (getVisibility() == View.VISIBLE && mPageRangeHandler != null) {
- mPageRangeHandler.adjustMaxPageToUpperVisibleRange();
- if (getChildCount() > 0) {
- for (PageMosaicView page : getChildViews()) {
- page.clearTiles();
- if (mPdfLoader != null) {
- mPdfLoader.cancelAllTileBitmaps(page.getPageNum());
- }
- }
- }
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mPageRangeHandler != null) {
- mPageRangeHandler.setVisiblePages(null);
- }
- }
-
/**
* Refreshes the page range for the visible area.
*/
@@ -350,7 +418,7 @@
if (getViewAt(pageNum) == null) {
mPageViewFactory.getOrCreatePageView(pageNum,
PaginationUtils.getPageElevationInPixels(getContext()),
- mPaginationModel.getPageSize(pageNum));
+ mModel.getPageSize(pageNum));
requiresLayoutPass = true;
}
}
@@ -400,7 +468,7 @@
PageMosaicView pageView = mPageViewFactory.getOrCreatePageView(
page,
PaginationUtils.getPageElevationInPixels(getContext()),
- mPaginationModel.getPageSize(page));
+ mModel.getPageSize(page));
pageView.clearTiles();
pageView.requestFastDrawAtZoom(stableZoom);
pageView.refreshPageContentAndOverlays();
@@ -412,7 +480,7 @@
PageMosaicView pageView = mPageViewFactory.getOrCreatePageView(
page,
PaginationUtils.getPageElevationInPixels(getContext()),
- mPaginationModel.getPageSize(page));
+ mModel.getPageSize(page));
pageView.requestDrawAtZoom(stableZoom);
pageView.refreshPageContentAndOverlays();
}
@@ -433,7 +501,7 @@
PageMosaicView pageView = mPageViewFactory.getOrCreatePageView(
page,
PaginationUtils.getPageElevationInPixels(getContext()),
- mPaginationModel.getPageSize(page));
+ mModel.getPageSize(page));
pageView.requestTiles();
}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index f11e659..8407a743 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -50,19 +50,14 @@
* <ol>
* <li>{@link #initialize(int)} with the number of pages it will contain.
* <li>{@link #addPage(int, Dimensions)} to set the dimensions for each page.
- * <li>{@link #setViewArea(Rect)} to report current visible area so pages can be moved
- * horizontally for maximum visibility.
* </ol>
*
* <p>This model is observable. Any classes implementing {@link PaginationModelObserver} can
* register themselves via {@link #addObserver(PaginationModelObserver)} and will be notified when
- * pages are added or the {@link #mViewArea} is changed.
+ * pages are added
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PaginationModel {
-
- private static final String TAG = PaginationModel.class.getSimpleName();
-
/**
* The spacing added before and after each page (the actual space between 2 consecutive pages is
* twice this distance), in pixels.
@@ -86,22 +81,6 @@
private int mAccumulatedPageSize = 0;
- /**
- * The portion of this model that is currently (or last we knew) exposed on the screen.
- *
- * <p>In the co-ordinates of this model - so if this entire model is within the visible area,
- * then
- * {@code viewArea} will contain the rect Rect(0, 0, getWidth, getHeight). Current visible area
- * should be reported to this model via {@link #setViewArea(Rect)}.
- */
- private final Rect mViewArea = new Rect();
-
- /**
- * A temp working instance for computing {@link #mViewArea} to avoid excessive object
- * creation.
- */
- private final Rect mTempViewArea = new Rect();
-
private final Set<PaginationModelObserver> mObservers = new HashSet<>();
public PaginationModel(@NonNull Context context) {
@@ -284,21 +263,7 @@
mMaxPages - mSize + 1));
}
- /**
- * Updates the portion of this model that is visible on the screen, in this model's
- * coordinates -
- * so relative to (0, 0)-(getWidth(), getHeight()).
- */
- public void setViewArea(@NonNull Rect viewArea) {
- mTempViewArea.set(viewArea);
- if (!mTempViewArea.intersect(
- 0, 0, getWidth(), getEstimatedFullHeight())) { // Modifies tempViewArea.
- }
- if (!mTempViewArea.equals(this.mViewArea)) {
- this.mViewArea.set(mTempViewArea);
- notifyViewAreaChanged();
- }
- }
+
/**
* Returns the location of the page in the model.
@@ -313,10 +278,11 @@
* </ul>
*
* @param pageNum - index of requested page
+ * @param viewArea - the current viewport in content coordinates
* @return - coordinates of the page within this model
*/
@NonNull
- public Rect getPageLocation(int pageNum) {
+ public Rect getPageLocation(int pageNum, @NonNull Rect viewArea) {
int left = 0;
int right = getWidth();
int top = mPageStops[pageNum];
@@ -324,15 +290,15 @@
int width = mPages[pageNum].getWidth();
if (width < right - left) {
// this page is smaller than the view's width, it may slide left or right.
- if (width < mViewArea.width()) {
+ if (width < viewArea.width()) {
// page is smaller than the view: center (but respect min left margin)
- left = Math.max(left, mViewArea.left + (mViewArea.width() - width) / 2);
+ left = Math.max(left, viewArea.left + (viewArea.width() - width) / 2);
} else {
// page is larger than view: scroll proportionally between the margins.
- if (mViewArea.right > right) {
+ if (viewArea.right > right) {
left = right - width;
- } else if (mViewArea.left > left) {
- left = mViewArea.left * (right - width) / (right - mViewArea.width());
+ } else if (viewArea.left > left) {
+ left = viewArea.left * (right - width) / (right - viewArea.width());
}
}
right = left + width;
@@ -371,23 +337,6 @@
return mMaxPages;
}
- /**
- * Returns the intersection of this model and the last viewArea that was reported to this model
- * via {@link #setViewArea(Rect)}.
- */
- @NonNull
- public Rect getViewArea() {
- return mViewArea;
- }
-
- /** Notify all observers that the {@code viewArea} has changed. */
- private void notifyViewAreaChanged() {
- Iterator<PaginationModelObserver> iterator = iterator();
- while (iterator.hasNext()) {
- iterator.next().onViewAreaChanged();
- }
- }
-
/** Notify all observers that a page has been added to the model. */
private void notifyPageAdded() {
Iterator<PaginationModelObserver> iterator = iterator();
@@ -432,7 +381,7 @@
@NonNull
public Iterator<PaginationModelObserver> iterator() {
synchronized (mObservers) {
- return new ArrayList<PaginationModelObserver>(mObservers).iterator();
+ return new ArrayList<>(mObservers).iterator();
}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelObserver.java
index 9619bfe..c404f42 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelObserver.java
@@ -35,13 +35,4 @@
* Implementations are free to use this information as desired.
*/
default void onPageAdded() {}
-
- /**
- * Notifies the implementation that the {@code viewArea} of the {@link PaginationModel} has
- * changed.
- *
- * <p>The {@link PaginationModel} does not enforce any implementation expectations.
- * Implementations are free to use this information as desired.
- */
- default void onViewAreaChanged() {}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java
index 405014c..925bdf2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java
@@ -94,4 +94,14 @@
protected void onDragHandleUp() {
mSelectionActionMode.resume();
}
+
+ @NonNull
+ public ImageView getStartHandle() {
+ return mStartHandle;
+ }
+
+ @NonNull
+ public ImageView getStopHandle() {
+ return mStopHandle;
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
index 0fe8777..02df1c8 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -229,7 +229,7 @@
mFindInFileView = mPdfViewer.findViewById(R.id.search);
mFastScrollView = mPdfViewer.findViewById(R.id.fast_scroll_view);
mPaginatedView = mPdfViewer.findViewById(R.id.pdf_view);
- mPaginationModel = mPaginatedView.getPaginationModel();
+ mPaginationModel = mPaginatedView.getModel();
mZoomView = mPdfViewer.findViewById(R.id.zoom_view);
mLoadingSpinner = mPdfViewer.findViewById(R.id.progress_indicator);
setUpEditFab();
@@ -279,7 +279,7 @@
new SearchQueryObserver(mPaginatedView);
mSearchModel.query().addObserver(mSearchQueryObserver);
- mSingleTapHandler = new SingleTapHandler(getContext(), mAnnotationButton,
+ mSingleTapHandler = new SingleTapHandler(getContext(), mAnnotationButton, mPaginatedView,
mFindInFileView, mZoomView, mSelectionModel, mPaginationModel, mLayoutHandler);
mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
mPaginatedView, mZoomView, mSingleTapHandler, mFindInFileView);
@@ -378,7 +378,6 @@
if (mPaginatedView != null) {
mPaginatedView.removeAllViews();
- mPaginationModel.removeObserver(mPaginatedView);
mPaginatedView = null;
}
@@ -627,8 +626,6 @@
mPaginatedView.getPageRangeHandler().setMaxPage(1);
if (viewState().get() != ViewState.NO_VIEW) {
mPaginationModel.initialize(numPages);
- mPaginatedView.setModel(mPaginationModel);
- mPaginationModel.addObserver(mPaginatedView);
mFastScrollView.setPaginationModel(mPaginationModel);
dismissPasswordDialog();
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
index 74f02a6..d5e21ac 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
@@ -38,6 +38,7 @@
public class SingleTapHandler {
private final Context mContext;
private final FloatingActionButton mFloatingActionButton;
+ private final PaginatedView mPaginatedView;
private final FindInFileView mFindInFileView;
private final ZoomView mZoomView;
private final PdfSelectionModel mPdfSelectionModel;
@@ -47,6 +48,7 @@
public SingleTapHandler(@NonNull Context context,
@NonNull FloatingActionButton floatingActionButton,
+ @NonNull PaginatedView paginatedView,
@NonNull FindInFileView findInFileView,
@NonNull ZoomView zoomView,
@NonNull PdfSelectionModel pdfSelectionModel,
@@ -54,6 +56,7 @@
@NonNull LayoutHandler layoutHandler) {
mContext = context;
mFloatingActionButton = floatingActionButton;
+ mPaginatedView = paginatedView;
mFindInFileView = findInFileView;
mZoomView = zoomView;
mPdfSelectionModel = pdfSelectionModel;
@@ -65,7 +68,6 @@
mIsAnnotationIntentResolvable = annotationIntentResolvable;
}
- /** */
public void handleSingleTapConfirmedEventOnPage(@NonNull MotionEvent event,
@NonNull PageMosaicView pageMosaicView) {
if (mIsAnnotationIntentResolvable) {
@@ -123,7 +125,8 @@
if (destination.getYCoordinate() != null) {
int pageY = (int) destination.getYCoordinate().floatValue();
- Rect pageRect = mPaginationModel.getPageLocation(destination.getPageNumber());
+ Rect pageRect = mPaginationModel.getPageLocation(destination.getPageNumber(),
+ mPaginatedView.getViewArea());
int x = pageRect.left + (pageRect.width() / 2);
int y = mPaginationModel.getLookAtY(destination.getPageNumber(), pageY);
// Zoom should match the width of the page.
@@ -155,7 +158,7 @@
return;
}
- Rect pageRect = mPaginationModel.getPageLocation(pageNum);
+ Rect pageRect = mPaginationModel.getPageLocation(pageNum, mPaginatedView.getViewArea());
int x = pageRect.left + (pageRect.width() / 2);
int y = pageRect.top + (pageRect.height() / 2);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
index 2fb40ab..f707a3e4 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
@@ -17,6 +17,7 @@
package androidx.pdf.viewer;
import android.animation.ValueAnimator;
+import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
@@ -68,10 +69,13 @@
@Override
public void onChange(@Nullable ZoomView.ZoomScroll oldPosition,
@Nullable ZoomView.ZoomScroll position) {
- if (mPaginatedView == null || !mPaginatedView.getPaginationModel().isInitialized()
- || position == null || mPaginatedView.getPaginationModel().getSize() == 0) {
+ if (mPaginatedView == null || !mPaginatedView.getModel().isInitialized()
+ || position == null || mPaginatedView.getModel().getSize() == 0) {
return;
}
+ // Stop showing context menu if there is any change in zoom or scroll, resume only when
+ // the new position is stable
+ mSelectionActionMode.stopActionMode();
mZoomView.loadPageAssets(mLayoutHandler, mViewState);
if (oldPosition.scrollY > position.scrollY) {
@@ -113,11 +117,8 @@
mPaginatedView.setConfigurationChanged(false);
}
- if (position.scrollY > 0) {
- mSelectionActionMode.stopActionMode();
- }
- if (position.scrollY == oldPosition.scrollY) {
- mSelectionActionMode.resume();
+ if (mPaginatedView.getSelectionModel().selection().get() != null && position.stable) {
+ setUpContextMenu();
}
}
@@ -152,4 +153,36 @@
public void setAnnotationIntentResolvable(boolean annotationIntentResolvable) {
mIsAnnotationIntentResolvable = annotationIntentResolvable;
}
-}
+
+ private void setUpContextMenu() {
+ // Resume the context menu if selected area is on the current viewing screen
+ if (mPaginatedView.getSelectionModel().getPage() != -1) {
+ int selectionPage = mPaginatedView.getSelectionModel().getPage();
+ int firstPageInVisibleRange =
+ mPaginatedView.getPageRangeHandler().getVisiblePages().getFirst();
+ int lastPageInVisisbleRange =
+ mPaginatedView.getPageRangeHandler().getVisiblePages().getLast();
+
+ // If selection is within the range of visible pages
+ if (selectionPage >= firstPageInVisibleRange
+ && selectionPage <= lastPageInVisisbleRange) {
+ // Start and stop coordinates in a page wrt pagination model
+ int startX = mPaginatedView.getModel().getLookAtX(selectionPage,
+ mPaginatedView.getSelectionModel().selection().get().getStart().getX());
+ int startY = mPaginatedView.getModel().getLookAtY(selectionPage,
+ mPaginatedView.getSelectionModel().selection().get().getStart().getY());
+ int stopX = mPaginatedView.getModel().getLookAtX(selectionPage,
+ mPaginatedView.getSelectionModel().selection().get().getStop().getX());
+ int stopY = mPaginatedView.getModel().getLookAtY(selectionPage,
+ mPaginatedView.getSelectionModel().selection().get().getStop().getY());
+
+ Rect currentViewArea = mPaginatedView.getViewArea();
+
+ if (currentViewArea.intersect(startX, startY, stopX, stopY)) {
+ mSelectionActionMode.resume();
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index c8b00b8..b5fbe27 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -117,28 +117,27 @@
return
}
- if (selection.page >= paginatedView.paginationModel.size) {
+ if (selection.page >= paginatedView.model.size) {
layoutHandler!!.layoutPages(selection.page + 1)
return
}
val rect = selection.pageMatches.getFirstRect(selection.selected)
- val x: Int = paginatedView.paginationModel.getLookAtX(selection.page, rect.centerX())
- val y: Int = paginatedView.paginationModel.getLookAtY(selection.page, rect.centerY())
+ val x: Int = paginatedView.model.getLookAtX(selection.page, rect.centerX())
+ val y: Int = paginatedView.model.getLookAtY(selection.page, rect.centerY())
zoomView.centerAt(x.toFloat(), y.toFloat())
pageViewFactory!!
.getOrCreatePageView(
selection.page,
pageElevationInPixels,
- paginatedView.paginationModel.getPageSize(selection.page)
+ paginatedView.model.getPageSize(selection.page)
)
.setOverlay(selection.overlay)
}
private fun isPageCreated(pageNum: Int): Boolean {
- return pageNum < paginatedView.paginationModel.size &&
- paginatedView.getViewAt(pageNum) != null
+ return pageNum < paginatedView.model.size && paginatedView.getViewAt(pageNum) != null
}
private fun getPage(pageNum: Int): PageViewFactory.PageView? {
@@ -198,16 +197,12 @@
paginatedView.pageRangeHandler.maxPage = 1
if (viewState.get() != ViewState.NO_VIEW) {
if (uri != null && data.uri == uri) {
- paginatedView.paginationModel.setMaxPages(-1)
+ paginatedView.model.setMaxPages(-1)
}
- paginatedView.paginationModel.initialize(numPages)
+ paginatedView.model.initialize(numPages)
- // Add pagination model to the view
- paginatedView.model = paginatedView.paginationModel
- paginatedView.let { paginatedView.paginationModel.addObserver(it) }
-
- fastScrollView.setPaginationModel(paginatedView.paginationModel)
+ fastScrollView.setPaginationModel(paginatedView.model)
dismissPasswordDialog()
@@ -233,6 +228,9 @@
"Document not loaded but status " + status.number
)
PdfStatus.PDF_ERROR -> {
+ loadingView.showErrorView(
+ context.resources.getString(R.string.error_cannot_open_pdf)
+ )
handleError(status)
}
PdfStatus.FILE_ERROR,
@@ -246,12 +244,12 @@
override fun pageBroken(page: Int) {
if (viewState.get() != ViewState.NO_VIEW) {
- if (page < paginatedView.paginationModel.numPages) {
+ if (page < paginatedView.model.numPages) {
pageViewFactory!!
.getOrCreatePageView(
page,
pageElevationInPixels,
- paginatedView.paginationModel.getPageSize(page)
+ paginatedView.model.getPageSize(page)
)
.setFailure(context.resources.getString(R.string.error_on_page, page + 1))
// TODO: Track render error.
@@ -262,9 +260,9 @@
override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
if (viewState.get() != ViewState.NO_VIEW) {
- paginatedView.paginationModel.addPage(pageNum, dimensions)
+ paginatedView.model.addPage(pageNum, dimensions)
- layoutHandler!!.pageLayoutReach = paginatedView.paginationModel.size
+ layoutHandler!!.pageLayoutReach = paginatedView.model.size
if (
searchModel!!.query().get() != null &&
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
index e8fe78f..441cf96 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
@@ -33,8 +33,7 @@
import androidx.pdf.service.PdfDocumentRemoteProto;
import androidx.pdf.util.TileBoard.TileInfo;
-import com.google.common.collect.ImmutableList;
-
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -650,7 +649,7 @@
protected List<GotoLink> doInBackground(PdfDocumentRemoteProto pdfDocument)
throws RemoteException {
if (TaskDenyList.sDisableLinks) {
- return ImmutableList.of();
+ return Collections.emptyList();
} else {
return pdfDocument.getPdfDocumentRemote().getPageGotoLinks(mPageNum);
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
index 3a6e022..92636fa 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
@@ -184,9 +184,9 @@
@Override
public void onStart() {
super.onStart();
- mTextDefaultColor = getResources().getColor(R.color.text_default);
- mTextErrorColor = getResources().getColor(R.color.text_error);
- mBlueColor = getResources().getColor(R.color.google_blue);
+ mTextDefaultColor = getResources().getColor(R.color.pdf_viewer_color_on_surface);
+ mTextErrorColor = getResources().getColor(R.color.pdf_viewer_color_on_error);
+ mBlueColor = getResources().getColor(R.color.pdf_viewer_color_primary);
EditText textField = (EditText) getDialog().findViewById(R.id.password);
textField.getBackground().setColorFilter(mBlueColor, PorterDuff.Mode.SRC_ATOP);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
index d7755ef..e5a021c 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
@@ -902,7 +902,7 @@
PaginatedView paginatedView = this.findViewById(R.id.pdf_view);
ZoomScroll position = this.zoomScroll().get();
- if (position == null || !paginatedView.getPaginationModel().isInitialized()) {
+ if (position == null || !paginatedView.getModel().isInitialized()) {
return;
}
@@ -911,7 +911,7 @@
this.setStableZoom(position.zoom);
}
- paginatedView.getPaginationModel().setViewArea(this.getVisibleAreaInContentCoords());
+ paginatedView.setViewArea(this.getVisibleAreaInContentCoords());
paginatedView.refreshPageRangeInVisibleArea(position, this.getHeight());
paginatedView.handleGonePages(false);
paginatedView.loadInvisibleNearPageRange(this.getStableZoom());
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java
index f2ca1b9..cf5a797 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java
@@ -16,6 +16,7 @@
package androidx.pdf.widget;
+import android.content.Context;
import android.content.res.Resources;
import android.view.MotionEvent;
import android.view.View;
@@ -168,16 +169,17 @@
*/
@NonNull
protected ImageView createHandle(@NonNull ViewGroup parent, boolean isStop, int id) {
- ImageView handle = new ImageView(parent.getContext());
+ Context context = parent.getContext();
+ ImageView handle = new ImageView(context);
handle.setId(id);
handle.setLayoutParams(
new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
handle.setColorFilter(
- parent.getContext().getResources().getColor(R.color.selection_handles));
+ context.getResources().getColor(R.color.pdf_viewer_selection_handles));
handle.setAlpha(HANDLE_ALPHA);
int descId = isStop ? R.string.desc_selection_stop : R.string.desc_selection_start;
- handle.setContentDescription(parent.getContext().getString(descId));
+ handle.setContentDescription(context.getString(descId));
handle.setVisibility(View.GONE);
parent.addView(handle);
handle.setOnTouchListener(mOnTouchListener);
diff --git a/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml b/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml
index d921c91..9c9ac06 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/custom_edit_text_cursor.xml
@@ -16,5 +16,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="2dp" />
- <solid android:color="@color/google_blue" />
+ <solid android:color="?attr/colorPrimary" />
</shape>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml b/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml
index 922dea5..47bd7c7 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/fastscroll_background.xml
@@ -17,5 +17,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
- <solid android:color="@android:color/white" />
+ <solid android:color="?attr/colorSurfaceContainerHigh" />
</shape>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/drawable/page_indicator_background.xml b/pdf/pdf-viewer/src/main/res/drawable/page_indicator_background.xml
index 4df08d2..fadbc1e 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/page_indicator_background.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/page_indicator_background.xml
@@ -17,5 +17,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp" />
- <solid android:color="@color/google_white" />
+ <solid android:color="?attr/colorSurfaceContainerHigh" />
</shape>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_left.xml b/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_left.xml
index 7132904..257d2c5 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_left.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_left.xml
@@ -22,7 +22,7 @@
android:viewportWidth="44">
<path
- android:fillColor="#FFA8C7FA"
+ android:fillColor="?attr/colorPrimaryFixedDim"
android:fillType="evenOdd"
android:pathData="M33.939,9.899L33.897,23.94C33.874,31.695 27.569,38.001 19.814,38.024C12.059,38.047 5.791,31.779 5.814,24.024C5.837,16.269 12.142,9.963 19.897,9.94L33.939,9.899Z" />
diff --git a/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_right.xml b/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_right.xml
index 49021c7b..146f43b 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_right.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/selection_drag_handle_right.xml
@@ -18,12 +18,11 @@
android:width="44dp"
android:height="44dp"
android:autoMirrored="true"
- android:tint="#FFA8C7FA"
android:viewportHeight="44"
android:viewportWidth="44">
<path
- android:fillColor="#FFA8C7FA"
+ android:fillColor="?attr/colorPrimaryFixedDim"
android:fillType="evenOdd"
android:pathData="M9.901,9.899L9.942,23.94C9.965,31.695 16.271,38.001 24.026,38.024C31.781,38.047 38.049,31.779 38.026,24.024C38.003,16.269 31.697,9.963 23.942,9.94L9.901,9.899Z" />
diff --git a/pdf/pdf-viewer/src/main/res/drawable/shape_find_in_file.xml b/pdf/pdf-viewer/src/main/res/drawable/shape_find_in_file.xml
index 7b756b0..d827d3a 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/shape_find_in_file.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/shape_find_in_file.xml
@@ -17,17 +17,17 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
- <solid android:color="@color/search_background"/>
-
- <padding
- android:left="12dp"
- android:top="12dp"
- android:bottom="12dp"/>
-
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="36dp"
- android:topRightRadius="36dp"/>
+ android:topRightRadius="36dp" />
+
+ <padding
+ android:bottom="12dp"
+ android:left="12dp"
+ android:top="12dp" />
+
+ <solid android:color="?attr/colorSurfaceContainer" />
</shape>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/drawable/shape_oval.xml b/pdf/pdf-viewer/src/main/res/drawable/shape_oval.xml
index e8b5b8e..46f1094 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/shape_oval.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/shape_oval.xml
@@ -15,10 +15,10 @@
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/search_textbox">
+ android:color="?attr/colorSurfaceBright">
<item>
<shape android:shape="oval">
- <solid android:color="@color/search_background" />
+ <solid android:color="?attr/colorSurfaceContainer" />
</shape>
</item>
</ripple>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/drawable/shape_textbox.xml b/pdf/pdf-viewer/src/main/res/drawable/shape_textbox.xml
index 3b07c26..caf6bf9 100644
--- a/pdf/pdf-viewer/src/main/res/drawable/shape_textbox.xml
+++ b/pdf/pdf-viewer/src/main/res/drawable/shape_textbox.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
- <solid android:color="@color/search_textbox"/>
+ <solid android:color="?attr/colorSurfaceBright"/>
<corners
android:radius="28dp"/>
diff --git a/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml b/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml
index 992d5b9..66ab9f0 100644
--- a/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/dialog_password.xml
@@ -21,32 +21,35 @@
android:id="@+id/password_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:padding="24dp"
- >
- <TextView android:id="@+id/label"
- style="@style/Label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/label_password_first"/>
- <EditText android:id="@+id/password"
- style="@style/TextField"
- android:inputType="textPassword"
+ android:padding="24dp">
+
+ <EditText
+ android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/label"
android:layout_alignParentLeft="true"
- android:selectAllOnFocus="true"
+ android:layout_below="@id/label"
android:contentDescription="@string/desc_password"
- android:textCursorDrawable="@drawable/custom_edit_text_cursor"
android:importantForAutofill="no"
+ android:inputType="textPassword"
+ android:selectAllOnFocus="true"
+ android:textCursorDrawable="@drawable/custom_edit_text_cursor"
tools:ignore="LabelFor" />
- <ImageView android:id="@+id/password_alert"
- android:src="@drawable/text_alert"
+
+ <ImageView
+ android:id="@+id/password_alert"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@id/label"
android:layout_alignParentRight="true"
+ android:layout_below="@id/label"
android:contentDescription="@string/desc_password_incorrect"
- android:visibility="gone"
- />
-</RelativeLayout>
\ No newline at end of file
+ android:src="@drawable/text_alert"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/label"
+ android:textColor="?attr/colorPrimary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/label_password_first" />
+</RelativeLayout>
diff --git a/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml b/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml
index 11ae8df..4bc4ceb 100644
--- a/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/fastscroll_handle.xml
@@ -15,11 +15,11 @@
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="52dp"
- android:layout_height="52dp"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
android:background="@drawable/fastscroll_background"
android:elevation="4dp"
android:importantForAccessibility="no"
android:scaleType="center"
android:src="@drawable/drag_indicator"
- android:translationX="18dp" />
\ No newline at end of file
+ android:translationX="8dp" />
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml b/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
index 873c2a5..176e323 100644
--- a/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
@@ -17,7 +17,6 @@
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/search_container"
@@ -33,15 +32,16 @@
android:layout_height="48dp"
android:layout_weight="1"
android:hint="@string/hint_find"
- android:textColor="@color/search_textColor"
- android:textColorHint="@color/search_texthint"
+ android:textColor="?attr/colorOnSurface"
+ android:textColorHint="?attr/colorOutline"
android:paddingLeft="16dp"
android:imeOptions="actionSearch"
android:inputType="textFilter"
android:textSize="20sp"
android:clickable="true"
android:focusable="true"
- android:background="@null">
+ android:background="@null"
+ style="@style/TextAppearance.Material3.TitleMedium">
</androidx.pdf.widget.SearchEditText>
<TextView android:id="@+id/match_status_textview"
@@ -49,7 +49,7 @@
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/query_box"
android:paddingRight="10dp"
- android:textColor="@color/search_count">
+ android:textColor="?attr/colorOnSurfaceVariant">
</TextView>
</LinearLayout>
@@ -59,7 +59,7 @@
android:layout_height="34dp"
android:background="@drawable/shape_oval"
android:src="@drawable/keyboard_up"
- app:tint="@color/search_prev_button"
+ app:tint="?attr/colorOnSurfaceVariant"
android:cropToPadding="true"
android:padding="3dp"
android:scaleType="centerInside"
@@ -72,7 +72,7 @@
android:layout_height="34dp"
android:background="@drawable/shape_oval"
android:src="@drawable/keyboard_down"
- app:tint="@color/search_next_button"
+ app:tint="?attr/colorOnSurfaceVariant"
android:cropToPadding="true"
android:padding="3dp"
android:scaleType="centerInside"
@@ -84,7 +84,7 @@
android:layout_height="34dp"
android:background="@drawable/shape_oval"
android:src="@drawable/close_button"
- app:tint="@color/search_close_button"
+ app:tint="?attr/colorOnSurfaceVariant"
android:cropToPadding="true"
android:padding="5dp"
android:scaleType="centerInside"
diff --git a/pdf/pdf-viewer/src/main/res/layout/loading_animation.xml b/pdf/pdf-viewer/src/main/res/layout/loading_animation.xml
index bb85a0e..fe9036d 100644
--- a/pdf/pdf-viewer/src/main/res/layout/loading_animation.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/loading_animation.xml
@@ -26,7 +26,6 @@
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
- app:trackColor="@color/material_on_background_emphasis_high_type"
- />
+ app:trackColor="?attr/colorSecondaryContainer" />
</LinearLayout>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/loading_view.xml b/pdf/pdf-viewer/src/main/res/layout/loading_view.xml
index 35c28a2..d52f385 100644
--- a/pdf/pdf-viewer/src/main/res/layout/loading_view.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/loading_view.xml
@@ -32,6 +32,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
- android:textColor="@android:color/holo_red_dark"
+ android:textColor= "?attr/colorOnSurface"
+ android:textAppearance="?attr/textAppearanceBodyMedium"
+ android:gravity= "center"
android:visibility="gone"/>
</LinearLayout>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/page_indicator.xml b/pdf/pdf-viewer/src/main/res/layout/page_indicator.xml
index a62c083..6de07dd 100644
--- a/pdf/pdf-viewer/src/main/res/layout/page_indicator.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/page_indicator.xml
@@ -27,7 +27,8 @@
android:gravity="center"
android:paddingLeft="12dp"
android:paddingRight="12dp"
- android:textColor="@color/google_grey"
+ android:textColor="?attr/colorOnSurface"
android:textSize="12sp"
tools:ignore="RtlHardcoded"
- tools:text="3 of 20" />
+ tools:text="3 of 20"
+ style="@style/TextAppearance.Material3.LabelMedium"/>
diff --git a/pdf/pdf-viewer/src/main/res/layout/pdf_view_container.xml b/pdf/pdf-viewer/src/main/res/layout/pdf_view_container.xml
index a5f3248..6f962e1 100644
--- a/pdf/pdf-viewer/src/main/res/layout/pdf_view_container.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/pdf_view_container.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,10 +17,10 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
- <include layout="@layout/file_viewer_pdf"/>
- <include layout="@layout/loading_animation"/>
+ <include layout="@layout/file_viewer_pdf" />
+
+ <include layout="@layout/loading_animation" />
</FrameLayout>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/layout/search.xml b/pdf/pdf-viewer/src/main/res/layout/search.xml
index 712c0d2..a833ba5 100644
--- a/pdf/pdf-viewer/src/main/res/layout/search.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/search.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,14 +14,12 @@
limitations under the License.
-->
-<androidx.pdf.find.FindInFileView
- xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.pdf.find.FindInFileView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="72dp"
+ android:layout_gravity="bottom"
android:background="@drawable/shape_find_in_file"
android:orientation="horizontal"
- android:visibility="gone"
- android:layout_gravity="bottom"
- >
+ android:visibility="gone">
</androidx.pdf.find.FindInFileView>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values-af/strings.xml b/pdf/pdf-viewer/src/main/res/values-af/strings.xml
index 19b1121c..3efa5d2 100644
--- a/pdf/pdf-viewer/src/main/res/values-af/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-af/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Lêertipe"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Hierdie lêer is beskerm"</string>
<string name="label_password_first" msgid="4456258714097111908">"Wagwoord"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Wagwoord verkeerd"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoem <xliff:g id="FIRST">%1$d</xliff:g> persent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Gaan na bladsy <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"bladsy <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Kies teks om opmerking te plaas"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tik op ’n area om opmerkings te maak"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Kanselleer"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Kan nie PDF vertoon nie (<xliff:g id="TITLE">%1$s</xliff:g> is ’n ongeldige formaat)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Kan nie bladsy <xliff:g id="PAGE">%1$d</xliff:g> vertoon nie (lêerfout)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Kan nie annotasiemodus vir hierdie item laai nie."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Skakel: webblad by <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Skakel: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-posadres: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"bladsy <xliff:g id="FIRST">%1$d</xliff:g> tot <xliff:g id="LAST">%2$d</xliff:g> van <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Prent: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Soek in lêer"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Geen passende resultate nie."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Vorige"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Volgende"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Maak toe"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Wysig lêer"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Voer wagwoord in om te ontsluit"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Kon nie die lêer oopmaak nie. Moontlike toestemmingkwessie?"</string>
<string name="page_broken" msgid="2968770793669433462">"Bladsy is vir die PDF-dokument gebreek"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Onvoldoende data om die PDF-dokument te verwerk"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-am/strings.xml b/pdf/pdf-viewer/src/main/res/values-am/strings.xml
index 59e6e04..a2679692 100644
--- a/pdf/pdf-viewer/src/main/res/values-am/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-am/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"የፋይል አይነት"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ይህ ፋይል የተጠበቀ ነው"</string>
<string name="label_password_first" msgid="4456258714097111908">"የይለፍ ቃል"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"የይለፍ ቃል ትክክል አይደለም"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> በመቶ ያጉሉ"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"ወደ ገጽ <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> ይሂዱ"</string>
<string name="desc_page" msgid="5684226167093594168">"ገፅ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"አስተያየትዎን ለማስቀመጥ ጽሑፍን ይምረጡ"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"አስተያየት ለመስጠት አንድ አካባቢ ላይ መታ ያድርጉ"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ይቅር"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ማሳየት አልተቻለም (<xliff:g id="TITLE">%1$s</xliff:g> ልክ ያልኾነ ቅርጸት ያለው ነው)"</string>
<string name="error_on_page" msgid="1592475819957182385">"ገጽ <xliff:g id="PAGE">%1$d</xliff:g>ን ማሳየት አልተቻለም (የፋይል ስሕተት)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ለዚህ ንጥል የማብራሪያ ሁነታን መጫን አልተቻለም።"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"አገናኝ፦ <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> ላይ ያለ ድረ-ገጽ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"አገናኝ፦ <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ኢሜይል፦ <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g> እስከ <xliff:g id="LAST">%2$d</xliff:g> ገጾች ከ<xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ምስል፦ <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ፋይል ውስጥ ያግኙ"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"ምንም ተመሳሳዮች አልተገኙም።"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ቀዳሚ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ቀጣይ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ዝጋ"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ፋይል አርትዕ"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ለመክፈት የይለፍ ቃል ያስገቡ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ፋይሉን መክፈት አልተሳካም። የፈቃድ ችግር ሊሆን ይችላል?"</string>
<string name="page_broken" msgid="2968770793669433462">"ለPDF ሰነዱ ገፅ ተበላሽቷል"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ሰነዱን ለማሰናዳት በቂ ያልሆነ ውሂብ"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ar/strings.xml b/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
index 8f89292..eae5948 100644
--- a/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"نوع الملف"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"هذا الملف محمي"</string>
<string name="label_password_first" msgid="4456258714097111908">"كلمة المرور"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"كلمة المرور غير صحيحة"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"التكبير أو التصغير بنسبة <xliff:g id="FIRST">%1$d</xliff:g> في المئة"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"الانتقال إلى الصفحة <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"الصفحة <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"حدِّد نصًا لكتابة تعليقك"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"انقر على منطقة للتعليق عليها"</string>
- <string name="action_cancel" msgid="5494417739210197522">"إلغاء"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"يتعذَّر عرض ملف PDF (تنسيق الملف \"<xliff:g id="TITLE">%1$s</xliff:g>\" غير صالح)"</string>
<string name="error_on_page" msgid="1592475819957182385">"يتعذَّر عرض الصفحة <xliff:g id="PAGE">%1$d</xliff:g> (خطأ في الملف)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"يتعذَّر تحميل وضع التعليقات التوضيحية لهذا العنصر."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"الرابط: صفحة ويب في <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"الرابط: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"البريد الإلكتروني: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"الصفحات من <xliff:g id="FIRST">%1$d</xliff:g> إلى <xliff:g id="LAST">%2$d</xliff:g> من إجمالي <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"صورة: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"البحث في الملف"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"لم يتم العثور على نتائج مطابِقة."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"السابق"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"التالي"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"إغلاق"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> من أصل <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"تعديل الملف"</string>
<string name="password_not_entered" msgid="8875370870743585303">"يجب إدخال كلمة المرور لفتح القفل"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"تعذّر فتح الملف. هل توجد مشكلة محتملة في الأذونات؟"</string>
<string name="page_broken" msgid="2968770793669433462">"تعذّر تحميل صفحة من مستند PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"البيانات غير كافية لمعالجة مستند PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-as/strings.xml b/pdf/pdf-viewer/src/main/res/values-as/strings.xml
index 3de154e..f9c9ef7 100644
--- a/pdf/pdf-viewer/src/main/res/values-as/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-as/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ফাইলৰ প্রকাৰ"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"এই ফাইলটো সুৰক্ষিত"</string>
<string name="label_password_first" msgid="4456258714097111908">"পাছৱৰ্ড"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"পাছৱৰ্ড ভুল হৈছে"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> শতাংশ জুম কৰক"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"পৃষ্ঠা <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>লৈ যাওক"</string>
<string name="desc_page" msgid="5684226167093594168">"পৃষ্ঠা <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"আপোনাৰ মন্তব্য দিবলৈ পাঠ বাছনি কৰক"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"মন্তব্য দিবলৈ এটুকুৰা ঠাইত টিপক"</string>
- <string name="action_cancel" msgid="5494417739210197522">"বাতিল কৰক"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF দেখুৱাব নোৱাৰি (<xliff:g id="TITLE">%1$s</xliff:g>ৰ ফৰ্মেটটো মান্য নহয়)"</string>
<string name="error_on_page" msgid="1592475819957182385">"পৃষ্ঠা <xliff:g id="PAGE">%1$d</xliff:g> দেখুৱাব নোৱাৰি (ফাইলৰ আঁসোৱাহ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"এই বস্তুটোৰ বাবে এন’টেশ্বন ম’ড ল’ড কৰিব নোৱাৰি।"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"লিংক: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>ত থকা ৱেবপৃষ্ঠা"</string>
<string name="desc_web_link" msgid="2776023299237058419">"লিংক: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ইমেইল: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> খন পৃষ্ঠাৰ <xliff:g id="FIRST">%1$d</xliff:g>ৰ পৰা <xliff:g id="LAST">%2$d</xliff:g>লৈ থকা পৃষ্ঠাসমূহ"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"প্ৰতিচ্ছবি: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ফাইলত বিচাৰক"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"কোনো মিল পোৱা নগ’ল।"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"পূৰ্বৱৰ্তী"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"পৰৱৰ্তী"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"বন্ধ কৰক"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ফাইল সম্পাদনা কৰক"</string>
<string name="password_not_entered" msgid="8875370870743585303">"আনলক কৰিবলৈ পাছৱৰ্ড দিয়ক"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ফাইলটো খুলিব পৰা নগ’ল। সম্ভাব্য অনুমতি সম্পৰ্কীয় সমস্যা?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF নথিৰ বাবে পৃষ্ঠাখন বিসংগতিপূৰ্ণ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF নথিখন প্ৰক্ৰিয়াকৰণ কৰিবলৈ অপৰ্যাপ্ত ডেটা"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-az/strings.xml b/pdf/pdf-viewer/src/main/res/values-az/strings.xml
index 4293921..6ef1751 100644
--- a/pdf/pdf-viewer/src/main/res/values-az/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-az/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Fayl növü"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Bu fayl qorunur"</string>
<string name="label_password_first" msgid="4456258714097111908">"Parol"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Parol səhvdir"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> faiz zum"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> səhifəsinə keçin"</string>
<string name="desc_page" msgid="5684226167093594168">"səhifə <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Şərhi yerləşdirmək üçün mətn seçin"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Şərh yazmaq üçün sahəyə toxunun"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Ləğv edin"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF göstərilmir (<xliff:g id="TITLE">%1$s</xliff:g> yanlış formatdadır)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> saylı səhifə göstərilmir (fayl xətası)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Bu element üçün annotasiya rejimi yüklənmir."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> ünvanında veb-səhifə"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-poçt: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> səhifənin <xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g> səhifələri"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Şəkil: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Faylda tapın"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Uyğunluq tapılmadı."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Əvvəlki"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Növbəti"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Bağlayın"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Faylı redaktə edin"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Kiliddən çıxarmaq üçün parol daxil edin"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Fayl açılmadı. İcazə problemi var?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF sənədi üçün səhifədə xəta var"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF sənədini emal etmək üçün kifayət qədər data yoxdur"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml b/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
index 2723618..f259654 100644
--- a/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tip fajla"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ovaj fajl je zaštićen"</string>
<string name="label_password_first" msgid="4456258714097111908">"Lozinka"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Lozinka je netačna"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"procenat zuma: <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Idi na stranicu <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>. stranica"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Izaberite tekst za postavljanje komentara"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Dodirnite oblast za komentarisanje"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Otkaži"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ne može da se prikaže (<xliff:g id="TITLE">%1$s</xliff:g> ima nevažeći format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Stranica <xliff:g id="PAGE">%1$d</xliff:g> ne može da se prikaže (greška u fajlu)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Učitavanje režima napomena za ovu stavku nije uspelo."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: veb-stranica na <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Imejl: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"stranice <xliff:g id="FIRST">%1$d</xliff:g>. do <xliff:g id="LAST">%2$d</xliff:g>. od <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Slika: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Pronađite u fajlu"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nije pronađeno nijedno podudaranje."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Prethodno"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Dalje"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zatvori"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Izmeni fajl"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Unesite lozinku za otključavanje"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje fajla nije uspelo. Možda postoje problemi sa dozvolom?"</string>
<string name="page_broken" msgid="2968770793669433462">"Neispravna stranica za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nedovoljno podataka za obradu PDF dokumenta"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-be/strings.xml b/pdf/pdf-viewer/src/main/res/values-be/strings.xml
index 179a2e7..c951b2c 100644
--- a/pdf/pdf-viewer/src/main/res/values-be/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-be/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Тып файла"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Гэты файл абаронены"</string>
<string name="label_password_first" msgid="4456258714097111908">"Пароль"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Няправільны пароль"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"маштаб <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"На старонку <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"старонка <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Выберыце тэкст, каб змясціць свой каментарый"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Націсніце там, дзе будзеце каментаваць"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Скасаваць"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Не ўдаецца паказаць PDF-файл \"<xliff:g id="TITLE">%1$s</xliff:g>\" (няправільны фармат)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Немагчыма адлюстраваць старонку <xliff:g id="PAGE">%1$d</xliff:g> (памылка файла)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Не ўдаецца загрузіць рэжым анатацый для гэтага элемента."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Спасылка: вэб-старонка <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Спасылка: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Адрас электроннай пошты: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"старонкі з <xliff:g id="FIRST">%1$d</xliff:g> па <xliff:g id="LAST">%2$d</xliff:g> (усяго <xliff:g id="TOTAL">%3$d</xliff:g>)"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Відарыс: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Знайсці ў файле"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Супадзенні не знойдзены."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Назад"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Далей"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Закрыць"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> з <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Рэдагаваць файл"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Увядзіце пароль для разблакіроўкі"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Не ўдалося адкрыць файл. Магчыма, праблема з дазволам?"</string>
<string name="page_broken" msgid="2968770793669433462">"Старонка дакумента PDF пашкоджана"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Не хапае даных для апрацоўкі дакумента PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bg/strings.xml b/pdf/pdf-viewer/src/main/res/values-bg/strings.xml
index 67b6be7..004be66 100644
--- a/pdf/pdf-viewer/src/main/res/values-bg/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bg/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Файлов тип"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Този файл е защитен"</string>
<string name="label_password_first" msgid="4456258714097111908">"Парола"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Неправилна парола"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"Процент на промяна на мащаба: <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Към страница <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"страница <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Изберете текст, за да поставите коментара си"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Докоснете област, която да коментирате"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Отказ"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF файлът <xliff:g id="TITLE">%1$s</xliff:g> не може да се покаже (форматът е невалиден)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Страница <xliff:g id="PAGE">%1$d</xliff:g> не може да се покаже (грешка във файла)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Режимът за пояснения не може да се зареди за този елемент."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Връзка: уеб страница от <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Връзка: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Имейл адрес: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"страници <xliff:g id="FIRST">%1$d</xliff:g> до <xliff:g id="LAST">%2$d</xliff:g> от <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Изображение: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Търсете във файла"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Няма намерени съответствия."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Назад"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Напред"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Затваряне"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Редактиране на файла"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Въведете паролата, за да отключите"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файлът не бе отворен. Възможно е да има проблем с разрешенията."</string>
<string name="page_broken" msgid="2968770793669433462">"Невалидна страница в PDF документа"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Няма достатъчно данни за обработването на PDF документа"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файлът не може да се отвори"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bn/strings.xml b/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
index 7b5287a..937be09 100644
--- a/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ফাইলের ধরন"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"এই ফাইল সুরক্ষিত আছে"</string>
<string name="label_password_first" msgid="4456258714097111908">"পাসওয়ার্ড"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"পাসওয়ার্ড সঠিক নয়"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> শতাংশ জুম করুন"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> নম্বর পৃষ্ঠায় যান"</string>
<string name="desc_page" msgid="5684226167093594168">"পৃষ্ঠা <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"আপনার কমেন্ট করতে টেক্সট বেছে নিন"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"কমেন্ট করতে কোনও একটি অংশে ট্যাপ করুন"</string>
- <string name="action_cancel" msgid="5494417739210197522">"বাতিল করুন"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"পিডিএফ দেখানো যাবে না (<xliff:g id="TITLE">%1$s</xliff:g> ভুল ফর্ম্যাটে আছে)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> পৃষ্ঠা দেখানো যাবে না (ফাইলে সমস্যা)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"এই আইটেমের জন্য অ্যানোটেশন মোড লোড করা যায়নি।"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"লিঙ্ক: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>-এ ওয়েবপেজ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"লিঙ্ক: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ইমেল: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>টির মধ্যে <xliff:g id="FIRST">%1$d</xliff:g> থেকে <xliff:g id="LAST">%2$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ছবি: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ফাইলে খুঁজুন"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"কোনও মিল খুঁজে পাওয়া যায়নি।"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"পূর্ববর্তী"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"পরবর্তী"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"বন্ধ করুন"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ফাইল এডিট করুন"</string>
<string name="password_not_entered" msgid="8875370870743585303">"আনলক করতে পাসওয়ার্ড লিখুন"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ফাইল খোলা যায়নি। অনুমতি সংক্রান্ত সমস্যার কারণে এটি হতে পারে?"</string>
<string name="page_broken" msgid="2968770793669433462">"পিডিএফ ডকুমেন্টের ক্ষেত্রে পৃষ্ঠা ভেঙে গেছে"</string>
<string name="needs_more_data" msgid="3520133467908240802">"পিডিএফ ডকুমেন্ট প্রসেস করার জন্য যথেষ্ট ডেটা নেই"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bs/strings.xml b/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
index 4944bfa..cd49b7a 100644
--- a/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Vrsta fajla"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Fajl je zaštićen"</string>
<string name="label_password_first" msgid="4456258714097111908">"Lozinka"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Pogrešna lozinka"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zumiranje <xliff:g id="FIRST">%1$d</xliff:g> posto"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Odlazak na <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>. stranicu"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>. stranica"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Odaberite tekst da unesete komentar"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Dodirnite područje koje ćete komentirati"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Otkaži"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Nije moguće prikazati PDF (fajl <xliff:g id="TITLE">%1$s</xliff:g> ima nevažeći format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Nije moguće prikazati <xliff:g id="PAGE">%1$d</xliff:g>. stranicu (greška fajla)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Nije moguće učitati način rada za bilješke za ovu stavku."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: web stranica na <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Adresa e-pošte: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"od <xliff:g id="FIRST">%1$d</xliff:g>. do <xliff:g id="LAST">%2$d</xliff:g>. stranice od <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Slika: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Pronađi u fajlu"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nije pronađeno nijedno podudaranje."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Nazad"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Naprijed"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zatvaranje"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Uredite fajl"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Unesite lozinku da otključate fajl"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje fajla nije uspjelo. Možda postoji problem s odobrenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Stranica je prelomljena za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nema dovoljno podataka za obradu PDF dokumenta"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ca/strings.xml b/pdf/pdf-viewer/src/main/res/values-ca/strings.xml
index b8b2b4c..4f3deac 100644
--- a/pdf/pdf-viewer/src/main/res/values-ca/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ca/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipus de fitxer"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Aquest fitxer està protegit"</string>
<string name="label_password_first" msgid="4456258714097111908">"Contrasenya"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Contrasenya incorrecta"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> per cent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ves a la pàgina <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"pàgina <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecciona text per escriure un comentari"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Toca una zona per comentar-la"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancel·la"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"No es pot mostrar el PDF (<xliff:g id="TITLE">%1$s</xliff:g> no és un format vàlid)"</string>
<string name="error_on_page" msgid="1592475819957182385">"No es pot mostrar la pàgina <xliff:g id="PAGE">%1$d</xliff:g> (error del fitxer)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"No es pot carregar el mode d\'anotació per a aquest element."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Enllaç: pàgina web a <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Enllaç: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Adreça electrònica: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pàgines <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imatge: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Cerca al fitxer"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No s\'han trobat coincidències."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Següent"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Tanca"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edita el fitxer"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Introdueix la contrasenya per desbloquejar-lo"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"No s\'ha pogut obrir el fitxer. És possible que hi hagi un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La pàgina no funciona per al document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Les dades són insuficients per processar el document PDF"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"No es pot obrir el fitxer PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-cs/strings.xml b/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
index 395aa47..cdc4b65 100644
--- a/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Typ souboru"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Tento soubor je chráněn"</string>
<string name="label_password_first" msgid="4456258714097111908">"Heslo"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Nesprávné heslo"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"lupa <xliff:g id="FIRST">%1$d</xliff:g> procent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Přejít na stránku <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"stránka <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Vyberte text k umístění komentáře"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Klepněte na oblast k okomentování"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Zrušit"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF nelze zobrazit (<xliff:g id="TITLE">%1$s</xliff:g> má neplatný formát)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Stránku <xliff:g id="PAGE">%1$d</xliff:g> nelze zobrazit (chyba souboru)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Pro tuto položku nelze načíst režim poznámek."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Odkaz: stránka na webu <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Odkaz: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E‑mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"stránky <xliff:g id="FIRST">%1$d</xliff:g> až <xliff:g id="LAST">%2$d</xliff:g> z <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Obrázek: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Najít v souboru"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nebyly nalezeny žádné shody."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Předchozí"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Další"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zavřít"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Upravit soubor"</string>
<string name="password_not_entered" msgid="8875370870743585303">"K odemknutí zadejte heslo"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Soubor se nepodařilo otevřít. Může se jednat o problém s oprávněním."</string>
<string name="page_broken" msgid="2968770793669433462">"Dokument PDF obsahuje poškozenou stránku"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nedostatek dat ke zpracování dokumentu PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-da/strings.xml b/pdf/pdf-viewer/src/main/res/values-da/strings.xml
index 9b95de8..5bcd02c 100644
--- a/pdf/pdf-viewer/src/main/res/values-da/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-da/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Filtype"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Denne fil er beskyttet"</string>
<string name="label_password_first" msgid="4456258714097111908">"Adgangskode"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Adgangskoden er forkert"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> %%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Gå til side <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"side <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Markér tekst for at indsætte din kommentar"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tryk på et område, du vil kommentere"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Annuller"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF kan ikke vises (<xliff:g id="TITLE">%1$s</xliff:g> er i et ugyldigt format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Side <xliff:g id="PAGE">%1$d</xliff:g> kan ikke vises (filfejl)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Annoteringstilstanden for dette element kan ikke indlæses."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: webside på <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Mailadresse: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"side <xliff:g id="FIRST">%1$d</xliff:g> til <xliff:g id="LAST">%2$d</xliff:g> af <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Billede: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Søg i fil"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Der blev ikke fundet noget match."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Forrige"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Næste"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Luk"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Rediger fil"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Angiv adgangskode for at låse op"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Filen kunne ikke åbnes Mon der er et problem med tilladelserne?"</string>
<string name="page_broken" msgid="2968770793669433462">"Siden er ødelagt for PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Der er ikke nok data til at behandle PDF-dokumentet"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-de/strings.xml b/pdf/pdf-viewer/src/main/res/values-de/strings.xml
index 1ac353e..72b1096 100644
--- a/pdf/pdf-viewer/src/main/res/values-de/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-de/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Dateityp"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Diese Datei ist geschützt"</string>
<string name="label_password_first" msgid="4456258714097111908">"Passwort"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Falsches Passwort"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"Zoom: <xliff:g id="FIRST">%1$d</xliff:g> %%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Gehe zu Seite <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"Seite <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Zum Einfügen des Kommentars Text auswählen"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Auf Bereich tippen, um zu kommentieren"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Abbrechen"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Anzeige von PDF nicht möglich („<xliff:g id="TITLE">%1$s</xliff:g>“ hat ungültiges Dateiformat)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Anzeige von Seite <xliff:g id="PAGE">%1$d</xliff:g> nicht möglich (Dateifehler)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Anmerkungsmodus für dieses Element kann nicht geladen werden."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: Webseite unter <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-Mail-Adresse: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"Seiten <xliff:g id="FIRST">%1$d</xliff:g> bis <xliff:g id="LAST">%2$d</xliff:g> von <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Bild: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"In Datei suchen"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Keine Übereinstimmungen gefunden."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Zurück"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Weiter"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Schließen"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Datei bearbeiten"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Gib zum Entsperren ein Passwort ein"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Datei konnte nicht geöffnet werden. Möglicherweise ein Berechtigungsproblem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Seite für PDF-Dokument ist fehlerhaft"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Keine ausreichenden Daten, um das PDF-Dokument zu verarbeiten"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-el/strings.xml b/pdf/pdf-viewer/src/main/res/values-el/strings.xml
index 74b6dfa..ada33d8 100644
--- a/pdf/pdf-viewer/src/main/res/values-el/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-el/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Τύπος αρχείου"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Αυτό το αρχείο είναι προστατευμένο"</string>
<string name="label_password_first" msgid="4456258714097111908">"Κωδικός πρόσβασης"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Εσφαλμένος κωδικός πρόσβασης"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"εστίαση <xliff:g id="FIRST">%1$d</xliff:g> τοις εκατό"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Μετάβαση στη σελίδα <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"σελίδα <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Επιλέξτε κείμενο για να τοποθετήσετε σχόλιο"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Πατήστε μια περιοχή για να σχολιάσετε"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Ακύρωση"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Δεν είναι δυνατή η προβολή του PDF (μη έγκυρη μορφή του <xliff:g id="TITLE">%1$s</xliff:g>)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Δεν είναι δυνατή η προβολή της σελίδας <xliff:g id="PAGE">%1$d</xliff:g> (σφάλμα αρχείου)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Δεν είναι δυνατή η φόρτωση της λειτουργίας σχολιασμού για αυτό το στοιχείο."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Σύνδεσμος: ιστοσελίδα στη διεύθυνση <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Σύνδεσμος: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Διεύθυνση ηλεκτρονικού ταχυδρομείου: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"σελίδες <xliff:g id="FIRST">%1$d</xliff:g> έως <xliff:g id="LAST">%2$d</xliff:g> από <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Εικόνα: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Εύρεση σε αρχείο"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Δεν εντοπίστηκαν αντιστοιχίσεις."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Προηγούμενο"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Επόμενο"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Κλείσιμο"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Επεξεργασία αρχείου"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Εισαγάγετε τον κωδικό πρόσβασης για ξεκλείδωμα"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Δεν ήταν δυνατό το άνοιγμα του αρχείου. Μήπως υπάρχει κάποιο πρόβλημα με την άδεια;"</string>
<string name="page_broken" msgid="2968770793669433462">"Δεν ήταν δυνατή η φόρτωση του εγγράφου PDF από τη σελίδα"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Μη επαρκή δεδομένα για την επεξεργασία του εγγράφου PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
index 609570b..2f9e726 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"File type"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"This file is protected"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Password incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> per cent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Go to page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Select text to place your comment"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tap an area to comment on it"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancel"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Cannot display PDF (<xliff:g id="TITLE">%1$s</xliff:g> is of invalid format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Cannot display page <xliff:g id="PAGE">%1$d</xliff:g> (file error)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Can\'t load annotation mode for this item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: web page at <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> to <xliff:g id="LAST">%2$d</xliff:g> of <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Find in file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No matches found."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Previous"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Next"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Close"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Enter password to unlock"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rCA/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rCA/strings.xml
index 3be5a4f..66f8a08 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rCA/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rCA/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"File type"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"This file is protected"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Password incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> percent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Go to page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Select text to place your comment"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tap an area to comment on"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancel"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Cannot display PDF (<xliff:g id="TITLE">%1$s</xliff:g> is of invalid format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Cannot display page <xliff:g id="PAGE">%1$d</xliff:g> (file error)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Can\'t load annotation mode for this item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: webpage at <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> to <xliff:g id="LAST">%2$d</xliff:g> of <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Find in file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No matches found."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Previous"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Next"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Close"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Enter password to unlock"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
index 609570b..2f9e726 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"File type"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"This file is protected"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Password incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> per cent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Go to page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Select text to place your comment"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tap an area to comment on it"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancel"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Cannot display PDF (<xliff:g id="TITLE">%1$s</xliff:g> is of invalid format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Cannot display page <xliff:g id="PAGE">%1$d</xliff:g> (file error)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Can\'t load annotation mode for this item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: web page at <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> to <xliff:g id="LAST">%2$d</xliff:g> of <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Find in file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No matches found."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Previous"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Next"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Close"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Enter password to unlock"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
index 609570b..2f9e726 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"File type"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"This file is protected"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Password incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> per cent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Go to page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Select text to place your comment"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tap an area to comment on it"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancel"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Cannot display PDF (<xliff:g id="TITLE">%1$s</xliff:g> is of invalid format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Cannot display page <xliff:g id="PAGE">%1$d</xliff:g> (file error)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Can\'t load annotation mode for this item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: web page at <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> to <xliff:g id="LAST">%2$d</xliff:g> of <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Find in file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No matches found."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Previous"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Next"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Close"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Enter password to unlock"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rXC/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rXC/strings.xml
index c63cb2d..f0cfb06 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rXC/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rXC/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"File type"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"This file is protected"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Password incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> percent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Go to page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Select text to place your comment"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tap an area to comment on"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancel"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Cannot display PDF (<xliff:g id="TITLE">%1$s</xliff:g> is of invalid format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Cannot display page <xliff:g id="PAGE">%1$d</xliff:g> (file error)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Can\'t load annotation mode for this item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: webpage at <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> to <xliff:g id="LAST">%2$d</xliff:g> of <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Find in file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No matches found."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Previous"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Next"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Close"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Enter password to unlock"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml b/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
index dbe8fae..388c071 100644
--- a/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo de archivo"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Este archivo está protegido"</string>
<string name="label_password_first" msgid="4456258714097111908">"Contraseña"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"La contraseña es incorrecta"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> por ciento"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ir a la página <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"página <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecciona texto para colocar tu comentario"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Presiona un área para comentar"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancelar"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"No se puede mostrar el PDF (<xliff:g id="TITLE">%1$s</xliff:g> tiene un formato no válido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"No se puede mostrar la página <xliff:g id="PAGE">%1$d</xliff:g> (error de archivo)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"No se puede cargar el modo de anotación para este elemento."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Vínculo: página web en <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Vínculo: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Correo electrónico: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"páginas <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imagen: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Buscar en el archivo"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No hay coincidencias."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Siguiente"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Cerrar"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editar el archivo"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Ingresa la contraseña para desbloquear"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"No se pudo abrir el archivo. ¿Puede que se deba a un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La página no funciona para el documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"No hay datos suficientes para procesar el documento PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-es/strings.xml b/pdf/pdf-viewer/src/main/res/values-es/strings.xml
index fa7f116..b1aca5f 100644
--- a/pdf/pdf-viewer/src/main/res/values-es/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-es/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo de archivo"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Este archivo está protegido"</string>
<string name="label_password_first" msgid="4456258714097111908">"Contraseña"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Contraseña incorrecta"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> por ciento"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ir a la página <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"página <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecciona el texto para añadir tu comentario"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Toca una zona para dejar un comentario"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancelar"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"No se puede mostrar el PDF (el formato de <xliff:g id="TITLE">%1$s</xliff:g> no es válido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"No se puede mostrar la página <xliff:g id="PAGE">%1$d</xliff:g> (error de archivo)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"No se puede cargar el modo de anotación en este elemento."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Enlace: página web en <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Enlace: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Correo: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"páginas <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imagen: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Buscar en el archivo"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"No se han encontrado coincidencias."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Siguiente"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Cerrar"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editar archivo"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Introduce la contraseña para desbloquear"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"No se ha podido abrir el archivo. ¿Puede que se deba a un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La página no funciona para el documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Datos insuficientes para procesar el documento PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-et/strings.xml b/pdf/pdf-viewer/src/main/res/values-et/strings.xml
index 4b085c6..d640ef5 100644
--- a/pdf/pdf-viewer/src/main/res/values-et/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-et/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Faili tüüp"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Fail on kaitstud"</string>
<string name="label_password_first" msgid="4456258714097111908">"Parool"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Parool on vale"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"suum <xliff:g id="FIRST">%1$d</xliff:g> protsenti"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Mine lehele <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"lk <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Valige tekst, millele kommentaar lisada"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Puudutage ala, millele kommentaar lisada"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Tühista"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF-i ei saa kuvada (<xliff:g id="TITLE">%1$s</xliff:g> on vales vormingus)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Lehekülge <xliff:g id="PAGE">%1$d</xliff:g> ei saa kuvada (viga failis)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Selle üksuse jaoks ei saa märkuste lisamise režiimi laadida."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: veebileht aadressil <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-posti aadress: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"lk <xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g>/<xliff:g id="TOTAL">%3$d</xliff:g>-st"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Pilt: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Otsige failist"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Vasteid ei leitud."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Eelmine"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Järgmine"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Sule"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Faili muutmine"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Avamiseks sisestage parool"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Faili avamine nurjus. Probleem võib olla seotud lubadega."</string>
<string name="page_broken" msgid="2968770793669433462">"Rikutud leht PDF-dokumendis"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF-dokumendi töötlemiseks pole piisavalt andmeid"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-eu/strings.xml b/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
index 458acf0..20770aa 100644
--- a/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Fitxategi mota"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Fitxategi hau babestuta dago"</string>
<string name="label_password_first" msgid="4456258714097111908">"Pasahitza"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Pasahitza okerra da"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zooma ehuneko <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Joan <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>. orrira"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>garren orria"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Iruzkina egiteko, hautatu testua"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Sakatu iruzkindu beharreko eremua"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Utzi"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Ezin da erakutsi PDFa (<xliff:g id="TITLE">%1$s</xliff:g> fitxategiaren formatuak ez du balio)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Ezin da erakutsi <xliff:g id="PAGE">%1$d</xliff:g> orria (fitxategi-errorea)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Ezin da kargatu oharpenen modua elementu honetarako."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Esteka: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> domeinuko web-orria"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Esteka: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Helbide elektronikoa: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g> eta <xliff:g id="LAST">%2$d</xliff:g> bitarteko orriak, guztira <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Irudia: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Bilatu fitxategia"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Ez da aurkitu emaitzarik."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Aurrekoa"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Hurrengoa"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Itxi"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editatu fitxategia"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Idatzi pasahitza desblokeatzeko"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Ezin izan da ireki fitxategia. Agian ez duzu horretarako baimenik?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokumentuaren orria hondatuta dago"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ez dago behar adina daturik PDF dokumentua prozesatzeko"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fa/strings.xml b/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
index 54bf7e7..0669b17 100644
--- a/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"نوع فایل"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"این فایل محافظت شده است"</string>
<string name="label_password_first" msgid="4456258714097111908">"گذرواژه"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"گذرواژه نادرست است"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"بزرگنمایی <xliff:g id="FIRST">%1$d</xliff:g> درصد"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"رفتن به صفحه <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"صفحه <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"نوشتار را برای نظر گذاشتن انتخاب کنید"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"روی قسمتی که میخواهید نظر بگذارید تکضرب بزنید"</string>
- <string name="action_cancel" msgid="5494417739210197522">"لغو کردن"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF نمایش داده نمیشود (قالب <xliff:g id="TITLE">%1$s</xliff:g> نامعتبر است)"</string>
<string name="error_on_page" msgid="1592475819957182385">"صفحه <xliff:g id="PAGE">%1$d</xliff:g> نمایش داده نمیشود (خطای فایل)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"حالت گزارمان برای این مورد بار نمیشود."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"پیوند: صفحه وب در <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"پیوند: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ایمیل: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"صفحههای <xliff:g id="FIRST">%1$d</xliff:g> تا <xliff:g id="LAST">%2$d</xliff:g> از <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"تصویر: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"پیدا کردن در فایل"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"مورد منطبقی یافت نشد."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"قبلی"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"بعدی"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"بستن"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ویرایش فایل"</string>
<string name="password_not_entered" msgid="8875370870743585303">"گذرواژه را برای بازگشایی قفل وارد کنید"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"فایل باز نشد. احتمالاً مشکلی در اجازه وجود دارد؟"</string>
<string name="page_broken" msgid="2968770793669433462">"صفحه سند PDF خراب است"</string>
<string name="needs_more_data" msgid="3520133467908240802">"دادهها برای پردازش سند PDF کافی نیست"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fi/strings.xml b/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
index 0b03cc8e..3518c19 100644
--- a/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tiedostotyyppi"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Tämä tiedosto on suojattu"</string>
<string name="label_password_first" msgid="4456258714097111908">"Salasana"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Väärä salasana"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoomaus <xliff:g id="FIRST">%1$d</xliff:g> prosenttia"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Siirry sivulle <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"sivu <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Valitse teksti, johon haluat lisätä kommentin"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Napauta kommentoitavaa aluetta"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Peruuta"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF:ää ei voi näyttää (<xliff:g id="TITLE">%1$s</xliff:g> on virheellinen muoto)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Sivua <xliff:g id="PAGE">%1$d</xliff:g> ei voi näyttää (tiedostovirhe)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Kohteen muistiinpanotilaa ei voi ladata."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Linkki: verkkosivu osoitteessa <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Linkki: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Sähköpostiosoite: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"sivut <xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g>/<xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Kuva: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Etsi tiedostosta"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Osumia ei löytynyt."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Edellinen"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Seuraava"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Sulje"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Muokkaa tiedostoa"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Poista lukitus lisäämällä salasana"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Tiedoston avaaminen epäonnistui. Mahdollinen lupaan liittyvä ongelma?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF-dokumenttiin liittyvä sivu on rikki"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Riittämätön data PDF-dokumentin käsittelyyn"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml b/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
index ffa53ba..8a25992 100644
--- a/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Type de fichier"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ce fichier est protégé"</string>
<string name="label_password_first" msgid="4456258714097111908">"Mot de passe"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Mot de passe incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> pour cent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Accéder à la page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Sélect. du texte pour placer le commentaire"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Touchez une zone à commenter"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Annuler"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Impossible d\'afficher le PDF (format de <xliff:g id="TITLE">%1$s</xliff:g> non valide)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Impossible d\'afficher la page <xliff:g id="PAGE">%1$d</xliff:g> (erreur de fichier)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Impossible de charger le mode d\'annotation pour cet élément."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Lien : page Web sur <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Lien : <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Adresse de courriel : <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> à <xliff:g id="LAST">%2$d</xliff:g> sur <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image : <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Trouver dans fichier"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Aucune correspondance."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Précédent"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Suivant"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Fermer"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Modifier le fichier"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Entrez le mot de passe pour déverrouiller le fichier"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Échec de l\'ouverture du fichier. Problème d\'autorisation éventuel?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page brisée pour le document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Données insuffisantes pour le traitement du document PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fr/strings.xml b/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
index fd1ee39..dd5fcad 100644
--- a/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Type de fichier"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ce fichier est protégé"</string>
<string name="label_password_first" msgid="4456258714097111908">"Mot de passe"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Mot de passe incorrect"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom : <xliff:g id="FIRST">%1$d</xliff:g> pour cent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Accéder à la page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Sélectionnez le texte à commenter"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Appuyez sur la zone à commenter"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Annuler"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Impossible d\'afficher le PDF (format non valide pour <xliff:g id="TITLE">%1$s</xliff:g>)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Impossible d\'afficher la page <xliff:g id="PAGE">%1$d</xliff:g> (erreur de fichier)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Impossible de charger le mode Annotation pour cet élément."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Lien : page Web du domaine <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Lien : <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Adresse e-mail : <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pages <xliff:g id="FIRST">%1$d</xliff:g> à <xliff:g id="LAST">%2$d</xliff:g> sur <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Image : <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Rechercher dans fichier"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Aucune correspondance."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Précédent"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Suivant"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Fermer"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Modifier le fichier"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Saisissez le mot de passe pour procéder au déverrouillage"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Échec de l\'ouverture du fichier. Problème d\'autorisation possible ?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page non fonctionnelle pour le document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Données insuffisantes pour le traitement du document PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-gl/strings.xml b/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
index 5965cb5..ebe0816 100644
--- a/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo de ficheiro"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Este ficheiro está protexido"</string>
<string name="label_password_first" msgid="4456258714097111908">"Contrasinal"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Contrasinal incorrecto"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom ao <xliff:g id="FIRST">%1$d</xliff:g> por cento"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Vai á páxina <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"páxina <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecciona o texto para engadir un comentario"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Toca a zona na que engadir un comentario"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancelar"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Non se puido mostrar o PDF (<xliff:g id="TITLE">%1$s</xliff:g> ten un formato non válido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Non se puido mostrar a páxina <xliff:g id="PAGE">%1$d</xliff:g> (hai un erro no ficheiro)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Non se puido cargar o modo de anotación para este elemento."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Ligazón: páxina web en <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Ligazón: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Correo electrónico: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"páxinas da <xliff:g id="FIRST">%1$d</xliff:g> á <xliff:g id="LAST">%2$d</xliff:g> dun total de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imaxe: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Busca no ficheiro"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Non se atopou ningunha coincidencia."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Seguinte"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Pechar"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editar o ficheiro"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Introduce o contrasinal para desbloquear o ficheiro"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Produciuse un erro ao abrir o ficheiro. É posible que haxa problemas co permiso?"</string>
<string name="page_broken" msgid="2968770793669433462">"Non funciona a páxina para o documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Os datos non son suficientes para procesar o documento PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-gu/strings.xml b/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
index e8bb6cf..ae95929 100644
--- a/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ફાઇલનો પ્રકાર"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"આ ફાઇલ સંરક્ષિત છે"</string>
<string name="label_password_first" msgid="4456258714097111908">"પાસવર્ડ"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"ખોટો પાસવર્ડ"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> ટકા નાનું-મોટું કરો"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"પેજ <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> પર જાઓ"</string>
<string name="desc_page" msgid="5684226167093594168">"પેજ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"તમારી કૉમેન્ટ શામેલ કરવા ટેક્સ્ટ પસંદ કરો"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"કૉમેન્ટ કરવા માટે, કોઈ ભાગ પર ટૅપ કરો"</string>
- <string name="action_cancel" msgid="5494417739210197522">"રદ કરો"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF બતાવી શકાતી નથી (<xliff:g id="TITLE">%1$s</xliff:g>નું ફૉર્મેટ અમાન્ય છે)"</string>
<string name="error_on_page" msgid="1592475819957182385">"પેજ <xliff:g id="PAGE">%1$d</xliff:g> બતાવી શકાતું નથી (ફાઇલમાં ભૂલ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"આ આઇટમ માટે ટીકાટિપ્પણીનો મોડ લોડ કરી શકાતો નથી."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"લિંક: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> પર વેબપેજ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"લિંક: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ઇમેઇલ: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>માંથી પેજ <xliff:g id="FIRST">%1$d</xliff:g>થી <xliff:g id="LAST">%2$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"છબી: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ફાઇલમાં શોધો"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"કોઈ મેળ મળ્યો નથી."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"પાછળ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"આગળ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"બંધ કરો"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ફાઇલમાં ફેરફાર કરો"</string>
<string name="password_not_entered" msgid="8875370870743585303">"અનલૉક કરવા માટે પાસવર્ડ દાખલ કરો"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ફાઇલ ખોલવામાં નિષ્ફળ રહ્યાં. શું તમારી પાસે આની પરવાનગી નથી?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF દસ્તાવેજ માટે પેજ લોડ થઈ રહ્યું નથી"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF દસ્તાવેજ પર પ્રક્રિયા કરવા માટે પર્યાપ્ત ડેટા નથી"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hi/strings.xml b/pdf/pdf-viewer/src/main/res/values-hi/strings.xml
index 5d44d0d..49f8c1d 100644
--- a/pdf/pdf-viewer/src/main/res/values-hi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hi/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"फ़ाइल टाइप"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"यह फ़ाइल सुरक्षित है"</string>
<string name="label_password_first" msgid="4456258714097111908">"पासवर्ड"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"पासवर्ड गलत है"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> प्रतिशत ज़ूम करें"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> पेज पर जाएं"</string>
<string name="desc_page" msgid="5684226167093594168">"पेज <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"टिप्पणी करने के लिए टेक्स्ट चुनें"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"टिप्पणी करने के लिए किसी जगह पर टैप करें"</string>
- <string name="action_cancel" msgid="5494417739210197522">"रद्द करें"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF फ़ाइल नहीं दिखाई जा सकती (<xliff:g id="TITLE">%1$s</xliff:g> अमान्य फ़ॉर्मैट में है)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> पेज नहीं दिखाया जा सकता (फ़ाइल में गड़बड़ी है)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"इस आइटम के लिए, एनोटेशन मोड लोड नहीं किया जा सका."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"लिंक: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> पर वेबपेज"</string>
<string name="desc_web_link" msgid="2776023299237058419">"लिंक: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ईमेल: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> में से <xliff:g id="FIRST">%1$d</xliff:g> से <xliff:g id="LAST">%2$d</xliff:g> पेज"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"इमेज: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"फ़ाइल में खोजें"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"इससे मिलता-जुलता कोई नतीजा नहीं मिला."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"पीछे जाएं"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"आगे बढ़ें"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"बंद करें"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"फ़ाइल में बदलाव करें"</string>
<string name="password_not_entered" msgid="8875370870743585303">"अनलॉक करने के लिए पासवर्ड डालें"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"फ़ाइल नहीं खोली जा सकी. क्या आपके पास इसकी अनुमति नहीं है?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF दस्तावेज़ के लिए पेज लोड नहीं हो रहा है"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF दस्तावेज़ को प्रोसेस करने के लिए, ज़रूरत के मुताबिक डेटा नहीं है"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF फ़ाइल नहीं खोली जा सकी"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hr/strings.xml b/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
index a0c4be4..7248c6ee 100644
--- a/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Vrsta datoteke"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Datoteka je zaštićena"</string>
<string name="label_password_first" msgid="4456258714097111908">"Zaporka"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Zaporka nije točna"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zumiranje <xliff:g id="FIRST">%1$d</xliff:g> posto"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Idite na stranicu <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"stranica <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Odaberite tekst za postavljanje komentara"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Dodirnite područje koje ćete komentirati"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Odustani"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF se ne može prikazati (format <xliff:g id="TITLE">%1$s</xliff:g> nije važeći)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Stranica <xliff:g id="PAGE">%1$d</xliff:g> ne može se prikazati (pogreška datoteke)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Način za napomene za tu stavku ne može se učitati."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Veza: web-stranica na domeni <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Veza: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-adresa: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"stranice od <xliff:g id="FIRST">%1$d</xliff:g> do <xliff:g id="LAST">%2$d</xliff:g> od ukupno <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Slika: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Pronađi u datoteci"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nisu pronađena podudaranja."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Prethodno"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Sljedeće"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zatvori"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Uređivanje datoteke"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Unesite zaporku za otključavanje"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje datoteke nije uspjelo. Možda postoji problem s dopuštenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Stranica je raščlanjena za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nema dovoljno podataka za obradu PDF dokumenta"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hu/strings.xml b/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
index 41020ca..f63a921 100644
--- a/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Fájltípus"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"A fájl védett"</string>
<string name="label_password_first" msgid="4456258714097111908">"Jelszó"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Helytelen jelszó"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> százalékos nagyítás"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ugrás erre az oldalra: <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>. oldal"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Válassza ki a szöveget a megjegyzéshez"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Koppintson arra a területre, amelyhez megjegyzést szeretne fűzni"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Mégse"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Nem sikerült megjeleníteni a PDF-et (<xliff:g id="TITLE">%1$s</xliff:g> formátuma érvénytelen)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Nem sikerült megjeleníteni a(z) <xliff:g id="PAGE">%1$d</xliff:g> oldalt (fájlhiba)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Az elemhez nem lehet betölteni kommentár módot."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: weboldal helye: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mail-cím: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>/<xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g>. oldal"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Kép: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Keresés a fájlban"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nincs találat."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Előző"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Következő"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Bezárás"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="TOTAL">%2$d</xliff:g>/<xliff:g id="POSITION">%1$d</xliff:g>."</string>
<string name="action_edit" msgid="5882082700509010966">"Fájl szerkesztése"</string>
<string name="password_not_entered" msgid="8875370870743585303">"A feloldáshoz írja be a jelszót"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Nem sikerült megnyitni a fájlt. Engedéllyel kapcsolatos problémáról lehet szó?"</string>
<string name="page_broken" msgid="2968770793669433462">"Az oldal nem tölt be a PDF-dokumentumban"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nem áll rendelkezésre elegendő adat a PDF-dokumentum feldolgozásához"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hy/strings.xml b/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
index 2e482a1..71d24c2 100644
--- a/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Ֆայլի տեսակը"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Այս ֆայլը պաշտպանված է"</string>
<string name="label_password_first" msgid="4456258714097111908">"Գաղտնաբառ"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Գաղտնաբառը սխալ է"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"մասշտաբ՝ <xliff:g id="FIRST">%1$d</xliff:g> տոկոս"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Անցեք <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> էջ"</string>
<string name="desc_page" msgid="5684226167093594168">"էջ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Նշեք տեքստը՝ մեկնաբանություն տեղադրելու համար"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Հպեք՝ մեկնաբանություն ավելացնելու համար"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Չեղարկել"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Հնարավոր չէ ցուցադրել PDF-ը (<xliff:g id="TITLE">%1$s</xliff:g> ֆայլը անվավեր ձևաչափ ունի)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Հնարավոր չէ ցուցադրել էջ <xliff:g id="PAGE">%1$d</xliff:g>-ը (ֆայլի սխալ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Չհաջողվեց բեռնել ծանոթագրության ռեժիմն այս տարրի համար։"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Հղում՝ կայքէջ <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>-ում"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Հղում՝ <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Էլ․ հասցե՝ <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"էջ <xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g>՝ <xliff:g id="TOTAL">%3$d</xliff:g>-ից"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Պատկեր՝ <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Գտեք ֆայլում"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Համընկնումներ չեն հայտնաբերվել։"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Նախորդը"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Հաջորդը"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Փակել"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Փոփոխել ֆայլը"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Մուտքագրեք գաղտնաբառը՝ ապակողպելու համար"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Չհաջողվեց բացել ֆայլը։ Գուցե թույլտվության հետ կապված խնդի՞ր է։"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF փաստաթղթի էջը վնասված է"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ոչ բավարար տվյալներ PDF փաստաթղթի մշակման համար"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-in/strings.xml b/pdf/pdf-viewer/src/main/res/values-in/strings.xml
index 0e9397c..3c9e624 100644
--- a/pdf/pdf-viewer/src/main/res/values-in/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-in/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Jenis file"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"File ini dilindungi"</string>
<string name="label_password_first" msgid="4456258714097111908">"Sandi"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Sandi salah"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> persen"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Buka halaman <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"halaman <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Pilih teks untuk memberikan komentar"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Ketuk area yang ingin dikomentari"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Batal"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Tidak dapat menampilkan PDF (format <xliff:g id="TITLE">%1$s</xliff:g> tidak valid)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Tidak dapat menampilkan halaman <xliff:g id="PAGE">%1$d</xliff:g> (kesalahan file)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Tidak dapat memuat mode anotasi untuk item ini."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: halaman web di <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"halaman <xliff:g id="FIRST">%1$d</xliff:g> sampai <xliff:g id="LAST">%2$d</xliff:g> dari <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Gambar: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Cari dalam file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Tidak ada yang cocok."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Sebelumnya"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Berikutnya"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Tutup"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Masukkan sandi untuk membuka kunci"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Gagal membuka file. Kemungkinan masalah izin?"</string>
<string name="page_broken" msgid="2968770793669433462">"Halaman dokumen PDF rusak"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Data tidak cukup untuk memproses dokumen PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-is/strings.xml b/pdf/pdf-viewer/src/main/res/values-is/strings.xml
index 1a2f9ff..9378e05 100644
--- a/pdf/pdf-viewer/src/main/res/values-is/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-is/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Skráargerð"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Þessi skrá er varin"</string>
<string name="label_password_first" msgid="4456258714097111908">"Aðgangsorð"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Rangt aðgangsorð"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"stækka/minnka <xliff:g id="FIRST">%1$d</xliff:g> prósent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Fara á síðu <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"síða <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Veldu texta til að setja inn athugasemd"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Ýttu á svæði til að færa inn athugasemd"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Hætta við"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Ekki hægt að birta PDF (<xliff:g id="TITLE">%1$s</xliff:g> er á ógildu sniði)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Ekki hægt að birta síðuna <xliff:g id="PAGE">%1$d</xliff:g> (villa í skrá)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Ekki er hægt að hlaða textaskýringastillingu fyrir þetta atriði."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Tengill: vefsíða á <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Tengill: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Netfang: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"síður <xliff:g id="FIRST">%1$d</xliff:g> til <xliff:g id="LAST">%2$d</xliff:g> af <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Mynd: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Leita í skrá"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Ekkert fannst."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Fyrri"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Næsta"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Loka"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Breyta skrá"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Sláðu inn aðgangsorð til að opna"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Ekki tókst að opna skrána. Hugsanlega vandamál tengt heimildum?"</string>
<string name="page_broken" msgid="2968770793669433462">"Síða í PDF-skjali er gölluð"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ekki næg gögn fyrir úrvinnslu á PDF-skjali"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-it/strings.xml b/pdf/pdf-viewer/src/main/res/values-it/strings.xml
index 3025646..89a8d17 100644
--- a/pdf/pdf-viewer/src/main/res/values-it/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-it/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo di file"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Questo file è protetto"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Password errata"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Vai alla pagina <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"pagina <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Seleziona il testo da inserire nel commento"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tocca un\'area per inserire un commento"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Annulla"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Impossibile visualizzare PDF (<xliff:g id="TITLE">%1$s</xliff:g> è in un formato non valido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Impossibile visualizzare la pagina <xliff:g id="PAGE">%1$d</xliff:g> (errore del file)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Impossibile caricare la modalità di annotazione per questo elemento."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: pagina web all\'indirizzo <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pagine da <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> di <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Immagine: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Trova nel file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nessuna corrispondenza."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Indietro"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Avanti"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Chiudi"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Modifica file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Inserisci la password per sbloccare il file"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Impossibile aprire il file. Possibile problema di autorizzazione?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagina inaccessibile per il documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dati insufficienti per l\'elaborazione del documento PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-iw/strings.xml b/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
index 0d21739..3dfd510 100644
--- a/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"סוג הקובץ"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"הקובץ הזה מוגן"</string>
<string name="label_password_first" msgid="4456258714097111908">"סיסמה"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"הסיסמה שגויה"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"שינוי מרחק התצוגה ב-<xliff:g id="FIRST">%1$d</xliff:g> אחוזים"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"מעבר לדף <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"עמוד <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"צריך לבחור את הטקסט לתגובה"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"אפשר להקיש על אזור כדי להגיב עליו"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ביטול"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"אי אפשר להציג קובץ PDF (<xliff:g id="TITLE">%1$s</xliff:g> בפורמט לא תקין)"</string>
<string name="error_on_page" msgid="1592475819957182385">"לא ניתן להציג את הדף <xliff:g id="PAGE">%1$d</xliff:g> (שגיאת קובץ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"אי אפשר לטעון את מצב ההערות בפריט הזה."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"קישור: דף אינטרנט בדומיין <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"קישור: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"אימייל: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"דפים <xliff:g id="FIRST">%1$d</xliff:g> עד <xliff:g id="LAST">%2$d</xliff:g> מתוך <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"תמונה: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"חיפוש בקובץ"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"לא נמצאו התאמות."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"הקודם"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"הבא"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"סגירה"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> מתוך <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"עריכת הקובץ"</string>
<string name="password_not_entered" msgid="8875370870743585303">"צריך להזין סיסמה לביטול הנעילה"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"לא ניתן לפתוח את הקובץ. יכול להיות שיש בעיה בהרשאה."</string>
<string name="page_broken" msgid="2968770793669433462">"קישור מנותק בדף למסמך ה-PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"אין מספיק נתונים כדי לעבד את מסמך ה-PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ja/strings.xml b/pdf/pdf-viewer/src/main/res/values-ja/strings.xml
index c5db90d..a48cc46 100644
--- a/pdf/pdf-viewer/src/main/res/values-ja/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ja/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ファイル形式"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"このファイルは保護されています"</string>
<string name="label_password_first" msgid="4456258714097111908">"パスワード"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"パスワードが正しくありません"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ズーム <xliff:g id="FIRST">%1$d</xliff:g> %%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> ページに移動します"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g> ページ"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"コメントを配置するテキストを選択してください"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"コメント対象の範囲をタップしてください"</string>
- <string name="action_cancel" msgid="5494417739210197522">"キャンセル"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF を表示できません(<xliff:g id="TITLE">%1$s</xliff:g> の形式が無効です)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> ページを表示できません(ファイルエラー)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"このアイテムのアノテーション モードを読み込めません。"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"リンク: ウェブページ(<xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>)"</string>
<string name="desc_web_link" msgid="2776023299237058419">"リンク: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"メールアドレス: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g>~<xliff:g id="LAST">%2$d</xliff:g>/<xliff:g id="TOTAL">%3$d</xliff:g> ページ"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"画像: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ファイル内を検索"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"一致する項目は見つかりませんでした。"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"前へ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"次へ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"閉じる"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ファイルを編集"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ロックを解除するには、パスワードを入力してください"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ファイルを開けませんでした。権限に問題がある可能性はありませんか?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ドキュメントのページが壊れています"</string>
<string name="needs_more_data" msgid="3520133467908240802">"データ不足のため PDF ドキュメントを処理できません"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ファイルを開けません"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ka/strings.xml b/pdf/pdf-viewer/src/main/res/values-ka/strings.xml
index cc30d31..3fd5c9b 100644
--- a/pdf/pdf-viewer/src/main/res/values-ka/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ka/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ფაილის ტიპი"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ეს ფაილი დაცულია"</string>
<string name="label_password_first" msgid="4456258714097111908">"პაროლი"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"პაროლი არასწორია"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"მასშტაბი <xliff:g id="FIRST">%1$d</xliff:g> პროცენტი"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g>-ე გვერდზე გადასვლა"</string>
<string name="desc_page" msgid="5684226167093594168">"გვერდი <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"კომენტარის განსათავსებლად მონიშნეთ ტექსტი"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"შეეხეთ ველს კომენტარის დასატოვებლად"</string>
- <string name="action_cancel" msgid="5494417739210197522">"გაუქმება"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF-ის ჩვენება შეუძლებელია (<xliff:g id="TITLE">%1$s</xliff:g> არასწორ ფორმატშია)"</string>
<string name="error_on_page" msgid="1592475819957182385">"გვერდის ჩვენება შეუძლებელია <xliff:g id="PAGE">%1$d</xliff:g> (ფაილის შეცდომა)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ამ ერთეულისთვის ანოტაციის რეჟიმის ჩატვირთვა არ არის შესაძლებელი."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ბმული: ვებგვერდი <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ბმული: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ელფოსტა: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g> გვერდები <xliff:g id="TOTAL">%3$d</xliff:g>-დან"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"სურათი: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ფაილში ძებნა"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"შესატყვისი ვერ მოიძებნა."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"წინა"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"შემდეგი"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"დახურვა"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ფაილის რედაქტირება"</string>
<string name="password_not_entered" msgid="8875370870743585303">"პაროლის შეყვანა განბლოკვისთვის"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ფაილის გახსნა ვერ მოხერხდა. შესაძლოა ნებართვის პრობლემა იყოს?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF დოკუმენტის გვერდი დაზიანებულია"</string>
<string name="needs_more_data" msgid="3520133467908240802">"მონაცემები არ არის საკმარისი PDF დოკუმენტის დასამუშავებლად"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ფაილის გახსნა ვერ ხერხდება"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-kk/strings.xml b/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
index 84b91ca..5846202 100644
--- a/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Файл түрі"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Бұл файл қорғалған"</string>
<string name="label_password_first" msgid="4456258714097111908">"Құпия сөз"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Құпия сөз қате"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> пайызға масштабтау"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g>-бетке өтіңіз."</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>-бет"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Пікір қалдыру үшін мәтін таңдаңыз."</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Пікір қалдыру үшін аймақты түртіңіз."</string>
- <string name="action_cancel" msgid="5494417739210197522">"Бас тарту"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF файлын көрсету мүмкін емес (<xliff:g id="TITLE">%1$s</xliff:g> форматы жарамсыз)."</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g>-бетті көрсету мүмкін емес (файл қатесі)."</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Бұл элемент үшін aннотация режимін жүктеу мүмкін емес."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Сілтеме: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> веб-беті"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Сілтеме: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Электрондық мекенжай: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"Бет: <xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g>/<xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Сурет: <xliff:g id="ALT_TEXT">%1$s</xliff:g>."</string>
<string name="hint_find" msgid="5385388836603550565">"Файлдан табу"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Ешқандай сәйкестік табылмады."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Алдыңғы"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Келесі"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Жабу"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Файлды өңдеу"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Құлыпты ашу үшін құпия сөзді енгізіңіз."</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Файл ашылмады. Бәлкім, рұқсатқа қатысты бір мәселе бар?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF құжатының беті бұзылған."</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF құжатын өңдеу үшін деректер жеткіліксіз."</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-km/strings.xml b/pdf/pdf-viewer/src/main/res/values-km/strings.xml
index 2e9bf3b..931c43f 100644
--- a/pdf/pdf-viewer/src/main/res/values-km/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-km/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ប្រភេទឯកសារ"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ឯកសារនេះត្រូវបានការពារ"</string>
<string name="label_password_first" msgid="4456258714097111908">"ពាក្យសម្ងាត់"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"ពាក្យសម្ងាត់មិនត្រឹមត្រូវ"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ពង្រីកបង្រួម <xliff:g id="FIRST">%1$d</xliff:g> ភាគរយ"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"ទៅទំព័រទី <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"ទំព័រទី <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"ជ្រើសរើសអក្សរ ដើម្បីផ្ដល់មតិរបស់អ្នក"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"ចុចកន្លែងណាមួយដើម្បីផ្ដល់មតិ"</string>
- <string name="action_cancel" msgid="5494417739210197522">"បោះបង់"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"មិនអាចបង្ហាញ PDF បានទេ (<xliff:g id="TITLE">%1$s</xliff:g> មានទម្រង់មិនត្រឹមត្រូវ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"មិនអាចបង្ហាញទំព័រទី <xliff:g id="PAGE">%1$d</xliff:g> បានទេ (បញ្ហាឯកសារ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"មិនអាចផ្ទុកមុខងារចំណារសម្រាប់ធាតុនេះបានទេ។"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"តំណ៖ ទំព័របណ្ដាញនៅ <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"តំណ៖ <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"អ៊ីមែល៖ <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"ទំព័រទី <xliff:g id="FIRST">%1$d</xliff:g> ដល់ <xliff:g id="LAST">%2$d</xliff:g> នៃ <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"រូបភាព៖ <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ស្វែងរកនៅក្នុងឯកសារ"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"រកមិនឃើញអ្វីដែលត្រូវគ្នាទេ។"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"មុន"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"បន្ទាប់"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"បិទ"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"កែឯកសារ"</string>
<string name="password_not_entered" msgid="8875370870743585303">"បញ្ចូលពាក្យសម្ងាត់ ដើម្បីដោះសោ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"មិនអាចបើកឯកសារនេះបានទេ។ អាចមានបញ្ហានៃការអនុញ្ញាតឬ?"</string>
<string name="page_broken" msgid="2968770793669433462">"ទំព័រមិនដំណើរការសម្រាប់ឯកសារ PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"មានទិន្នន័យមិនគ្រប់គ្រាន់សម្រាប់ដំណើរការឯកសារ PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-kn/strings.xml b/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
index 00aa9bb..fb8b009 100644
--- a/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ಫೈಲ್ ಪ್ರಕಾರ"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ಈ ಫೈಲ್ ಅನ್ನು ಸಂರಕ್ಷಿಸಲಾಗಿದೆ"</string>
<string name="label_password_first" msgid="4456258714097111908">"ಪಾಸ್ವರ್ಡ್"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"ಪಾಸ್ವರ್ಡ್ ತಪ್ಪಾಗಿದೆ"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ಶೇಕಡಾ <xliff:g id="FIRST">%1$d</xliff:g> ರಷ್ಟು ಝೂಮ್ ಮಾಡಿ"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"ಪುಟ <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> ಕ್ಕೆ ತೆರಳಿ"</string>
<string name="desc_page" msgid="5684226167093594168">"ಪುಟ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"ನಿಮ್ಮ ಕಾಮೆಂಟ್ ಅನ್ನು ಇರಿಸಲು ಪಠ್ಯವನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"ಕಾಮೆಂಟ್ ಮಾಡಲು ಪ್ರದೇಶವನ್ನು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ರದ್ದುಮಾಡಿ"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ಅನ್ನು ಪ್ರದರ್ಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ (<xliff:g id="TITLE">%1$s</xliff:g> ಅಮಾನ್ಯವಾದ ಫಾರ್ಮ್ಯಾಟ್ ಆಗಿದೆ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> ಪುಟವನ್ನು ಪ್ರದರ್ಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ (ಫೈಲ್ ದೋಷ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ಈ ಐಟಂಗೆ ಟಿಪ್ಪಣಿಯನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ಲಿಂಕ್: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> ನಲ್ಲಿರುವ ವೆಬ್ಪುಟ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ಲಿಂಕ್: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ಇಮೇಲ್: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g> ನಿಂದ <xliff:g id="LAST">%2$d</xliff:g> ವರೆಗಿನ <xliff:g id="TOTAL">%3$d</xliff:g> ಪುಟಗಳು"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ಚಿತ್ರ: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ಫೈಲ್ನಲ್ಲಿ ಹುಡುಕಿ"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"ಯಾವುದೇ ಹೊಂದಾಣಿಕೆಗಳು ಕಂಡುಬಂದಿಲ್ಲ."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ಹಿಂದಿನದು"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ಮುಂದಿನದು"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ಮುಚ್ಚಿರಿ"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ಫೈಲ್ ಎಡಿಟ್ ಮಾಡಿ"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ಅನ್ಲಾಕ್ ಮಾಡಲು ಪಾಸವರ್ಡ್ ಅನ್ನು ನಮೂದಿಸಿ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ಫೈಲ್ ತೆರೆಯಲು ವಿಫಲವಾಗಿದೆ. ಸಂಭವನೀಯ ಅನುಮತಿ ಸಮಸ್ಯೆ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ಡಾಕ್ಯುಮೆಂಟ್ಗೆ ಸಂಬಂಧಿಸಿದ ಪುಟ ಮುರಿದಿದೆ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ಡಾಕ್ಯುಮೆಂಟ್ ಅನ್ನು ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲು ಸಾಕಷ್ಟು ಡೇಟಾ ಇಲ್ಲ"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ko/strings.xml b/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
index 8189d99..9530691 100644
--- a/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"파일 형식"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"보호된 파일임"</string>
<string name="label_password_first" msgid="4456258714097111908">"비밀번호"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"비밀번호가 잘못됨"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g>%% 확대/축소"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> 페이지로 이동"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>페이지"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"댓글을 달려는 텍스트를 선택하세요."</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"댓글을 달려는 영역을 탭하세요."</string>
- <string name="action_cancel" msgid="5494417739210197522">"취소"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"<xliff:g id="TITLE">%1$s</xliff:g>의 형식이 잘못되어 PDF를 표시할 수 없음"</string>
<string name="error_on_page" msgid="1592475819957182385">"파일 오류로 <xliff:g id="PAGE">%1$d</xliff:g> 페이지를 표시할 수 없음"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"이 항목에 대한 주석 모드를 로드할 수 없습니다."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"링크: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>의 웹페이지"</string>
<string name="desc_web_link" msgid="2776023299237058419">"링크: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"이메일: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>페이지 중 <xliff:g id="FIRST">%1$d</xliff:g>~<xliff:g id="LAST">%2$d</xliff:g>페이지"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"이미지: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"파일에서 찾기"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"일치하는 항목이 없습니다."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"이전"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"다음"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"닫기"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"파일 수정"</string>
<string name="password_not_entered" msgid="8875370870743585303">"잠금 해제하려면 비밀번호 입력"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"파일을 열 수 없습니다. 권한 문제가 있을 수 있나요?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 문서의 페이지가 손상되었습니다."</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF 문서 처리를 위한 데이터가 부족합니다."</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ky/strings.xml b/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
index a0e0558..92e5119 100644
--- a/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Файл түрү"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Бул файл корголгон"</string>
<string name="label_password_first" msgid="4456258714097111908">"Сырсөз"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Сырсөз туура эмес"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> пайыз чоңойтуу/кичирейтүү"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> бетине өтүңүз"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>-бет"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Пикирди жайгаштыруу үчүн текст тандаңыз"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Пикир билдирүү үчүн тийиштүү жерди басыңыз"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Жокко чыгаруу"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF\'ти көрсөтүү мүмкүн эмес (<xliff:g id="TITLE">%1$s</xliff:g> форматы жараксыз)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> бетти көрсөтүү мүмкүн эмес (файл катасы)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Бул нерсе үчүн аннотация түзүү режими жүктөлгөн жок."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Шилтеме: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> веб-баракчасы"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Шилтеме: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Электрондук почта: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> ичинен <xliff:g id="FIRST">%1$d</xliff:g>—<xliff:g id="LAST">%2$d</xliff:g>-беттер"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Сүрөт: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Файлдан издөө"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Эч нерсе табылган жок."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Мурунку"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Кийинки"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Жабуу"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Файлды түзөтүү"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Кулпусун ачуу үчүн сырсөздү териңиз"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Файл ачылган жок. Керектүү уруксаттар жок окшойт."</string>
<string name="page_broken" msgid="2968770793669433462">"PDF документинин барагы бузук"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF документин иштетүү үчүн маалымат жетишсиз"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lo/strings.xml b/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
index 06a320b..baa1c28 100644
--- a/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ປະເພດໄຟລ໌"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ໄຟລ໌ນີ້ຖືກປ້ອງກັນໄວ້"</string>
<string name="label_password_first" msgid="4456258714097111908">"ລະຫັດຜ່ານ"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"ລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ຊູມ <xliff:g id="FIRST">%1$d</xliff:g> ເປີເຊັນ"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"ໄປທີ່ໜ້າ <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"ໜ້າ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"ເລືອກຂໍ້ຄວາມເພື່ອວາງຄຳເຫັນຂອງທ່ານ"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"ແຕະໃສ່ພື້ນທີ່ໃດໜຶ່ງເພື່ອຂຽນຄຳເຫັນໃສ່"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ຍົກເລີກ"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"ບໍ່ສາມາດສະແດງ PDF (<xliff:g id="TITLE">%1$s</xliff:g> ມີຮູບແບບທີ່ບໍ່ຖືກຕ້ອງ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"ບໍ່ສາມາດສະແດງໜ້າ <xliff:g id="PAGE">%1$d</xliff:g> (ໄຟລ໌ຜິດພາດ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ບໍ່ສາມາດໂຫຼດໂໝດການອະທິບາຍຄວາມເຫັນສຳລັບລາຍການນີ້ໄດ້."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ລິ້ງ: ໜ້າເວັບທີ່ <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ລິ້ງ: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ອີເມວ: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"ໜ້າທີ <xliff:g id="FIRST">%1$d</xliff:g> ຫາ <xliff:g id="LAST">%2$d</xliff:g> ຈາກທັງໝົດ <xliff:g id="TOTAL">%3$d</xliff:g> ໜ້າ"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ຮູບ: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ຊອກຫາໃນໄຟລ໌"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"ບໍ່ພົບຂໍ້ມູນທີ່ກົງກັນ."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ກ່ອນໜ້າ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ຕໍ່ໄປ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ປິດ"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ແກ້ໄຂໄຟລ໌"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ໃສ່ລະຫັດເພື່ອປົດລັອກ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ເປີດໄຟລ໌ບໍ່ສຳເລັດ. ອາດເປັນຍ້ອນບັນຫາທາງການອະນຸຍາດບໍ?"</string>
<string name="page_broken" msgid="2968770793669433462">"ໜ້າເສຍຫາຍສໍາລັບເອກະສານ PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"ຂໍ້ມູນບໍ່ພຽງພໍສໍາລັບການປະມວນຜົນເອກະສານ PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lt/strings.xml b/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
index 2d6a562..588ff87 100644
--- a/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Failo tipas"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Šis failas yra apsaugotas"</string>
<string name="label_password_first" msgid="4456258714097111908">"Slaptažodis"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Slaptažodis netinkamas"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"keisti mastelį <xliff:g id="FIRST">%1$d</xliff:g> proc."</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Eiti į <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> puslapį"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g> psl."</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Pasirinkite tekstą, kad pateiktumėte komentarą"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Palieskite komentuotiną sritį"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Atšaukti"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Negalima pateikti PDF („<xliff:g id="TITLE">%1$s</xliff:g>“ netinkamo formato)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Negalima pateikti <xliff:g id="PAGE">%1$d</xliff:g> puslapio (failo klaida)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Nepavyko įkelti šio elemento komentaro režimo."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Nuoroda: tinklalapis adresu <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Nuoroda: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"El. pašto adresas: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g> psl. iš <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Vaizdas: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Rasti failą"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nerasta atitikčių."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Ankstesnis"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Kitas"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Uždaryti"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Redaguoti failą"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Įveskite slaptažodį, kad atrakintumėte"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Nepavyko atidaryti failo. Galima su leidimais susijusi problema?"</string>
<string name="page_broken" msgid="2968770793669433462">"Sugadintas PDF dokumento puslapis"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nepakanka duomenų PDF dokumentui apdoroti"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lv/strings.xml b/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
index 84afd6d..738615d 100644
--- a/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Faila tips"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Šis fails ir aizsargāts"</string>
<string name="label_password_first" msgid="4456258714097111908">"Parole"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Parole nav pareiza"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"tālummaiņa procentos ir <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Doties uz <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>. lapu"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>. lapa"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Atlasiet tekstu, lai pievienotu komentāru"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Pieskarieties komentējamajam apgabalam"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Atcelt"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Nevar parādīt PDF (faila “<xliff:g id="TITLE">%1$s</xliff:g>” formāts nav derīgs)."</string>
<string name="error_on_page" msgid="1592475819957182385">"Nevar parādīt <xliff:g id="PAGE">%1$d</xliff:g>. lapu (faila kļūda)."</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Šim vienumam nevar ielādēt anotēšanas režīmu."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Saite: tīmekļa lapa domēnā <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Saite: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-pasta adrese: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="FIRST">%1$d</xliff:g>.–<xliff:g id="LAST">%2$d</xliff:g>. lapa no <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Attēls: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Meklēt failā"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nav atrasta neviena atbilstība."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Atpakaļ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Tālāk"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Aizvērt"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Rediģēt failu"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Lai atbloķētu, ievadiet paroli."</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Neizdevās atvērt failu. Iespējams, ir radusies problēma ar atļaujām."</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokumenta lapa ir bojāta"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nepietiekams datu apjoms, lai apstrādātu PDF dokumentu"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mk/strings.xml b/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
index 854802a..e6b85c2 100644
--- a/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Вид датотека"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Датотекава е заштитена"</string>
<string name="label_password_first" msgid="4456258714097111908">"Лозинка"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Лозинката е неточна"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"зумирајте <xliff:g id="FIRST">%1$d</xliff:g> проценти"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Одете на страницата <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"страница <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Изберете текст за поставување на коментарот"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Допрете област за која ќе коментирате"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Откажи"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Не може да се прикаже PDF (<xliff:g id="TITLE">%1$s</xliff:g> е со неважечки формат)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Не може да се прикаже страницата <xliff:g id="PAGE">%1$d</xliff:g> (грешка во датотеката)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Не може да се вчита „Режимот за прибелешки“ за ставкава."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Линк: веб-страница на <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Линк: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Е-пошта: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"од страница <xliff:g id="FIRST">%1$d</xliff:g> до <xliff:g id="LAST">%2$d</xliff:g> од <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Слика: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Најдете во датотека"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Не се најдени совпаѓања."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Претходно"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Следно"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Затвори"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Изменете ја датотеката"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Внесете лозинка за да отклучите"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Не можеше да се отвори датотеката. Можеби има проблем со дозволата?"</string>
<string name="page_broken" msgid="2968770793669433462">"Страницата не може да го вчита PDF-документот"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недоволно податоци за обработка на PDF-документот"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ml/strings.xml b/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
index 516f0cc..760a696 100644
--- a/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ഫയല് തരം"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ഈ ഫയൽ പരിരക്ഷിതമാണ്"</string>
<string name="label_password_first" msgid="4456258714097111908">"പാസ്വേഡ്"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"പാസ്വേഡ് തെറ്റാണ്"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> ശതമാനം സൂം ചെയ്യുക"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> പേജിലേക്ക് പോകുക"</string>
<string name="desc_page" msgid="5684226167093594168">"പേജ് <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"നിങ്ങളുടെ കമന്റ് നൽകാൻ ടെക്സ്റ്റ് തിരഞ്ഞെടുക്കുക"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"കമന്റിടേണ്ട ഏരിയ ടാപ്പ് ചെയ്യുക"</string>
- <string name="action_cancel" msgid="5494417739210197522">"റദ്ദാക്കുക"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ദൃശ്യമാക്കാനാവില്ല (<xliff:g id="TITLE">%1$s</xliff:g>, അസാധുവായ ഫോർമാറ്റിലാണ്)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> പേജ് പ്രദർശിപ്പിക്കാനാവില്ല (ഫയൽ പിശക്)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ഈ ഇനത്തിന് അനോട്ടേഷൻ മോഡ് ലോഡ് ചെയ്യാനാകില്ല."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ലിങ്ക്: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> എന്നതിലെ വെബ്പേജ്"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ലിങ്ക്: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ഇമെയിൽ: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>-ൽ <xliff:g id="FIRST">%1$d</xliff:g> മുതൽ <xliff:g id="LAST">%2$d</xliff:g> വരെയുള്ള പേജുകൾ"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ചിത്രം: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ഫയലിൽ കണ്ടെത്തുക"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"പൊരുത്തങ്ങളൊന്നും കണ്ടെത്തിയില്ല."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"മുമ്പത്തേത്"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"അടുത്തത്"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"അടയ്ക്കുക"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ഫയൽ എഡിറ്റ് ചെയ്യുക"</string>
<string name="password_not_entered" msgid="8875370870743585303">"അൺലോക്ക് ചെയ്യാൻ പാസ്വേഡ് നൽകുക"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ഫയൽ തുറക്കാനായില്ല. അനുമതി സംബന്ധിച്ച പ്രശ്നമാകാൻ സാധ്യതയുണ്ടോ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ഡോക്യുമെന്റിനായി പേജ് ലോഡ് ചെയ്യാനായില്ല"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ഡോക്യുമെന്റ് പ്രോസസ് ചെയ്യാൻ മതിയായ ഡാറ്റയില്ല"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mn/strings.xml b/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
index 65ed1dc..9c8f052 100644
--- a/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Файлын төрөл"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Энэ файл хамгаалагдсан"</string>
<string name="label_password_first" msgid="4456258714097111908">"Нууц үг"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Нууц үг буруу байна"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> хувь томруулсан"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g>-р хуудсанд очих"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>-р хуудас"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Сэтгэгдлээ байрлуулахын тулд текст сонгоно уу"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Сэтгэгдэл бичих хэсэг дээр товшино уу"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Цуцлах"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF-г үзүүлэх боломжгүй (<xliff:g id="TITLE">%1$s</xliff:g>-н формат буруу байна)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> хуудсыг үзүүлэх боломжгүй (файлын алдаа)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Энэ зүйлд тэмдэглэгээний горимыг ачаалах боломжгүй."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Холбоос: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> дээрх веб хуудас"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Холбоос: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Имэйл: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"Нийт <xliff:g id="TOTAL">%3$d</xliff:g> хуудасны <xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g>-р хуудаснууд"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Зураг: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Файлаас олох"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Ямар ч таарц олдсонгүй."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Өмнөх"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Дараах"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Хаах"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Файлыг засах"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Түгжээг тайлахын тулд нууц үг оруулна уу"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Файлыг нээж чадсангүй. Зөвшөөрөлтэй холбоотой асуудал байж болох уу?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF баримт бичгийн хуудас эвдэрсэн"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF баримт бичгийг боловсруулахад өгөгдөл хангалтгүй байна"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mr/strings.xml b/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
index ae238c2..40ee1a8 100644
--- a/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"फाइल प्रकार"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ही फाइल संरक्षित आहे"</string>
<string name="label_password_first" msgid="4456258714097111908">"पासवर्ड"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"पासवर्ड चुकीचा आहे"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> टक्के झूम करा"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"पेज <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> वर जा"</string>
<string name="desc_page" msgid="5684226167093594168">"पेज <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"तुमच्या टिप्पणी ठेवण्यासाठी मजकूर निवडा"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"टिप्पणी करायच्या क्षेत्रावर टॅप करा"</string>
- <string name="action_cancel" msgid="5494417739210197522">"रद्द करा"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF दाखवू शकत नाही (<xliff:g id="TITLE">%1$s</xliff:g> चा फॉरमॅट चुकीचा आहे)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> पेज दाखवू शकत नाही (फाइल एरर)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"या आयटमसाठी भाष्य मोड लोड करू शकत नाही."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"लिंक: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> वरील वेबपेज"</string>
<string name="desc_web_link" msgid="2776023299237058419">"लिंक: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ईमेल: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> पैकी पेज <xliff:g id="FIRST">%1$d</xliff:g> ते <xliff:g id="LAST">%2$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"इमेज: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"फाइल शोधा"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"कोणत्याही जुळण्या आढळल्या नाहीत."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"मागील"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"पुढील"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"बंद करा"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"फाइल संपादित करा"</string>
<string name="password_not_entered" msgid="8875370870743585303">"अनलॉक करण्यासाठी पासवर्ड एंटर करा"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"फाइल उघडता आली नाही. परवानगीशी संबंधित संभाव्य समस्या?"</string>
<string name="page_broken" msgid="2968770793669433462">"पीडीएफ दस्तऐवजासाठी पेज खंडित झाले आहे"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF दस्तऐवजावर प्रक्रिया करण्यासाठी डेटा पुरेसा नाही"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ms/strings.xml b/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
index b42f674..f905244 100644
--- a/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Jenis fail"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Fail ini dilindungi"</string>
<string name="label_password_first" msgid="4456258714097111908">"Kata laluan"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Kata laluan salah"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zum <xliff:g id="FIRST">%1$d</xliff:g> peratus"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Akses halaman <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"halaman <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Pilih teks untuk meletakkan ulasan anda"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Ketik bahagian untuk diulas"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Batal"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Tidak dapat memaparkan PDF (format untuk <xliff:g id="TITLE">%1$s</xliff:g> tidak sah)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Tidak dapat memaparkan halaman <xliff:g id="PAGE">%1$d</xliff:g> (ralat fail)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Tidak dapat memuatkan mod anotasi untuk item ini."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Pautan: halaman web pada <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Pautan: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mel: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"halaman <xliff:g id="FIRST">%1$d</xliff:g> hingga <xliff:g id="LAST">%2$d</xliff:g> daripada <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imej: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Temukan dalam fail"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Tiada padanan ditemukan."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Sebelumnya"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Seterusnya"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Tutup"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edit fail"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Masukkan kata laluan untuk membuka kunci"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Gagal membuka fail. Kemungkinan terdapat masalah berkaitan dengan kebenaran?"</string>
<string name="page_broken" msgid="2968770793669433462">"Halaman rosak untuk dokumen PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Data tidak mencukupi untuk memproses dokumen PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-my/strings.xml b/pdf/pdf-viewer/src/main/res/values-my/strings.xml
index ecaf6e9..c288df3 100644
--- a/pdf/pdf-viewer/src/main/res/values-my/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-my/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ဖိုင်အမျိုးအစား"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ဤဖိုင်ကို ကာကွယ်ထားသည်"</string>
<string name="label_password_first" msgid="4456258714097111908">"စကားဝှက်"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"စကားဝှက် မှားနေသည်"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ဇူးမ် <xliff:g id="FIRST">%1$d</xliff:g> ရာခိုင်နှုန်း"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"စာမျက်နှာ <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> သို့"</string>
<string name="desc_page" msgid="5684226167093594168">"စာမျက်နှာ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"သင်၏မှတ်ချက်ထည့်ရန် စာသားကို ရွေးပါ"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"မှတ်ချက်ပေးရန် နေရာကို တို့ပါ"</string>
- <string name="action_cancel" msgid="5494417739210197522">"မလုပ်တော့"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ကို ပြ၍မရပါ (<xliff:g id="TITLE">%1$s</xliff:g> သည် မမှန်ကန်သော ဖော်မက်ဖြစ်သည်)"</string>
<string name="error_on_page" msgid="1592475819957182385">"စာမျက်နှာ <xliff:g id="PAGE">%1$d</xliff:g> ကို ပြ၍မရပါ (ဖိုင်အမှား)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ဤအကြောင်းအရာအတွက် မှတ်ချက်မုဒ် ဖွင့်၍မရပါ။"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"လင့်ခ်- <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> ရှိ ဝဘ်စာမျက်နှာ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"လင့်ခ်- <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"အီးမေးလ်- <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"စာမျက်နှာ <xliff:g id="TOTAL">%3$d</xliff:g> အနက် <xliff:g id="FIRST">%1$d</xliff:g> မှ <xliff:g id="LAST">%2$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ပုံ- <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ဖိုင်တွင် ရှာရန်"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"ကိုက်ညီမှု မတွေ့ပါ။"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ယခင်"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ရှေ့သို့"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ပိတ်ရန်"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ဖိုင် တည်းဖြတ်ရန်"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ဖွင့်ရန် စကားဝှက်ထည့်ပါ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ဖိုင်ကို ဖွင့်၍မရလိုက်ပါ။ ခွင့်ပြုချက် ပြဿနာ ဖြစ်နိုင်လား။"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF မှတ်တမ်းအတွက် စာမျက်နှာ ပျက်နေသည်"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF မှတ်တမ်း လုပ်ဆောင်ရန်အတွက် ဒေတာ မလုံလောက်ပါ"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-nb/strings.xml b/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
index 9bb458d..06e5547 100644
--- a/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Filtype"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Denne filen er beskyttet"</string>
<string name="label_password_first" msgid="4456258714097111908">"Passord"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Passordet er feil"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> prosent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Gå til side <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"side <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Merk tekst for å sette inn kommentaren din"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Trykk på et område du vil kommentere"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Avbryt"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Kan ikke vise PDF-filen (<xliff:g id="TITLE">%1$s</xliff:g> har ugyldig format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Kan ikke vise side <xliff:g id="PAGE">%1$d</xliff:g> (filfeil)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Kan ikke laste inn annoteringsmodus for dette elementet."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: nettside på <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-postadresse: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"side <xliff:g id="FIRST">%1$d</xliff:g> til <xliff:g id="LAST">%2$d</xliff:g> av <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Bilde: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Finn i filen"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Fant ingen treff."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Forrige"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Neste"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Lukk"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Endre filen"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Skriv inn passordet for å låse opp"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Kunne ikke åpne filen. Kan det være et problem med tillatelser?"</string>
<string name="page_broken" msgid="2968770793669433462">"Siden er ødelagt for PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Det er utilstrekkelige data for behandling av PDF-dokumentet"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ne/strings.xml b/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
index 346ef60..d847ed0c 100644
--- a/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"फाइलको प्रकार"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"यो फाइलमा पासवर्ड राखिएको छ"</string>
<string name="label_password_first" msgid="4456258714097111908">"पासवर्ड"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"पासवर्ड मिलेन"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> प्रतिशत जुम गर्नुहोस्"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"पेज <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> मा जानुहोस्"</string>
<string name="desc_page" msgid="5684226167093594168">"पेज <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"आफ्नो कमेन्ट गर्न टेक्स्ट चयन गर्नुहोस्"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"कमेन्ट गर्नु पर्ने क्षेत्रमा ट्याप गर्नुहोस्"</string>
- <string name="action_cancel" msgid="5494417739210197522">"रद्द गर्नुहोस्"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF देखाउन मिल्दैन (<xliff:g id="TITLE">%1$s</xliff:g> को फर्म्याट अवैध छ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"पेज <xliff:g id="PAGE">%1$d</xliff:g> देखाउन मिल्दैन (फाइलसम्बन्धी त्रुटि)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"यो वस्तुमा एनोटेसन मोड लोड गर्न मिल्दैन।"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"लिंक: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> मा भएको वेबपेज"</string>
<string name="desc_web_link" msgid="2776023299237058419">"लिंक: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"इमेल: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> मध्ये <xliff:g id="LAST">%2$d</xliff:g> देखि <xliff:g id="FIRST">%1$d</xliff:g> पेजहरू"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"फोटो: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"फाइलमा खोज्नुहोस्"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"मिल्दोजुल्दो परिणाम भेटिएन।"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"अघिल्लो"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"अर्को"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"बन्द गर्नुहोस्"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"फाइल सम्पादन गर्नुहोस्"</string>
<string name="password_not_entered" msgid="8875370870743585303">"अनलक गर्न पासवर्ड हाल्नुहोस्"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"फाइल खोल्न सकिएन। तपाईंसँग यो फाइल खोल्ने अनुमति छैन?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF डकुमेन्टको पेज लोड गर्न सकिएन"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF डकुमेन्ट प्रोसेस गर्न पर्याप्त जानकारी छैन"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-night-v34/colors.xml b/pdf/pdf-viewer/src/main/res/values-night-v34/colors.xml
deleted file mode 100644
index 2b2a21c..0000000
--- a/pdf/pdf-viewer/src/main/res/values-night-v34/colors.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
- Copyright 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources>
-
- <color name="google_blue">#ff1a73e8</color>
- <color name="google_white">#ffffffff</color>
- <color name="google_grey">#ff3c4043</color>
- <color name="text_default">#666</color>
- <color name="text_error">#da4336</color>
- <color name="selection_handles">#00aadd</color>
- <color name="search_background">@android:color/system_surface_container_dark</color>
- <color name="search_textbox">@android:color/system_surface_bright_dark</color>
- <color name="search_texthint">@android:color/system_on_surface_variant_dark</color>
- <color name="search_textColor">@android:color/system_on_surface_dark</color>
- <color name="search_count">@android:color/system_on_surface_variant_dark</color>
- <color name="search_prev_button">@android:color/system_on_surface_variant_dark</color>
- <color name="search_next_button">@android:color/system_on_surface_variant_dark</color>
- <color name="search_close_button">@android:color/system_on_surface_variant_dark</color>
-
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values-night/colors.xml b/pdf/pdf-viewer/src/main/res/values-night/colors.xml
index 3e27f46..ce96df2 100644
--- a/pdf/pdf-viewer/src/main/res/values-night/colors.xml
+++ b/pdf/pdf-viewer/src/main/res/values-night/colors.xml
@@ -1,5 +1,5 @@
-<!--
- Copyright 2023 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,21 +15,7 @@
-->
<resources>
-
- <color name="google_blue">#ff1a73e8</color>
- <color name="google_white">#ffffffff</color>
- <color name="google_grey">#ff3c4043</color>
- <color name="text_default">#666</color>
- <color name="text_error">#da4336</color>
- <color name="selection_handles">#00aadd</color>
- <color name="search_background">#241F17</color>
- <color name="search_textbox">#3E382F</color>
- <color name="search_texthint">#EBE1D4</color>
- <color name="search_textColor">#EBE1D4</color>
- <color name="search_count">#D1C5B4</color>
- <color name="search_prev_button">#D1C5B4</color>
- <color name="search_next_button">#D1C5B4</color>
- <color name="search_close_button">#D1C5B4</color>
-
-
-</resources>
\ No newline at end of file
+ <color name="pdf_viewer_color_primary">@color/m3_sys_color_dark_primary</color>
+ <color name="pdf_viewer_color_on_surface">@color/m3_sys_color_dark_on_surface</color>
+ <color name="pdf_viewer_color_on_error">@color/m3_sys_color_dark_on_error</color>
+</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-nl/strings.xml b/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
index a8a14e9..5ceea0c 100644
--- a/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Bestandstype"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Dit bestand is beveiligd"</string>
<string name="label_password_first" msgid="4456258714097111908">"Wachtwoord"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Wachtwoord is onjuist"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom <xliff:g id="FIRST">%1$d</xliff:g> procent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ga naar pagina <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"pagina <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecteer tekst om je reactie te plaatsen"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tik op een gedeelte waarop je wilt reageren"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Annuleren"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Kan pdf niet weergeven (indeling van <xliff:g id="TITLE">%1$s</xliff:g> is ongeldig)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Kan pagina <xliff:g id="PAGE">%1$d</xliff:g> niet weergeven (bestandsfout)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Kan de annotatiemodus voor dit item niet laden."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: webpagina op <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"pagina\'s <xliff:g id="FIRST">%1$d</xliff:g> tot en met <xliff:g id="LAST">%2$d</xliff:g> van <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Afbeelding: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Zoeken in bestand"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Geen overeenkomsten gevonden."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Vorige"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Volgende"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Sluiten"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Bestand bewerken"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Voer het wachtwoord in om te ontgrendelen"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Kan het bestand niet openen. Mogelijk rechtenprobleem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagina van het pdf-document kan niet worden geladen"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Onvoldoende gegevens om het pdf-document te verwerken"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-or/strings.xml b/pdf/pdf-viewer/src/main/res/values-or/strings.xml
index 26a54de..66b7f68 100644
--- a/pdf/pdf-viewer/src/main/res/values-or/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-or/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ଫାଇଲ୍ର ପ୍ରକାର"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ଏହି ଫାଇଲ ସୁରକ୍ଷିତ ହୋଇଛି"</string>
<string name="label_password_first" msgid="4456258714097111908">"ପାସୱାର୍ଡ"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"ପାସୱାର୍ଡ ଭୁଲ ଅଛି"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ଜୁମ <xliff:g id="FIRST">%1$d</xliff:g> ଶତକଡ଼ା"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ"</string>
<string name="desc_page" msgid="5684226167093594168">"ପୃଷ୍ଠା <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"ଆପଣଙ୍କର ମନ୍ତବ୍ୟ ଦେବା ପାଇଁ ଟେକ୍ସଟ ଚୟନ କରନ୍ତୁ"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"ମନ୍ତବ୍ୟ ଦେବା ପାଇଁ ଏକ ଏରିଆରେ ଟାପ କରନ୍ତୁ"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ବାତିଲ କରନ୍ତୁ"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ଡିସପ୍ଲେ ହୋଇପାରିବ ନାହିଁ (<xliff:g id="TITLE">%1$s</xliff:g>ର ଫର୍ମାଟ ଅବୈଧ ଅଟେ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> ପୃଷ୍ଠା ଡିସପ୍ଲେ ହୋଇପାରିବ ନାହିଁ (ଫାଇଲରେ ତ୍ରୁଟି)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ଏହି ଆଇଟମ ପାଇଁ ଏନୋଟେସନ ମୋଡ ଲୋଡ କରାଯାଇପାରିବ ନାହିଁ।"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ଲିଙ୍କ: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>ରେ ଥିବା ୱେବପୃଷ୍ଠା"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ଲିଙ୍କ: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ଇମେଲ୍: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>ର <xliff:g id="FIRST">%1$d</xliff:g>ରୁ <xliff:g id="LAST">%2$d</xliff:g> ପୃଷ୍ଠା"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ଇମେଜ: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ଫାଇଲରେ ଖୋଜନ୍ତୁ"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"କୌଣସି ମେଳ ମିଳୁ ନାହିଁ।"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ପୂର୍ବବର୍ତ୍ତୀ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ପରବର୍ତ୍ତୀ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ଫାଇଲକୁ ଏଡିଟ କରନ୍ତୁ"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ଅନଲକ କରିବା ପାଇଁ ପାସୱାର୍ଡ ଲେଖନ୍ତୁ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ଫାଇଲ ଖୋଲିବାରେ ବିଫଳ ହୋଇଛି। ସମ୍ଭାବ୍ୟ ଅନୁମତି ସମସ୍ୟା ଅଛି?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ଡକ୍ୟୁମେଣ୍ଟ ପାଇଁ ପୃଷ୍ଠା ବିଭାଜିତ ହୋଇଛି"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ଡକ୍ୟୁମେଣ୍ଟ ପ୍ରକ୍ରିୟାକରଣ ପାଇଁ ପର୍ଯ୍ୟାପ୍ତ ଡାଟା ନାହିଁ"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pa/strings.xml b/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
index 876f5ae..8d4c105 100644
--- a/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ਫ਼ਾਈਲ ਦੀ ਕਿਸਮ"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ਇਹ ਫ਼ਾਈਲ ਸੁਰੱਖਿਅਤ ਹੈ"</string>
<string name="label_password_first" msgid="4456258714097111908">"ਪਾਸਵਰਡ"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"ਪਾਸਵਰਡ ਗਲਤ ਹੈ"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ਜ਼ੂਮ <xliff:g id="FIRST">%1$d</xliff:g> ਫ਼ੀਸਦ"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> ਪੰਨੇ \'ਤੇ ਜਾਓ"</string>
<string name="desc_page" msgid="5684226167093594168">"ਪੰਨਾ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"ਆਪਣੀ ਟਿੱਪਣੀ ਕਰਨ ਲਈ ਲਿਖਤ ਨੂੰ ਚੁਣੋ"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"ਟਿੱਪਣੀ ਕਰਨ ਲਈ ਕਿਸੇ ਖੇਤਰ \'ਤੇ ਟੈਪ ਕਰੋ"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ਰੱਦ ਕਰੋ"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ਨੂੰ ਦਿਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ (<xliff:g id="TITLE">%1$s</xliff:g> ਅਵੈਧ ਫਾਰਮੈਟ ਦਾ ਹੈ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g> ਪੰਨੇ ਨੂੰ ਦਿਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ (ਫ਼ਾਈਲ ਗੜਬੜ)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ਇਸ ਆਈਟਮ ਲਈ ਐਨੋਟੇਸ਼ਨ ਮੋਡ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ।"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ਲਿੰਕ: ਵੈੱਬ-ਪੰਨਾ <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> \'ਤੇ ਹੈ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ਲਿੰਕ: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ਈਮੇਲ: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"ਕੁੱਲ <xliff:g id="TOTAL">%3$d</xliff:g> ਵਿੱਚੋਂ <xliff:g id="FIRST">%1$d</xliff:g> ਤੋਂ <xliff:g id="LAST">%2$d</xliff:g> ਤੱਕ ਪੰਨੇ"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ਚਿੱਤਰ: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ਫ਼ਾਈਲ ਵਿੱਚ ਲੱਭੋ"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"ਕੋਈ ਮੇਲ ਨਹੀਂ ਮਿਲਿਆ।"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ਪਿੱਛੇ"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ਅੱਗੇ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ਬੰਦ ਕਰੋ"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ਫ਼ਾਈਲ ਦਾ ਸੰਪਾਦਨ ਕਰੋ"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ਅਣਲਾਕ ਕਰਨ ਲਈ ਪਾਸਵਰਡ ਦਾਖਲ ਕਰੋ"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ਫ਼ਾਈਲ ਨੂੰ ਖੋਲ੍ਹਣਾ ਅਸਫਲ ਰਿਹਾ। ਕੀ ਸੰਭਵ ਇਜਾਜ਼ਤ ਸੰਬੰਧੀ ਸਮੱਸਿਆ ਹੈ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ਦਸਤਾਵੇਜ਼ ਲਈ ਪੰਨਾ ਲੋਡ ਨਹੀਂ ਹੋ ਰਿਹਾ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ਦਸਤਾਵੇਜ਼ \'ਤੇ ਪ੍ਰਕਿਰਿਆ ਕਰਨ ਲਈ ਲੋੜੀਂਦਾ ਡਾਟਾ ਨਹੀਂ ਹੈ"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pl/strings.xml b/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
index f791b6d..6583cb5 100644
--- a/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Typ pliku"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ten plik jest chroniony"</string>
<string name="label_password_first" msgid="4456258714097111908">"Hasło"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Nieprawidłowe hasło"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"powiększenie <xliff:g id="FIRST">%1$d</xliff:g> procent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Idź do strony <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"strona <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Wybierz tekst, który chcesz skomentować"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Kliknij obszar, który chcesz skomentować"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Anuluj"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Nie można wyświetlić PDF-a (<xliff:g id="TITLE">%1$s</xliff:g> ma zły format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Nie można wyświetlić strony <xliff:g id="PAGE">%1$d</xliff:g> (błąd pliku)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Nie można wczytać trybu adnotacji dla tego elementu."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: strona internetowa w domenie <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"strony od <xliff:g id="FIRST">%1$d</xliff:g> do <xliff:g id="LAST">%2$d</xliff:g> z <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Obraz: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Znajdź w pliku"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Brak wyników."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Wstecz"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Dalej"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zamknij"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Edytuj plik"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Podaj hasło, aby odblokować"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Nie udało się otworzyć pliku. Może to przez problem z uprawnieniami?"</string>
<string name="page_broken" msgid="2968770793669433462">"Strona w dokumencie PDF jest uszkodzona"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Brak wystarczającej ilości danych do przetworzenia dokumentu PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
index 7e0ddc0..e8beb95 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo de arquivo"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Este arquivo está protegido"</string>
<string name="label_password_first" msgid="4456258714097111908">"Senha"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Senha incorreta"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom de <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ir para a página <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"página <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecione o texto para inserir seu comentário"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Toque em uma área para comentar"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancelar"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Não é possível mostrar o PDF (<xliff:g id="TITLE">%1$s</xliff:g>: formato inválido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Não é possível mostrar a página <xliff:g id="PAGE">%1$d</xliff:g> (erro de arquivo)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Não foi possível carregar o modo de anotação deste item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: página da Web de <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"páginas <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imagem: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Localizar no arquivo"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nenhum resultado encontrado."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Próxima"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Fechar"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editar arquivo"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Digite a senha para desbloquear"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o arquivo. Possível problema de permissão?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página do documento PDF corrompida"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processamento do documento PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt-rPT/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt-rPT/strings.xml
index 53c34e4..2a1a80a 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt-rPT/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt-rPT/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo de ficheiro"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Este ficheiro está protegido"</string>
<string name="label_password_first" msgid="4456258714097111908">"Palavra-passe"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Palavra-passe incorreta"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom a <xliff:g id="FIRST">%1$d</xliff:g> por cento"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ir para a página <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"página <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecione o texto para adicionar o comentário"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Toque numa área para comentar"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancelar"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Não é possível apresentar o PDF (<xliff:g id="TITLE">%1$s</xliff:g> tem um formato inválido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Não é possível apresentar a página <xliff:g id="PAGE">%1$d</xliff:g> (erro de ficheiro)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Não é possível carregar o modo de anotação para este item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: página Web em <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"páginas <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imagem: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Procure no ficheiro"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Sem correspondências."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Seguinte"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Fechar"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editar ficheiro"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Introduza a palavra-passe para desbloquear"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o ficheiro. Possível problema de autorização?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página danificada para o documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processar o documento PDF"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Não é possível abrir o ficheiro PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
index 7e0ddc0..e8beb95 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tipo de arquivo"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Este arquivo está protegido"</string>
<string name="label_password_first" msgid="4456258714097111908">"Senha"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Senha incorreta"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom de <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Ir para a página <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"página <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selecione o texto para inserir seu comentário"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Toque em uma área para comentar"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Cancelar"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Não é possível mostrar o PDF (<xliff:g id="TITLE">%1$s</xliff:g>: formato inválido)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Não é possível mostrar a página <xliff:g id="PAGE">%1$d</xliff:g> (erro de arquivo)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Não foi possível carregar o modo de anotação deste item."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: página da Web de <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"páginas <xliff:g id="FIRST">%1$d</xliff:g> a <xliff:g id="LAST">%2$d</xliff:g> de <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imagem: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Localizar no arquivo"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nenhum resultado encontrado."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Anterior"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Próxima"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Fechar"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editar arquivo"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Digite a senha para desbloquear"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o arquivo. Possível problema de permissão?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página do documento PDF corrompida"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processamento do documento PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ro/strings.xml b/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
index 06344cf..ef6b73d 100644
--- a/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Tip de fișier"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Acest fișier este protejat"</string>
<string name="label_password_first" msgid="4456258714097111908">"Parolă"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Parolă incorectă"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zoom de <xliff:g id="FIRST">%1$d</xliff:g> %%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Accesează pagina <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"pagina <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Selectează textul pentru a comenta"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Atinge o zonă pentru a comenta"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Anulează"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Nu se poate afișa ca PDF (<xliff:g id="TITLE">%1$s</xliff:g> are un format nevalid)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Nu se poate afișa pagina <xliff:g id="PAGE">%1$d</xliff:g> (eroare de fișier)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Nu se poate încărca modul de adnotare pentru acest element."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: pagina web de la <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"paginile <xliff:g id="FIRST">%1$d</xliff:g> – <xliff:g id="LAST">%2$d</xliff:g> din <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imagine: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Găsește în fișier"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nicio potrivire găsită."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Înapoi"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Înainte"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Închide"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Editează fișierul"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Introdu parola pentru a debloca"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Nu s-a putut deschide fișierul. Există vreo problemă cu permisiunile?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagină deteriorată pentru documentul PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Date insuficiente pentru procesarea documentului PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ru/strings.xml b/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
index 7f1fe6b..665e7cb 100644
--- a/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Тип файла"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Файл защищен паролем"</string>
<string name="label_password_first" msgid="4456258714097111908">"Пароль"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Неверный пароль"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"масштаб: <xliff:g id="FIRST">%1$d</xliff:g> %%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Перейти на страницу <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"страница <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Чтобы добавить комментарий, выделите текст."</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Чтобы добавить комментарий, коснитесь области."</string>
- <string name="action_cancel" msgid="5494417739210197522">"Отмена"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Недопустимый формат файла \"<xliff:g id="TITLE">%1$s</xliff:g>\"."</string>
<string name="error_on_page" msgid="1592475819957182385">"Невозможно показать страницу <xliff:g id="PAGE">%1$d</xliff:g> (ошибка файла)."</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Не удалось загрузить режим заметок для этого объекта."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Ссылка: страница на сайте <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Ссылка: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Адрес электронной почты: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"страницы <xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g> из <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Изображение: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Найти в файле"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Ничего не найдено."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Назад"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Далее"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Закрыть"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> из <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Редактировать файл"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Введите пароль для разблокировки."</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Не удалось открыть файл. Возможно, нет необходимых разрешений."</string>
<string name="page_broken" msgid="2968770793669433462">"Страница документа PDF повреждена"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недостаточно данных для обработки документа PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-si/strings.xml b/pdf/pdf-viewer/src/main/res/values-si/strings.xml
index 54c42c3..dc5d075 100644
--- a/pdf/pdf-viewer/src/main/res/values-si/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-si/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ගොනු වර්ගය"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"මෙම ගොනුව ආරක්ෂා කර ඇත"</string>
<string name="label_password_first" msgid="4456258714097111908">"මුරපදය"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"මුරපදය වැරදියි"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"විශාලනය සියයට <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> වෙනි පිටුවට යන්න"</string>
<string name="desc_page" msgid="5684226167093594168">"පිටුව <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"ඔබේ අදහස තැබීමට පෙළ තෝරන්න"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"අදහස් දැක්වීමට ප්රදේශයක් තට්ටු කරන්න"</string>
- <string name="action_cancel" msgid="5494417739210197522">"අවලංගු කරන්න"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF සංදර්ශනය කළ නොහැක (<xliff:g id="TITLE">%1$s</xliff:g> අවලංගු ආකෘතියකි)"</string>
<string name="error_on_page" msgid="1592475819957182385">"පිටුව සංදර්ශනය කළ නොහැක <xliff:g id="PAGE">%1$d</xliff:g> (ගොනු දෝෂය)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"මෙම අයිතමය සඳහා අනුසටහන් ප්රකාරය පූරණය කළ නොහැක."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"සබැඳිය: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> හි දී වෙබ් පිටුව"</string>
<string name="desc_web_link" msgid="2776023299237058419">"සබැඳිය: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ඉ-තැපෑල: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"පිටු <xliff:g id="TOTAL">%3$d</xliff:g>න් <xliff:g id="FIRST">%1$d</xliff:g> සිට <xliff:g id="LAST">%2$d</xliff:g> දක්වා"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"රූපය: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ගොනුව සොයා ගන්න"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"කිසි ගැළපීමක් හමු නොවිය."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"පෙර"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"මීළඟ"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"වසන්න"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ගොනුව සංස්කරණ කරන්න"</string>
<string name="password_not_entered" msgid="8875370870743585303">"අගුලු හැරීමට මුරපදය ඇතුළත් කරන්න"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ගොනුව විවෘත කිරීමට අසමත් විය. අවසර ගැටලුවක් විය හැකි ද?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ලේඛනය සඳහා පිටුව හානි වී ඇත"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ලේඛනය සැකසීම සඳහා ප්රමාණවත් දත්ත නොමැත"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sk/strings.xml b/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
index f2886cd..d44c7b6 100644
--- a/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Typ súboru"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Tento súbor je chránený"</string>
<string name="label_password_first" msgid="4456258714097111908">"Heslo"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Heslo je nesprávne"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"lupa na <xliff:g id="FIRST">%1$d</xliff:g> percent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Prejsť na stránku <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"strana <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Vyberte text, kde chcete umiestniť komentár"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Klep. na oblasť, ktorú chcete komentovať"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Zrušiť"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Súbor PDF sa nedá zobraziť (<xliff:g id="TITLE">%1$s</xliff:g> má neplatný formát)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g>. strana sa nedá zobraziť (chyba súboru)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Pre túto položku sa nedá načítať režim anotácií."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Odkaz: webová stránka v doméne <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Odkaz: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E‑mail: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"strany od <xliff:g id="FIRST">%1$d</xliff:g> do <xliff:g id="LAST">%2$d</xliff:g> z <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Obrázok: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Vyhľadajte v súbore"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Neboli nájdené žiadne zhody."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Naspäť"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Ďalej"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zavrieť"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Upraviť súbor"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Zadajte heslo na odomknutie"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Súbor sa nepodarilo otvoriť. Možno sa vyskytol problém s povolením."</string>
<string name="page_broken" msgid="2968770793669433462">"Stránka sa v dokumente vo formáte PDF nedá načítať"</string>
<string name="needs_more_data" msgid="3520133467908240802">"V dokumente vo formáte PDF nie je dostatok údajov na spracovanie"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sl/strings.xml b/pdf/pdf-viewer/src/main/res/values-sl/strings.xml
index 457b9f1..afef99a 100644
--- a/pdf/pdf-viewer/src/main/res/values-sl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sl/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Vrsta datoteke"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ta datoteka je zaščitena"</string>
<string name="label_password_first" msgid="4456258714097111908">"Geslo"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Napačno geslo"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"povečava <xliff:g id="FIRST">%1$d</xliff:g> %%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Pojdi na stran <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"stran <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Izberite besedilo, da umestite komentar"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Dotaknite se, kjer želite komentirati"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Prekliči"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF-ja ni mogoče prikazati (<xliff:g id="TITLE">%1$s</xliff:g> ni veljavna oblika)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Strani <xliff:g id="PAGE">%1$d</xliff:g> ni mogoče prikazati (napaka v datoteki)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Načina opomb za ta element ni mogoče naložiti."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Povezava: spletna stran v domeni <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Povezava: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-poštni naslov: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"strani <xliff:g id="FIRST">%1$d</xliff:g> do <xliff:g id="LAST">%2$d</xliff:g> od <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Slika: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Iskanje v datoteki"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Ni rezultatov."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Nazaj"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Naprej"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Zapri"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Urejanje datoteke"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Vnesite geslo za odklepanje"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Odpiranje datoteke ni uspelo. Ali morda gre za težavo z dovoljenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Strani iz dokumenta PDF ni mogoče prikazati"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nezadostni podatki za obdelavo dokumenta PDF"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Datoteke PDF ni mogoče odpreti"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
index ece0da4..37347d3 100644
--- a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Lloji i skedarit"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ky skedar është i mbrojtur."</string>
<string name="label_password_first" msgid="4456258714097111908">"Fjalëkalimi"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Fjalëkalimi është i pasaktë"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zmadho me <xliff:g id="FIRST">%1$d</xliff:g> për qind"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Shko te faqja <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"faqja <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Zgjidh tekstin për të vendosur komentin"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Trokit te një zonë për të komentuar"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Anulo"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Skedari PDF nuk mund të shfaqet (\"<xliff:g id="TITLE">%1$s</xliff:g>\" ka format të pavlefshëm)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Nuk mund të shfaqet faqja <xliff:g id="PAGE">%1$d</xliff:g> (gabim i skedarit)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Modaliteti i shënimit nuk mund të ngarkohet për këtë artikull."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Lidhja: faqe uebi te <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Lidhja: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email-i: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"faqet nga <xliff:g id="FIRST">%1$d</xliff:g> deri në <xliff:g id="LAST">%2$d</xliff:g> nga <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Imazhi: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Gjej te skedari"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Nuk u gjetën përputhje."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Pas"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Para"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Mbyll"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Modifiko skedarin"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Fut fjalëkalimin për ta shkyçur"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Hapja e skedarit dështoi. Problem i mundshëm me lejet?"</string>
<string name="page_broken" msgid="2968770793669433462">"Faqe e dëmtuar për dokumentin PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Të dhëna të pamjaftueshme për përpunimin e dokumentit PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sr/strings.xml b/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
index e8b8b9c..bcab510 100644
--- a/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Тип фајла"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Овај фајл је заштићен"</string>
<string name="label_password_first" msgid="4456258714097111908">"Лозинка"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Лозинка је нетачна"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"проценат зума: <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Иди на страницу <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"<xliff:g id="PAGE">%1$d</xliff:g>. страница"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Изаберите текст за постављање коментара"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Додирните област за коментарисање"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Откажи"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF не може да се прикаже (<xliff:g id="TITLE">%1$s</xliff:g> има неважећи формат)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Страница <xliff:g id="PAGE">%1$d</xliff:g> не може да се прикаже (грешка у фајлу)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Учитавање режима напомена за ову ставку није успело."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Линк: веб-страница на <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Линк: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Имејл: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"странице <xliff:g id="FIRST">%1$d</xliff:g>. до <xliff:g id="LAST">%2$d</xliff:g>. од <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Слика: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Пронађите у фајлу"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Није пронађено ниједно подударање."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Претходно"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Даље"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Затвори"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Измени фајл"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Унесите лозинку за откључавање"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Отварање фајла није успело. Можда постоје проблеми са дозволом?"</string>
<string name="page_broken" msgid="2968770793669433462">"Неисправна страница за PDF документ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недовољно података за обраду PDF документа"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sv/strings.xml b/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
index 90ee5c1..3fa349f 100644
--- a/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Filtyp"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Den här filen är skyddad"</string>
<string name="label_password_first" msgid="4456258714097111908">"Lösenord"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Felaktigt lösenord"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zooma <xliff:g id="FIRST">%1$d</xliff:g> procent"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Öppna sidan <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"sida <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Markera text för att kommentera"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Tryck på ett område för att kommentera"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Avbryt"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Det går inte att visa PDF-filen (<xliff:g id="TITLE">%1$s</xliff:g> har ett ogiltigt format)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Det går inte att visa sidan <xliff:g id="PAGE">%1$d</xliff:g> (filfel)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Det gick inte att läsa in kommentarsläget för det här objektet."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Länk: webbsida på <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Länk: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-postadress: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"sidorna <xliff:g id="FIRST">%1$d</xliff:g> till <xliff:g id="LAST">%2$d</xliff:g> av <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Bild: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Hitta i filen"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Inga matchningar hittades."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Föregående"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Nästa"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Stäng"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Redigera fil"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Ange lösenord för att låsa upp"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Det gick inte att öppna filen. Detta kan bero på ett behörighetsproblem."</string>
<string name="page_broken" msgid="2968770793669433462">"Det gick inte att läsa in en sida i PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Otillräcklig data för att behandla PDF-dokumentet"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sw/strings.xml b/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
index 2219b10..abf9c4b5 100644
--- a/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Aina ya faili"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Faili hii inalindwa"</string>
<string name="label_password_first" msgid="4456258714097111908">"Nenosiri"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Nenosiri si sahihi"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"kuza kwa asilimia <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Fungua ukurasa wa <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"ukurasa wa <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Chagua maandishi ili utoe maoni yako"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Gusa sehemu ili utoe maoni kuihusu"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Acha"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Imeshindwa kuonyesha PDF (muundo wa <xliff:g id="TITLE">%1$s</xliff:g> si sahihi)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Imeshindwa kuonyesha ukurasa wa <xliff:g id="PAGE">%1$d</xliff:g> (hitilafu ya faili)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Imeshindwa kupakia hali ya vidokezo kwenye kipengee hiki."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Kiungo: ukurasa wa wavuti katika <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Kiungo: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Anwani ya Barua Pepe: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"ukurasa wa <xliff:g id="FIRST">%1$d</xliff:g> hadi <xliff:g id="LAST">%2$d</xliff:g> kati ya <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Picha: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Tafuta kwenye faili"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Hakuna vipengee vinavyolingana."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Iliyotangulia"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Endelea"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Funga"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> kati ya <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Badilisha faili"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Weka nenosiri ili ufungue"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Imeshindwa kufungua faili. Je, linaweza kuwa tatizo la ruhusa?"</string>
<string name="page_broken" msgid="2968770793669433462">"Ukurasa wa hati ya PDF una tatizo"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Hamna data ya kutosha kuchakata hati ya PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ta/strings.xml b/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
index 4c28feb..2d1d291 100644
--- a/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ஃபைல் வகை"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"இந்த ஃபைல் பாதுகாக்கப்பட்டுள்ளது"</string>
<string name="label_password_first" msgid="4456258714097111908">"கடவுச்சொல்"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"கடவுச்சொல் தவறானது"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"<xliff:g id="FIRST">%1$d</xliff:g> சதவீத அளவை மாற்று"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"பக்கம் <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>க்கு செல்லும்"</string>
<string name="desc_page" msgid="5684226167093594168">"பக்கம் <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"கருத்து வழங்க வார்த்தையைத் தேர்ந்தெடுக்கவும்"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"கருத்து வழங்குவதற்கான பகுதியைத் தட்டவும்"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ரத்துசெய்"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDFஐக் காட்ட முடியவில்லை (<xliff:g id="TITLE">%1$s</xliff:g> தவறான வடிவத்தில் உள்ளது)"</string>
<string name="error_on_page" msgid="1592475819957182385">"பக்கம் <xliff:g id="PAGE">%1$d</xliff:g>ஐக் காட்ட முடியவில்லை (ஃபைல் பிழை)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"இதற்கான விரிவுரைப் பயன்முறையை ஏற்ற முடியவில்லை."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"இணைப்பு: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> இல் உள்ள இணையப் பக்கம்"</string>
<string name="desc_web_link" msgid="2776023299237058419">"இணைப்பு: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"மின்னஞ்சல்: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g> பக்கங்களில் <xliff:g id="FIRST">%1$d</xliff:g>முதல் <xliff:g id="LAST">%2$d</xliff:g> வரை"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"படம்: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ஃபைலில் தேடுக"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"பொருத்தங்கள் கண்டறியப்படவில்லை."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"முந்தையதற்குச் செல்லும்"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"அடுத்ததற்குச் செல்லும்"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"மூடும்"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ஃபைலைத் திருத்து"</string>
<string name="password_not_entered" msgid="8875370870743585303">"அன்லாக் செய்ய கடவுச்சொல்லை டைப் செய்யவும்"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"ஃபைலைத் திறக்க முடியவில்லை. அனுமதி தொடர்பான சிக்கல் உள்ளதா?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ஆவணத்தை ஏற்ற முடியவில்லை"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ஆவணத்தைச் செயலாக்குவதற்குப் போதுமான தரவு இல்லை"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-te/strings.xml b/pdf/pdf-viewer/src/main/res/values-te/strings.xml
index 26c2954..ffcee7a 100644
--- a/pdf/pdf-viewer/src/main/res/values-te/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-te/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ఫైల్ రకం"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ఈ ఫైల్ సంరక్షించబడుతోంది"</string>
<string name="label_password_first" msgid="4456258714097111908">"పాస్వర్డ్"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"పాస్వర్డ్ తప్పు"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"జూమ్ <xliff:g id="FIRST">%1$d</xliff:g> శాతం"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> పేజీకి వెళ్లండి"</string>
<string name="desc_page" msgid="5684226167093594168">"పేజీ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"మీరు కామెంట్ చేయడానికి టెక్స్ట్ను ఎంచుకోండి"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"ఎక్కడ కామెంట్ చేయాలో ట్యాప్ చేయండి"</string>
- <string name="action_cancel" msgid="5494417739210197522">"రద్దు చేయండి"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF డిస్ప్లే చేయడం సాధ్యం కాదు (<xliff:g id="TITLE">%1$s</xliff:g> చెల్లని ఫార్మాట్లో ఉంది)"</string>
<string name="error_on_page" msgid="1592475819957182385">"పేజీని డిస్ప్లే చేయడం సాధ్యం కాదు <xliff:g id="PAGE">%1$d</xliff:g> (ఫైల్ ఎర్రర్)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"ఈ ఐటెమ్ కోసం అదనపు గమనిక మోడ్ను లోడ్ చేయడం సాధ్యం కాదు."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"లింక్: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>లో వెబ్ పేజీ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"లింక్: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ఈమెయిల్: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"<xliff:g id="TOTAL">%3$d</xliff:g>లో <xliff:g id="FIRST">%1$d</xliff:g> నుండి <xliff:g id="LAST">%2$d</xliff:g> పేజీలు"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"ఇమేజ్: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ఫైల్లో కనుగొనండి"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"మ్యాచ్లు ఏవీ దొరకలేదు."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"మునుపటి"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"తర్వాత"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"మూసివేయండి"</string>
<string name="message_match_status" msgid="6288242289981639727">"మొత్తం <xliff:g id="TOTAL">%2$d</xliff:g>లో <xliff:g id="POSITION">%1$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"ఫైల్ను ఎడిట్ చేయండి"</string>
<string name="password_not_entered" msgid="8875370870743585303">"అన్లాక్ చేయడానికి పాస్వర్డ్ను నమోదు చేయండి"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ఫైల్ను తెరవడం విఫలమైంది. అనుమతికి సంబంధించిన సమస్య కావచ్చా?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF డాక్యుమెంట్కు సంబంధించి పేజీ బ్రేక్ అయింది"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF డాక్యుమెంట్ను ప్రాసెస్ చేయడానికి డేటా తగినంత లేదు"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ఫైల్ను తెరవడం సాధ్యపడదు"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-th/strings.xml b/pdf/pdf-viewer/src/main/res/values-th/strings.xml
index 4565375..be31750 100644
--- a/pdf/pdf-viewer/src/main/res/values-th/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-th/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"ประเภทไฟล์"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"ไฟล์นี้ได้รับการป้องกัน"</string>
<string name="label_password_first" msgid="4456258714097111908">"รหัสผ่าน"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"รหัสผ่านไม่ถูกต้อง"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"ซูม <xliff:g id="FIRST">%1$d</xliff:g> เปอร์เซ็นต์"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"ไปที่หน้า <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"หน้า <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"เลือกข้อความเพื่อใส่ความคิดเห็น"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"แตะบริเวณที่ต้องการแสดงความคิดเห็น"</string>
- <string name="action_cancel" msgid="5494417739210197522">"ยกเลิก"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"ไม่สามารถแสดง PDF (<xliff:g id="TITLE">%1$s</xliff:g> มีรูปแบบไม่ถูกต้อง)"</string>
<string name="error_on_page" msgid="1592475819957182385">"ไม่สามารถแสดงหน้า <xliff:g id="PAGE">%1$d</xliff:g> (ไฟล์มีข้อผิดพลาด)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"โหลดโหมดคําอธิบายประกอบสําหรับรายการนี้ไม่ได้"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"ลิงก์: หน้าเว็บที่ <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"ลิงก์: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"อีเมล: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"หน้า <xliff:g id="FIRST">%1$d</xliff:g> ถึง <xliff:g id="LAST">%2$d</xliff:g> จาก <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"รูปภาพ: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"ค้นหาในไฟล์"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"ไม่พบข้อมูลที่ตรงกัน"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"ก่อนหน้า"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"ถัดไป"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"ปิด"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"แก้ไขไฟล์"</string>
<string name="password_not_entered" msgid="8875370870743585303">"ป้อนรหัสผ่านเพื่อปลดล็อก"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"เปิดไฟล์ไม่สำเร็จ อาจเกิดจากปัญหาด้านสิทธิ์"</string>
<string name="page_broken" msgid="2968770793669433462">"หน้าในเอกสาร PDF เสียหาย"</string>
<string name="needs_more_data" msgid="3520133467908240802">"ข้อมูลไม่เพียงพอสำหรับการประมวลผลเอกสาร PDF"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"เปิดไฟล์ PDF ไม่ได้"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-tl/strings.xml b/pdf/pdf-viewer/src/main/res/values-tl/strings.xml
index 6399462..5b20647 100644
--- a/pdf/pdf-viewer/src/main/res/values-tl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-tl/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Uri ng file"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Pinoprotektahan ang file na ito"</string>
<string name="label_password_first" msgid="4456258714097111908">"Password"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Mali ang password"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"i-zoom nang <xliff:g id="FIRST">%1$d</xliff:g> porsyento"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Pumunta sa page <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"page <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Pumili ng text para ilagay ang iyong komento"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Mag-tap ng bahaging kokomentuhan"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Kanselahin"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Hindi maipakita ang PDF (invalid ang format ng <xliff:g id="TITLE">%1$s</xliff:g>)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Hindi maipakita ang page <xliff:g id="PAGE">%1$d</xliff:g> (error sa file)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Hindi ma-load ang annotation mode para sa item na ito."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Link: webpage sa <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Link: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"page <xliff:g id="FIRST">%1$d</xliff:g> hanggang <xliff:g id="LAST">%2$d</xliff:g> sa <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Larawan: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Maghanap sa file"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Walang nahanap na tugma."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Nakaraan"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Susunod"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Isara"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"I-edit ang file"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Ilagay ang password para i-unlock"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Hindi nabuksan ang file. Baka may isyu sa pahintulot?"</string>
<string name="page_broken" msgid="2968770793669433462">"Sira ang page para sa PDF na dokumento"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Kulang ang data para maproseso ang PDF na dokumento"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Hindi mabuksan ang PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-tr/strings.xml b/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
index c306300..8ad6bf2 100644
--- a/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Dosya türü"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Bu dosya korunuyor"</string>
<string name="label_password_first" msgid="4456258714097111908">"Şifre"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Şifre yanlış"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"yakınlaştırma yüzdesi <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Şu sayfaya git <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"sayfa <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Yorumunuzu yerleştireceğiniz metni seçin"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Hakkında yorum yapacağınız alana dokunun"</string>
- <string name="action_cancel" msgid="5494417739210197522">"İptal"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF görüntülenemiyor (<xliff:g id="TITLE">%1$s</xliff:g> geçersiz biçimde)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g>. sayfa görüntülenemiyor (dosya hatası)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Bu öğe için ek açıklama modu yüklenemiyor."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Bağlantı: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> alan adındaki web sayfası"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Bağlantı: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"E-posta gönder: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"sayfa <xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g>/<xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Resim: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Dosyada bul"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Eşleşme bulunamadı."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Önceki"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Sonraki"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Kapat"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Dosyayı düzenle"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Kilidi açmak için şifreyi girin"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Dosya açılamadı. İzin sorunundan kaynaklanıyor olabilir mi?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokümanının sayfası bozuk"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF dokümanını işleyecek kadar yeterli veri yok"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-uk/strings.xml b/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
index 8585b92..c381f20 100644
--- a/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Тип файлу"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Цей файл захищено"</string>
<string name="label_password_first" msgid="4456258714097111908">"Пароль"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Неправильний пароль"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"масштаб <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Перейти на сторінку <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"сторінка <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Виділіть текст, щоб додати коментар"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Торкніться місця, яке хочете коментувати"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Скасувати"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Не вдається відобразити PDF (недійсний формат файлу \"<xliff:g id="TITLE">%1$s</xliff:g>\")"</string>
<string name="error_on_page" msgid="1592475819957182385">"Не вдається відобразити сторінку <xliff:g id="PAGE">%1$d</xliff:g> (помилка файлу)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Не вдається завантажити режим анотацій для цього об’єкта."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Посилання: вебсторінка в домені <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Посилання: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Електронна адреса: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"сторінки <xliff:g id="FIRST">%1$d</xliff:g>–<xliff:g id="LAST">%2$d</xliff:g> з <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Зображення: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Пошук у файлі"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Нічого не знайдено."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Назад"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Далі"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Закрити"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Редагувати файл"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Введіть пароль, щоб розблокувати"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Не вдалося відкрити файл. Можливо, виникла проблема з дозволом."</string>
<string name="page_broken" msgid="2968770793669433462">"Сторінку документа PDF пошкоджено"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недостатньо даних для обробки документа PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ur/strings.xml b/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
index 10a6c0a..6027e36 100644
--- a/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"فائل کی قسم"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"یہ فائل محفوظ ہے"</string>
<string name="label_password_first" msgid="4456258714097111908">"پاس ورڈ"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"پاس ورڈ غلط ہے"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"زوم <xliff:g id="FIRST">%1$d</xliff:g> فیصد"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g> صفحہ پر جائیں"</string>
<string name="desc_page" msgid="5684226167093594168">"صفحہ <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"اپنا تبصرہ شامل کرنے کیلئے ٹیکسٹ منتخب کریں"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"تبصرہ کرنے کے لیے کسی حصہ پر تھپھتپائیں"</string>
- <string name="action_cancel" msgid="5494417739210197522">"منسوخ کریں"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF ڈسپلے نہیں کر سکتا (<xliff:g id="TITLE">%1$s</xliff:g> کا فارمیٹ غلط ہے)"</string>
<string name="error_on_page" msgid="1592475819957182385">"صفحہ <xliff:g id="PAGE">%1$d</xliff:g> ڈسپلے نہیں کر سکتا (فائل کی خرابی)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"اس آئٹم کے لیے تشریح موڈ لوڈ نہیں ہو سکتا۔"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"لنک: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> پر ویب صفحہ"</string>
<string name="desc_web_link" msgid="2776023299237058419">"لنک: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"ای میل: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"صفحات <xliff:g id="FIRST">%1$d</xliff:g> سے <xliff:g id="LAST">%2$d</xliff:g> از <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"تصویر: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"فائل میں تلاش کریں"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"کسی مماثلت کا پتا نہیں چلا۔"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"پچھلا"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"اگلا"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"بند کریں"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="TOTAL">%2$d</xliff:g> / <xliff:g id="POSITION">%1$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"فائل میں ترمیم کریں"</string>
<string name="password_not_entered" msgid="8875370870743585303">"غیر مقفل کرنے کیلئے پاس ورڈ درج کریں"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"فائل کھولنے میں ناکام۔ کیا یہ اجازت کا مسئلہ ہو سکتا ہے؟"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF دستاویز کیلئے شکستہ صفحہ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF دستاویز پر کارروائی کرنے کیلئے ڈیٹا ناکافی ہے"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-uz/strings.xml b/pdf/pdf-viewer/src/main/res/values-uz/strings.xml
index c313d09..41b89f8 100644
--- a/pdf/pdf-viewer/src/main/res/values-uz/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-uz/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Fayl turi"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Ushbu fayl himoyalangan"</string>
<string name="label_password_first" msgid="4456258714097111908">"Parol"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Parol xato"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"zum: <xliff:g id="FIRST">%1$d</xliff:g> foiz"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"<xliff:g id="PAGE_NUMBER">%1$d</xliff:g>-sahifaga oʻtish"</string>
<string name="desc_page" msgid="5684226167093594168">"sahifa: <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Fikr bildirish uchun matnni tanlang."</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Fikr qoʻshish uchun biror maydonni bosing"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Bekor qilish"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"PDF fayl koʻrsatilmaydi (<xliff:g id="TITLE">%1$s</xliff:g> yaroqsiz formatda)"</string>
<string name="error_on_page" msgid="1592475819957182385">"<xliff:g id="PAGE">%1$d</xliff:g>-sahifa koʻrsatilmaydi (fayl xatosi)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Bu obyekt uchun izoh rejimi yuklanmadi."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Havola: <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> sahifasi"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Havola: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"sahifalar: <xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g> / <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Tasvir: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Fayl ichidan topish"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Hech narsa topilmadi."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Avvalgisi"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Keyingisi"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Yopish"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Faylni tahrirlash"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Ochish uchun parolni kiriting"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Fayl ochilmadi. Ruxsat bilan muammo bormi?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF hujjat sahifasi yaroqsiz"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF hujjatni qayta ishlash uchun kerakli axborotlar yetarli emas"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF fayk ochilmadi"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-v34/colors.xml b/pdf/pdf-viewer/src/main/res/values-v34/colors.xml
deleted file mode 100644
index 29c9a19..0000000
--- a/pdf/pdf-viewer/src/main/res/values-v34/colors.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
- Copyright 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources>
-
- <color name="google_blue">#ff1a73e8</color>
- <color name="google_white">#ffffffff</color>
- <color name="google_grey">#ff3c4043</color>
- <color name="text_default">#666</color>
- <color name="text_error">#da4336</color>
- <color name="selection_handles">#00aadd</color>
- <color name="search_background">@android:color/system_surface_container_light</color>
- <color name="search_textbox">@android:color/system_surface_bright_light</color>
- <color name="search_texthint">@android:color/system_on_surface_variant_light</color>
- <color name="search_textColor">@android:color/system_on_surface_light</color>
- <color name="search_count">@android:color/system_on_surface_variant_light</color>
- <color name="search_prev_button">@android:color/system_on_surface_variant_light</color>
- <color name="search_next_button">@android:color/system_on_surface_variant_light</color>
- <color name="search_close_button">@android:color/system_on_surface_variant_light</color>
-
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values-vi/strings.xml b/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
index ea11021..5f50634 100644
--- a/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Loại tệp"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Tệp này đang được bảo vệ"</string>
<string name="label_password_first" msgid="4456258714097111908">"Mật khẩu"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Mật khẩu không chính xác"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"thu phóng <xliff:g id="FIRST">%1$d</xliff:g> phần trăm"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Chuyển đến trang <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"trang <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Chọn văn bản để đưa ra nhận xét"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Nhấn vào một khu vực để thêm nhận xét"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Huỷ"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Không hiển thị được tệp PDF (<xliff:g id="TITLE">%1$s</xliff:g> có định dạng không hợp lệ)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Không hiển thị được trang <xliff:g id="PAGE">%1$d</xliff:g> (lỗi tệp)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Không tải được chế độ chú giải cho mục này."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Đường liên kết: trang trên trang web <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Đường liên kết: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"Email: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"các trang <xliff:g id="FIRST">%1$d</xliff:g> đến <xliff:g id="LAST">%2$d</xliff:g> trong số <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Hình ảnh: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Tìm trong tệp"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Không tìm thấy kết quả phù hợp."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Trước"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Tiếp theo"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Đóng"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Chỉnh sửa tệp"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Nhập mật khẩu để mở khoá"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Không mở được tệp này. Có thể là do vấn đề về quyền?"</string>
<string name="page_broken" msgid="2968770793669433462">"Tài liệu PDF này bị lỗi trang"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Không đủ dữ liệu để xử lý tài liệu PDF này"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rCN/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rCN/strings.xml
index aea5179..c6dbf7e 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rCN/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rCN/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"文件类型"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"此文件受密码保护"</string>
<string name="label_password_first" msgid="4456258714097111908">"密码"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"密码不正确"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"缩放比例为百分之 <xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"前往第 <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> 页"</string>
<string name="desc_page" msgid="5684226167093594168">"第 <xliff:g id="PAGE">%1$d</xliff:g> 页"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"选择要添加评论的文本"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"点按要添加评论的区域"</string>
- <string name="action_cancel" msgid="5494417739210197522">"取消"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"无法显示 PDF(“<xliff:g id="TITLE">%1$s</xliff:g>”的格式无效)"</string>
<string name="error_on_page" msgid="1592475819957182385">"无法显示第 <xliff:g id="PAGE">%1$d</xliff:g> 页(文件错误)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"无法为此内容加载注解模式。"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"链接:位于 <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> 的网页"</string>
<string name="desc_web_link" msgid="2776023299237058419">"链接:<xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"电子邮件地址:<xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"第 <xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g> 页,共 <xliff:g id="TOTAL">%3$d</xliff:g> 页"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"图片:<xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"在文件中查找"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"未找到匹配项。"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"上一页"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"下一页"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"关闭"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"编辑文件"</string>
<string name="password_not_entered" msgid="8875370870743585303">"请输入密码进行解锁"</string>
@@ -56,4 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"无法打开文件。可能是由于权限问题导致?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文档的页面已损坏"</string>
<string name="needs_more_data" msgid="3520133467908240802">"数据不足,无法处理 PDF 文档"</string>
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"无法打开 PDF 文件"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
index 8cff251..691bdfe 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"檔案類型"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"此檔案受到保護"</string>
<string name="label_password_first" msgid="4456258714097111908">"密碼"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"密碼不正確"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"縮放 <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"前往第 <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> 頁"</string>
<string name="desc_page" msgid="5684226167093594168">"第 <xliff:g id="PAGE">%1$d</xliff:g> 頁"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"選取文字以加入留言"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"輕按要加入留言的區域"</string>
- <string name="action_cancel" msgid="5494417739210197522">"取消"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"無法顯示 PDF (<xliff:g id="TITLE">%1$s</xliff:g> 格式無效)"</string>
<string name="error_on_page" msgid="1592475819957182385">"無法顯示第 <xliff:g id="PAGE">%1$d</xliff:g> 頁 (檔案錯誤)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"無法為此項目載入註釋模式。"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"連結:位於 <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> 的網頁"</string>
<string name="desc_web_link" msgid="2776023299237058419">"連結:<xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"電郵地址:<xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"第 <xliff:g id="FIRST">%1$d</xliff:g> 至 <xliff:g id="LAST">%2$d</xliff:g> 頁 (共 <xliff:g id="TOTAL">%3$d</xliff:g> 頁)"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"圖片:<xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"在檔案中搜尋"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"找不到相符項目。"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"上一個"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"下一個"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"閂"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"編輯檔案"</string>
<string name="password_not_entered" msgid="8875370870743585303">"輸入密碼即可解鎖"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"無法開啟檔案。可能有權限問題?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文件頁面已損毀"</string>
<string name="needs_more_data" msgid="3520133467908240802">"沒有足夠資料處理 PDF 文件"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
index 57010b7..b2be3f4 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"檔案類型"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"這個檔案受密碼保護"</string>
<string name="label_password_first" msgid="4456258714097111908">"密碼"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"密碼不正確"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"縮放 <xliff:g id="FIRST">%1$d</xliff:g>%%"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"前往第 <xliff:g id="PAGE_NUMBER">%1$d</xliff:g> 頁"</string>
<string name="desc_page" msgid="5684226167093594168">"第 <xliff:g id="PAGE">%1$d</xliff:g> 頁"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"選取要加註的文字"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"輕觸要加註的區域"</string>
- <string name="action_cancel" msgid="5494417739210197522">"取消"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"無法顯示 PDF (「<xliff:g id="TITLE">%1$s</xliff:g>」的格式無效)"</string>
<string name="error_on_page" msgid="1592475819957182385">"無法顯示第 <xliff:g id="PAGE">%1$d</xliff:g> 頁 (檔案錯誤)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"無法載入這個項目的註解模式。"</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"連結:位於 <xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g> 的網頁"</string>
<string name="desc_web_link" msgid="2776023299237058419">"連結:<xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"電子郵件地址:<xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"第 <xliff:g id="FIRST">%1$d</xliff:g> 到 <xliff:g id="LAST">%2$d</xliff:g> 頁,共 <xliff:g id="TOTAL">%3$d</xliff:g> 頁"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"圖片:<xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"在檔案中搜尋"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"找不到相符項目。"</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"上一個"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"下一個"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"關閉"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"編輯檔案"</string>
<string name="password_not_entered" msgid="8875370870743585303">"輸入密碼即可解鎖"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"無法開啟檔案。有可能是權限問題?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文件的頁面損毀"</string>
<string name="needs_more_data" msgid="3520133467908240802">"資料不足,無法處理 PDF 文件"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zu/strings.xml b/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
index 8d4f6b1..ba456e5 100644
--- a/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
@@ -17,7 +17,6 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="desc_file_type" msgid="8918077960128045611">"Uhlobo lwefayela"</string>
<string name="title_dialog_password" msgid="2018068413709926925">"Leli fayela livikelwe"</string>
<string name="label_password_first" msgid="4456258714097111908">"Iphasiwedi"</string>
<string name="label_password_incorrect" msgid="8449142641187704667">"Iphasiwedi ayilungile"</string>
@@ -31,12 +30,8 @@
<string name="desc_zoom" msgid="7318480946145947242">"sondeza iphesenti elingu-<xliff:g id="FIRST">%1$d</xliff:g>"</string>
<string name="desc_goto_link" msgid="2461368384824849714">"Iya ekhasini <xliff:g id="PAGE_NUMBER">%1$d</xliff:g>"</string>
<string name="desc_page" msgid="5684226167093594168">"ikhasi <xliff:g id="PAGE">%1$d</xliff:g>"</string>
- <string name="message_select_text_to_comment" msgid="5725327644007067522">"Khetha umbhalo ukuze ubeke amazwana akho"</string>
- <string name="message_tap_to_comment" msgid="7820801719181709999">"Thepha indawo ukuze ubeke amazwana kuyo"</string>
- <string name="action_cancel" msgid="5494417739210197522">"Khansela"</string>
<string name="error_file_format_pdf" msgid="7567006188638831878">"Ayikwazi ukubonisa i-PDF (i-<xliff:g id="TITLE">%1$s</xliff:g> ingeyefomethi engavumelekile)"</string>
<string name="error_on_page" msgid="1592475819957182385">"Ayikwazi ukubonisa ikhasi elingu-<xliff:g id="PAGE">%1$d</xliff:g> (iphutha lefayela)"</string>
- <string name="annotation_mode_failed_to_open" msgid="1659648756255912463">"Ayikwazi ukulayisha imodi yesichasiselo yale nto."</string>
<string name="desc_web_link_shortened_to_domain" msgid="3323639528531061592">"Ilinki: ikhasi lewebhu ku-<xliff:g id="DESTINATION_DOMAIN">%1$s</xliff:g>"</string>
<string name="desc_web_link" msgid="2776023299237058419">"Ilinki: <xliff:g id="DESTINATION">%1$s</xliff:g>"</string>
<string name="desc_email_link" msgid="7027325672358507448">"I-imeyili: <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
@@ -47,7 +42,9 @@
<string name="desc_page_range" msgid="5286496438609641577">"amakhasi <xliff:g id="FIRST">%1$d</xliff:g> ukuya ku-<xliff:g id="LAST">%2$d</xliff:g> kwangu-<xliff:g id="TOTAL">%3$d</xliff:g>"</string>
<string name="desc_image_alt_text" msgid="7700601988820586333">"Umfanekiso: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
<string name="hint_find" msgid="5385388836603550565">"Thola kufayela"</string>
- <string name="message_no_matches_found" msgid="6965828658999779258">"Akukho okufanayo okutholiwe."</string>
+ <string name="previous_button_description" msgid="1169511027880317546">"Okwangaphambilini"</string>
+ <string name="next_button_description" msgid="4702699322249103693">"Okulandelayo"</string>
+ <string name="close_button_description" msgid="7379823906921067675">"Vala"</string>
<string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="action_edit" msgid="5882082700509010966">"Hlela ifayela"</string>
<string name="password_not_entered" msgid="8875370870743585303">"Faka iphasiwedi ukuvula"</string>
@@ -56,4 +53,6 @@
<string name="file_error" msgid="4003885928556884091">"Yehlulekile ukuvula ifayela. Inkinga yemvume engaba khona?"</string>
<string name="page_broken" msgid="2968770793669433462">"Ikhasi eliphuliwe ledokhumenti ye-PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Idatha enganele yokucubungula idokhumenti ye-PDF"</string>
+ <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
+ <skip />
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/colors.xml b/pdf/pdf-viewer/src/main/res/values/colors.xml
index a860b33..63ae166 100644
--- a/pdf/pdf-viewer/src/main/res/values/colors.xml
+++ b/pdf/pdf-viewer/src/main/res/values/colors.xml
@@ -15,21 +15,10 @@
-->
<resources>
+ <color name="pdf_viewer_color_primary">@color/m3_sys_color_light_primary</color>
+ <color name="pdf_viewer_color_on_surface">@color/m3_sys_color_light_on_surface</color>
+ <color name="pdf_viewer_color_on_error">@color/m3_sys_color_light_on_error</color>
+ <!-- m3_sys_color_primary_fixed_dim doesn't have a day/night version, therefore defined only once -->
+ <color name="pdf_viewer_selection_handles">@color/m3_sys_color_primary_fixed_dim</color>
+</resources>
- <color name="google_blue">#ff1a73e8</color>
- <color name="google_white">#ffffffff</color>
- <color name="google_grey">#ff3c4043</color>
- <color name="text_default">#666</color>
- <color name="text_error">#da4336</color>
- <color name="selection_handles">#00aadd</color>
- <color name="search_background">#F3EDE8</color>
- <color name="search_textbox">#FEF8F3</color>
- <color name="search_texthint">#494643</color>
- <color name="search_textColor">#1D1B19</color>
- <color name="search_count">#494643</color>
- <color name="search_prev_button">#494643</color>
- <color name="search_next_button">#494643</color>
- <color name="search_close_button">#494643</color>
-
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values/dimensions.xml b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
index 4869951..03bf958 100644
--- a/pdf/pdf-viewer/src/main/res/values/dimensions.xml
+++ b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
@@ -16,17 +16,12 @@
<resources>
<dimen name="viewer_doc_additional_top_offset">12dp</dimen>
- <dimen name="viewer_fastscroll_edge_offset">15dp</dimen>
- <dimen name="viewer_fastscroll_rounded_corner_radius">2dp</dimen>
+ <dimen name="viewer_fastscroll_edge_offset">24dp</dimen>
<dimen name="viewer_doc_padding_y">4dp</dimen>
<dimen name="viewer_doc_padding_x">0dp</dimen>
- <dimen name="mtrl_min_touch_target_size">48dp</dimen>
-
- <dimen name="viewer_progress_bar_height">4dp</dimen>
- <dimen name="viewer_frame_error_height">80dp</dimen>
<!--Edit button FAB -->
- <dimen name="viewer_edit_fab_margin_bottom">42dp</dimen>
- <dimen name="viewer_edit_fab_margin_right">24dp</dimen>
+ <dimen name="viewer_edit_fab_margin_bottom">16dp</dimen>
+ <dimen name="viewer_edit_fab_margin_right">16dp</dimen>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/strings.xml b/pdf/pdf-viewer/src/main/res/values/strings.xml
index 79cf9a2..5a8f507 100644
--- a/pdf/pdf-viewer/src/main/res/values/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values/strings.xml
@@ -15,10 +15,6 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Content description for an icon representing the file's type (e.g. image or pdf). If
- we know the exact file type, it will be appended to this description, and result in e.g.
- "file type: Image" [CHAR LIMIT=25] -->
- <string name="desc_file_type">File type</string>
<!-- Title of the password dialog [CHAR LIMIT=40] -->
<string name="title_dialog_password">This file is protected</string>
@@ -61,16 +57,6 @@
<!-- Content description for a page of a paginated document, eg "page 3". [CHAR LIMIT=50] -->
<string name="desc_page">page <xliff:g example="3" id="page">%1$d</xliff:g></string>
- <!-- Message for instructing the user to select text to create an inline comment anchor. [CHAR LIMIT=45] -->
- <string name="message_select_text_to_comment">Select text to place your comment</string>
-
- <!-- Message for instructing the user to tap to create a positional comment anchor. [CHAR LIMIT=40] -->
- <string name="message_tap_to_comment">Tap an area to comment on</string>
-
- <!-- Text for an action to cancel an operation. [CHAR LIMIT=20] -->
- <string name="action_cancel">Cancel</string>
-
-
<!-- Error message when file format isn't valid PDF. [CHAR LIMIT=60] -->
<string name="error_file_format_pdf">Cannot display PDF ("<xliff:g example="Treasure Island" id="title">%1$s</xliff:g>" is of invalid format)</string>
@@ -78,10 +64,6 @@
<string name="error_on_page">Cannot display page <xliff:g example="3" id="page">%1$d</xliff:g>
(file error)</string>
- <!-- Contents of a snackbar message that is shown when launching annotation mode fails. The user
- can try again but it is unlikely that will solve the problem. [CHAR LIMIT=100] -->
- <string name="annotation_mode_failed_to_open">Can\'t load annotation mode for this item.</string>
-
<!-- Content description for a web link within a document that has been shortened to only include the domain.
The entire URL is too long to read aloud, but the user should know that it is a link and the domain of the webpage that it links to. [CHAR LIMIT=40] -->
<string name="desc_web_link_shortened_to_domain">Link: webpage at <xliff:g example="www.example.com" id="destination_domain">%1$s</xliff:g></string>
@@ -126,9 +108,6 @@
<!-- Content description for close button in find in file menu -->
<string name="close_button_description">Close</string>
- <!-- Message for no matches found when searching for query text inside the file. [CHAR LIMIT=40] -->
- <string name="message_no_matches_found">No matches found.</string>
-
<!-- Message to show which match is selected out of how many matches when searching for query text inside the file.
e.g. "3 of 12" meaning "match 3 is selected, out of 12 matches" [CHAR LIMIT=10] -->
<string name="message_match_status"><xliff:g id="position" example="3">%1$d</xliff:g> / <xliff:g id="total" example="12">%2$d</xliff:g></string>
@@ -156,4 +135,7 @@
<!-- Exception message when the PDF document was insufficient file contents -->
<string name="needs_more_data">Insufficient data for processing the PDF document</string>
+
+ <!-- Error Message on PDF File Load Error -->
+ <string name="error_cannot_open_pdf">Can\'t open PDF file</string>
</resources>
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java
index eecca89..a568c6f 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginationModelTest.java
@@ -178,25 +178,25 @@
mPaginationModel.addPage(1, mdDimensions);
mPaginationModel.addPage(2, lgDimensions);
// Setting viewArea large enough to accommodate the entire model.
- mPaginationModel.setViewArea(new Rect(0, 0, 800, 800));
+ Rect viewArea = new Rect(0, 0, 800, 800);
Rect expectedSmLocation = new Rect(300, 0, 500, 100);
- assertThat(mPaginationModel.getPageLocation(0)).isEqualTo(expectedSmLocation);
+ assertThat(mPaginationModel.getPageLocation(0, viewArea)).isEqualTo(expectedSmLocation);
Rect expectedMdLocation =
new Rect(200, 100 + getSpacingAbovePage(1), 600, 300 + getSpacingAbovePage(1));
- assertThat(mPaginationModel.getPageLocation(1)).isEqualTo(expectedMdLocation);
+ assertThat(mPaginationModel.getPageLocation(1, viewArea)).isEqualTo(expectedMdLocation);
Rect expectedLgLocation =
new Rect(0, 300 + getSpacingAbovePage(2), 800, 700 + getSpacingAbovePage(2));
- Assert.assertEquals(expectedLgLocation, mPaginationModel.getPageLocation(2));
- assertThat(mPaginationModel.getPageLocation(2)).isEqualTo(expectedLgLocation);
+ assertThat(mPaginationModel.getPageLocation(2, viewArea)).isEqualTo(expectedLgLocation);
}
/**
- * {@link PaginationModel#getPageLocation(int)} should try to fit as much of each page into the
- * viewable area as possible. Dimensions do not change vertically but pages that are smaller
- * than {@link PaginationModel#getWidth()} can be moved horizontally to make this happen.
+ * {@link PaginationModel#getPageLocation(int, Rect)} should try to fit as much of each page
+ * into the viewable area as possible. Dimensions do not change vertically but pages that are
+ * smaller than {@link PaginationModel#getWidth()} can be moved horizontally to make this
+ * happen.
*
* <p>Page 1's width is smaller than {@link PaginationModel#getWidth()} so it will be placed in
* the middle when the visible area covers the whole model {@see #testGetPageLocation}. When the
@@ -216,11 +216,11 @@
mPaginationModel.addPage(2, lgDimensions);
// Setting viewArea to a 300x200 section in the bottom-left corner of the model.
- mPaginationModel.setViewArea(new Rect(0, 500, 200, 800));
+ Rect viewArea = new Rect(0, 500, 200, 800);
Rect expectedMdLocation =
new Rect(0, 100 + getSpacingAbovePage(1), 400, 300 + getSpacingAbovePage(1));
- assertThat(mPaginationModel.getPageLocation(1)).isEqualTo(expectedMdLocation);
+ assertThat(mPaginationModel.getPageLocation(1, viewArea)).isEqualTo(expectedMdLocation);
}
/**
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
index b19e190..93e2b09 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
@@ -80,7 +80,7 @@
mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 0, false);
when(mMockPaginatedView.getPageRangeHandler()).thenReturn(mPageRangeHandler);
- when(mMockPaginatedView.getPaginationModel()).thenReturn(mMockPaginationModel);
+ when(mMockPaginatedView.getModel()).thenReturn(mMockPaginationModel);
when(mMockPaginationModel.isInitialized()).thenReturn(true);
when(mMockZoomView.getHeight()).thenReturn(100);
when(mPageRangeHandler.computeVisibleRange(0, 1.0f, 100, false)).thenReturn(PAGE_RANGE);
@@ -103,7 +103,7 @@
zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
verify(mMockZoomView).setStableZoom(1.0f);
- verify(mMockPaginationModel).setViewArea(RECT);
+ verify(mMockPaginatedView).setViewArea(RECT);
verify(mMockPaginatedView).refreshPageRangeInVisibleArea(mNewPosition, 100);
verify(mMockPaginatedView).handleGonePages(false);
verify(mMockPaginatedView).loadInvisibleNearPageRange(1.0f);
diff --git a/playground-common/playground-plugin/build.gradle b/playground-common/playground-plugin/build.gradle
index c5b8bb8..8081016 100644
--- a/playground-common/playground-plugin/build.gradle
+++ b/playground-common/playground-plugin/build.gradle
@@ -21,7 +21,7 @@
dependencies {
implementation(project(":shared"))
- implementation("com.gradle:develocity-gradle-plugin:3.17.2")
+ implementation("com.gradle:develocity-gradle-plugin:3.18")
implementation("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
implementation("supportBuildSrc:private")
implementation("supportBuildSrc:public")
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index e3086fc..b7c215d 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
# Disable docs
androidx.enableDocumentation=false
androidx.playground.snapshotBuildId=11349412
-androidx.playground.metalavaBuildId=12216051
-androidx.studio.type=playground
\ No newline at end of file
+androidx.playground.metalavaBuildId=12253410
+androidx.studio.type=playground
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
index e685b4c..ef1a4b4 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/VersionCompatUtil.kt
@@ -32,25 +32,9 @@
SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= minVersion
}
- fun isRWithMinExtServicesVersion(minVersion: Int): Boolean {
- return Build.VERSION.SDK_INT == 30 &&
- SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= minVersion
- }
-
// Helper method to determine if version is testable, for APIs that are S+ only
fun isTestableVersion(minAdServicesVersion: Int, minExtServicesVersion: Int): Boolean {
return isTPlusWithMinAdServicesVersion(minAdServicesVersion) ||
isSWithMinExtServicesVersion(minExtServicesVersion)
}
-
- // Helper method to determine if version is testable, for APIs that are available on R
- fun isTestableVersion(
- minAdServicesVersion: Int,
- minExtServicesVersionS: Int,
- minExtServicesVersionR: Int
- ): Boolean {
- return isTPlusWithMinAdServicesVersion(minAdServicesVersion) ||
- isSWithMinExtServicesVersion(minExtServicesVersionS) ||
- isRWithMinExtServicesVersion(minExtServicesVersionR)
- }
}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
index dcf44e7d..3b2c545 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
@@ -17,7 +17,6 @@
package androidx.privacysandbox.ads.adservices.java.adid
import android.adservices.adid.AdIdManager
-import android.adservices.common.AdServicesOutcomeReceiver
import android.content.Context
import android.os.Looper
import android.os.OutcomeReceiver
@@ -54,14 +53,12 @@
private var mSession: StaticMockitoSession? = null
private val mValidAdExtServicesSdkExtVersionS =
VersionCompatUtil.isSWithMinExtServicesVersion(9)
- private val mValidAdExtServicesSdkExtVersionR =
- VersionCompatUtil.isRWithMinExtServicesVersion(11)
@Before
fun setUp() {
mContext = spy(ApplicationProvider.getApplicationContext<Context>())
- if (mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR) {
+ if (mValidAdExtServicesSdkExtVersionS) {
// setup a mockitoSession to return the mocked manager
// when the static method .get() is called
mSession =
@@ -78,11 +75,10 @@
@SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
fun testAdIdOlderVersions() {
Assume.assumeFalse(
- "maxSdkVersion = API 33 ext 3 or API 31/32 ext 8 or API 30 ext 10",
+ "maxSdkVersion = API 33 ext 3 or API 31/32 ext 8",
VersionCompatUtil.isTestableVersion(
/* minAdServicesVersion=*/ 4,
/* minExtServicesVersionS=*/ 9,
- /* minExtServicesVersionR=*/ 11
)
)
Truth.assertThat(AdIdManagerFutures.from(mContext)).isEqualTo(null)
@@ -91,24 +87,15 @@
@Test
fun testAdIdAsync() {
Assume.assumeTrue(
- "minSdkVersion = API 33 ext 4 or API 31/32 ext 9 or API 30 ext 11",
+ "minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
VersionCompatUtil.isTestableVersion(
/* minAdServicesVersion= */ 4,
/* minExtServicesVersionS=*/ 9,
- /* minExtServicesVersionR=*/ 11
)
)
- val adIdManager =
- mockAdIdManager(
- mContext,
- mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
- )
-
- when (mValidAdExtServicesSdkExtVersionR) {
- true -> setupResponseR(adIdManager)
- false -> setupResponseSPlus(adIdManager)
- }
+ val adIdManager = mockAdIdManager(mContext, mValidAdExtServicesSdkExtVersionS)
+ setupResponseSPlus(adIdManager)
val managerCompat = AdIdManagerFutures.from(mContext)
@@ -117,11 +104,7 @@
// Verify that the result of the compat call is correct.
verifyResponse(result.get())
-
- when (mValidAdExtServicesSdkExtVersionR) {
- true -> verifyOnR(adIdManager)
- false -> verifyOnSPlus(adIdManager)
- }
+ verifyOnSPlus(adIdManager)
}
@SdkSuppress(minSdkVersion = 30)
@@ -157,37 +140,6 @@
)
}
- private fun setupResponseR(adIdManager: AdIdManager) {
- // Set up the response that AdIdManager will return when the compat code calls it.
- val adId = android.adservices.adid.AdId("1234", false)
- val answer = { args: InvocationOnMock ->
- assertNotEquals(Looper.getMainLooper(), Looper.myLooper())
- val receiver =
- args.getArgument<
- AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>
- >(
- 1
- )
- receiver.onResult(adId)
- null
- }
- Mockito.doAnswer(answer)
- .`when`(adIdManager)
- .getAdId(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
- )
- }
-
- private fun verifyOnR(adIdManager: AdIdManager) {
- // Verify that the compat code was invoked correctly.
- Mockito.verify(adIdManager)
- .getAdId(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
- )
- }
-
private fun verifyOnSPlus(adIdManager: AdIdManager) {
// Verify that the compat code was invoked correctly.
Mockito.verify(adIdManager)
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
index 8eea312..2df8a7a 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -115,17 +115,6 @@
}
}
- public void enableBackCompatOnR() {
- runShellCommand("device_config put adservices adservice_enabled true");
- runShellCommand("device_config put adservices enable_back_compat true");
- }
-
-
- public void disableBackCompatOnR() {
- runShellCommand("device_config put adservices adservice_enabled false");
- runShellCommand("device_config put adservices enable_back_compat false");
- }
-
public void enableBackCompatOnS() {
runShellCommand("device_config put adservices enable_back_compat true");
runShellCommand("device_config put adservices consent_source_of_truth 3");
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
index dd54c93..dc17deb 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
@@ -80,8 +80,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 4,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
AdIdManagerFutures adIdManager =
AdIdManagerFutures.from(ApplicationProvider.getApplicationContext());
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
index fc16065..7f45668 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
@@ -96,8 +96,6 @@
MeasurementManagerFutures.from(ApplicationProvider.getApplicationContext());
if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
mTestUtil.enableBackCompatOnS();
- } else if (VersionCompatUtil.INSTANCE.isRWithMinExtServicesVersion(11)) {
- mTestUtil.enableBackCompatOnR();
}
// Put in a short sleep to make sure the updated config propagates
@@ -115,8 +113,6 @@
mTestUtil.overrideDisableMeasurementEnrollmentCheck("0");
if (VersionCompatUtil.INSTANCE.isSWithMinExtServicesVersion(9)) {
mTestUtil.disableBackCompatOnS();
- } else if (VersionCompatUtil.INSTANCE.isRWithMinExtServicesVersion(11)) {
- mTestUtil.disableBackCompatOnR();
}
// Cool-off rate limiter
@@ -129,8 +125,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
assertThat(
mMeasurementManager
@@ -146,8 +141,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
SourceRegistrationRequest request =
new SourceRegistrationRequest.Builder(
@@ -162,8 +156,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
assertThat(mMeasurementManager.registerTriggerAsync(TRIGGER_REGISTRATION_URI).get())
.isNotNull();
@@ -175,8 +168,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
WebSourceParams webSourceParams = new WebSourceParams(SOURCE_REGISTRATION_URI, false);
@@ -199,8 +191,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
WebTriggerParams webTriggerParams = new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
WebTriggerRegistrationRequest webTriggerRegistrationRequest =
@@ -236,8 +227,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
DeletionRequest deletionRequest =
new DeletionRequest.Builder(
@@ -258,8 +248,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
DeletionRequest deletionRequest =
new DeletionRequest.Builder(
@@ -283,8 +272,7 @@
Assume.assumeTrue(
VersionCompatUtil.INSTANCE.isTestableVersion(
/* minAdServicesVersion= */ 5,
- /* minExtServicesVersionS= */ 9,
- /* minExtServicesVersionR= */ 11));
+ /* minExtServicesVersionS= */ 9));
int result = mMeasurementManager.getMeasurementApiStatusAsync().get();
assertThat(result).isEqualTo(1);
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
index 363191c..eeae81f 100644
--- a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.java.measurement
-import android.adservices.common.AdServicesOutcomeReceiver
import android.adservices.measurement.MeasurementManager
import android.content.Context
import android.net.Uri
@@ -75,16 +74,12 @@
private var mSession: StaticMockitoSession? = null
private val mValidAdExtServicesSdkExtVersionS =
VersionCompatUtil.isSWithMinExtServicesVersion(9)
- private val mValidAdExtServicesSdkExtVersionR =
- VersionCompatUtil.isRWithMinExtServicesVersion(11)
- private val mValidExtServicesVersion =
- mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
@Before
fun setUp() {
mContext = spy(ApplicationProvider.getApplicationContext<Context>())
- if (mValidExtServicesVersion) {
+ if (mValidAdExtServicesSdkExtVersionS) {
// setup a mockitoSession to return the mocked manager
// when the static method .get() is called
mSession =
@@ -104,11 +99,10 @@
@SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
fun testMeasurementOlderVersions() {
Assume.assumeFalse(
- "maxSdkVersion = API 33 ext 4 or API 31/32 ext 8 or API 30 ext 10",
+ "maxSdkVersion = API 33 ext 4 or API 31/32 ext 8",
VersionCompatUtil.isTestableVersion(
/* minAdServicesVersion=*/ 5,
/* minExtServicesVersionS=*/ 9,
- /* minExtServicesVersionR=*/ 11
)
)
assertThat(from(mContext)).isEqualTo(null)
@@ -556,410 +550,6 @@
)
}
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testDeleteRegistrationsAsyncOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = from(mContext)
-
- // Set up the request.
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- null
- }
- doAnswer(answer)
- .`when`(mMeasurementManager)
- .deleteRegistrations(
- any<android.adservices.measurement.DeletionRequest>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, java.lang.Exception>>()
- )
-
- // Actually invoke the compat code.
- val request =
- DeletionRequest(
- DeletionRequest.DELETION_MODE_ALL,
- DeletionRequest.MATCH_BEHAVIOR_DELETE,
- Instant.now(),
- Instant.now(),
- listOf(uri1),
- listOf(uri1)
- )
-
- managerCompat!!.deleteRegistrationsAsync(request).get()
-
- // Verify that the compat code was invoked correctly.
- val captor =
- ArgumentCaptor.forClass(android.adservices.measurement.DeletionRequest::class.java)
- verify(mMeasurementManager)
- .deleteRegistrations(
- captor.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, java.lang.Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- verifyDeletionRequest(captor.value)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterSourceAsyncOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val inputEvent = mock(InputEvent::class.java)
- val managerCompat = from(mContext)
-
- val answer = { args: InvocationOnMock ->
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(mMeasurementManager)
- .registerSource(
- any<Uri>(),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Actually invoke the compat code.
- managerCompat!!.registerSourceAsync(uri1, inputEvent).get()
-
- // Verify that the compat code was invoked correctly.
- val captor1 = ArgumentCaptor.forClass(Uri::class.java)
- val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
- verify(mMeasurementManager)
- .registerSource(
- captor1.capture(),
- captor2.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- assertThat(captor1.value == uri1)
- assertThat(captor2.value == inputEvent)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterTriggerAsyncOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = from(mContext)
-
- val answer = { args: InvocationOnMock ->
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(mMeasurementManager)
- .registerTrigger(
- any<Uri>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Actually invoke the compat code.
- managerCompat!!.registerTriggerAsync(uri1).get()
-
- // Verify that the compat code was invoked correctly.
- val captor1 = ArgumentCaptor.forClass(Uri::class.java)
- verify(mMeasurementManager)
- .registerTrigger(
- captor1.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- assertThat(captor1.value == uri1)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterWebSourceAsyncOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = from(mContext)
-
- val answer = { args: InvocationOnMock ->
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(mMeasurementManager)
- .registerWebSource(
- any<android.adservices.measurement.WebSourceRegistrationRequest>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- val request =
- WebSourceRegistrationRequest.Builder(listOf(WebSourceParams(uri2, false)), uri1)
- .setAppDestination(appDestination)
- .build()
-
- // Actually invoke the compat code.
- managerCompat!!.registerWebSourceAsync(request).get()
-
- // Verify that the compat code was invoked correctly.
- val captor1 =
- ArgumentCaptor.forClass(
- android.adservices.measurement.WebSourceRegistrationRequest::class.java
- )
- verify(mMeasurementManager)
- .registerWebSource(
- captor1.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- val actualRequest = captor1.value
- assertThat(actualRequest.topOriginUri == uri1)
- assertThat(actualRequest.sourceParams.size == 1)
- assertThat(actualRequest.appDestination == appDestination)
- assertThat(actualRequest.sourceParams[0].registrationUri == uri2)
- assertThat(!actualRequest.sourceParams[0].isDebugKeyAllowed)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterWebTriggerAsyncOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = from(mContext)
-
- val answer = { args: InvocationOnMock ->
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(mMeasurementManager)
- .registerWebTrigger(
- any<android.adservices.measurement.WebTriggerRegistrationRequest>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- val request = WebTriggerRegistrationRequest(listOf(WebTriggerParams(uri1, false)), uri2)
-
- // Actually invoke the compat code.
- managerCompat!!.registerWebTriggerAsync(request).get()
-
- // Verify that the compat code was invoked correctly.
- val captor1 =
- ArgumentCaptor.forClass(
- android.adservices.measurement.WebTriggerRegistrationRequest::class.java
- )
- verify(mMeasurementManager)
- .registerWebTrigger(
- captor1.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- val actualRequest = captor1.value
- assertThat(actualRequest.destination == uri2)
- assertThat(actualRequest.triggerParams.size == 1)
- assertThat(actualRequest.triggerParams[0].registrationUri == uri1)
- assertThat(!actualRequest.triggerParams[0].isDebugKeyAllowed)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testMeasurementApiStatusAsyncOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = from(mContext)
-
- val state = MeasurementManager.MEASUREMENT_API_STATE_DISABLED
- val answer = { args: InvocationOnMock ->
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Int, Exception>>(1)
- receiver.onResult(state)
- null
- }
- doAnswer(answer)
- .`when`(mMeasurementManager)
- .getMeasurementApiStatus(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Int, Exception>>()
- )
-
- // Actually invoke the compat code.
- val result = managerCompat!!.getMeasurementApiStatusAsync()
- result.get()
-
- // Verify that the compat code was invoked correctly.
- verify(mMeasurementManager)
- .getMeasurementApiStatus(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Int, Exception>>()
- )
-
- // Verify that the result.
- assertThat(result.get() == state)
- }
-
- @ExperimentalFeatures.RegisterSourceOptIn
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterSourceAsync_allSuccessOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val inputEvent = mock(InputEvent::class.java)
- val managerCompat = from(mContext)
-
- val successCallback = { args: InvocationOnMock ->
- assertNotEquals(Looper.myLooper(), Looper.getMainLooper())
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onResult(Object())
- null
- }
- doAnswer(successCallback)
- .`when`(mMeasurementManager)
- .registerSource(
- any<Uri>(),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Actually invoke the compat code.
- val request =
- SourceRegistrationRequest.Builder(listOf(uri1, uri2)).setInputEvent(inputEvent).build()
- managerCompat!!.registerSourceAsync(request).get()
-
- // Verify that the compat code was invoked correctly.
- verify(mMeasurementManager)
- .registerSource(
- eq(uri1),
- eq(inputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- verify(mMeasurementManager)
- .registerSource(
- eq(uri2),
- eq(inputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
-
- @ExperimentalFeatures.RegisterSourceOptIn
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterSource_15thOf20Fails_atLeast15thExecutesOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val mMeasurementManager =
- mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val mockInputEvent = mock(InputEvent::class.java)
- val managerCompat = from(mContext)
-
- val successCallback = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onResult(Object())
- null
- }
-
- val errorMessage = "some error occurred"
- val errorCallback = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onError(IllegalArgumentException(errorMessage))
- null
- }
-
- val uris =
- (1..20)
- .map { i ->
- val uri = Uri.parse("www.uri$i.com")
- if (i == 15) {
- doAnswer(errorCallback)
- .`when`(mMeasurementManager)
- .registerSource(
- eq(uri),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- } else {
- doAnswer(successCallback)
- .`when`(mMeasurementManager)
- .registerSource(
- eq(uri),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
- uri
- }
- .toList()
-
- val request = SourceRegistrationRequest(uris, mockInputEvent)
-
- // Actually invoke the compat code.
- runBlocking {
- try {
- withContext(Dispatchers.Main) { managerCompat!!.registerSourceAsync(request).get() }
- fail("Expected failure.")
- } catch (e: ExecutionException) {
- assertTrue(e.cause!! is IllegalArgumentException)
- assertThat(e.cause!!.message).isEqualTo(errorMessage)
- }
- }
-
- // Verify that the compat code was invoked correctly.
- // registerSource gets called 1-20 times. We cannot predict the exact number because
- // uri15 would crash asynchronously. Other uris may succeed and those threads on default
- // dispatcher won't crash.
- verify(mMeasurementManager, atLeastOnce())
- .registerSource(
- any<Uri>(),
- eq(mockInputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- verify(mMeasurementManager, atMost(20))
- .registerSource(
- any<Uri>(),
- eq(mockInputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
-
@SdkSuppress(minSdkVersion = 30)
companion object {
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
index e35df7a..5610d1a 100644
--- a/privacysandbox/ads/ads-adservices/build.gradle
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -51,10 +51,6 @@
}
android {
- buildTypes.all {
- consumerProguardFiles "proguard-rules.pro"
- }
-
compileSdk = 34
compileSdkExtension = 12
namespace "androidx.privacysandbox.ads.adservices"
diff --git a/privacysandbox/ads/ads-adservices/proguard-rules.pro b/privacysandbox/ads/ads-adservices/proguard-rules.pro
deleted file mode 100644
index e092df1..0000000
--- a/privacysandbox/ads/ads-adservices/proguard-rules.pro
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (C) 2024 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# A rule that will keep the internal ContinuationOutcomeReceiver class used to
-# work with AdServicesOutcomeReceiver on Android R
--keep class androidx.privacysandbox.ads.adservices.internal.ContinuationOutcomeReceiver {
- <methods>;
-}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
index b1b53a6..b937f1d 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.adid
-import android.adservices.common.AdServicesOutcomeReceiver
import android.content.Context
import android.os.OutcomeReceiver
import android.os.ext.SdkExtensions
@@ -53,13 +52,12 @@
private var mSession: StaticMockitoSession? = null
private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 4
private val mValidAdExtServicesSdkExtVersionS = AdServicesInfo.extServicesVersionS() >= 9
- private val mValidAdExtServicesSdkExtVersionR = AdServicesInfo.extServicesVersionR() >= 11
@Before
fun setUp() {
mContext = spy(ApplicationProvider.getApplicationContext<Context>())
- if (mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR) {
+ if (mValidAdExtServicesSdkExtVersionS) {
// setup a mockitoSession to return the mocked manager
// when the static method .get() is called
mSession =
@@ -79,16 +77,12 @@
fun testAdIdOlderVersions() {
Assume.assumeTrue("maxSdkVersion = API 33 ext 3", !mValidAdServicesSdkExtVersion)
Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersionS)
- Assume.assumeTrue("maxSdkVersion = API 30 ext 10", !mValidAdExtServicesSdkExtVersionR)
assertThat(AdIdManager.obtain(mContext)).isNull()
}
@Test
fun testAdIdManagerNoClassDefFoundError() {
- Assume.assumeTrue(
- "minSdkVersion = API 31/32 ext 9 or API 30 ext 11",
- mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
- )
+ Assume.assumeTrue("minSdkVersion = API 31/32 ext 9", mValidAdExtServicesSdkExtVersionS)
`when`(android.adservices.adid.AdIdManager.get(any())).thenThrow(NoClassDefFoundError())
assertThat(AdIdManager.obtain(mContext)).isNull()
@@ -96,19 +90,13 @@
@Test
fun testAdIdAsync() {
- val validExtServicesVersion =
- mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
Assume.assumeTrue(
- "minSdkVersion = API 33 ext 4 or API 31/32 ext 9 or API 30 ext 11",
- mValidAdServicesSdkExtVersion || validExtServicesVersion
+ "minSdkVersion = API 33 ext 4 or API 31/32 ext 9",
+ mValidAdServicesSdkExtVersion || mValidAdExtServicesSdkExtVersionS
)
- val adIdManager = mockAdIdManager(mContext, validExtServicesVersion)
-
- when (mValidAdExtServicesSdkExtVersionR) {
- true -> setupResponseR(adIdManager)
- false -> setupResponseSPlus(adIdManager)
- }
+ val adIdManager = mockAdIdManager(mContext, mValidAdExtServicesSdkExtVersionS)
+ setupResponseSPlus(adIdManager)
val managerCompat = AdIdManager.obtain(mContext)
@@ -116,10 +104,7 @@
val result = runBlocking { managerCompat!!.getAdId() }
// Verify that the compat code was invoked correctly.
- when (mValidAdExtServicesSdkExtVersionR) {
- true -> verifyOnR(adIdManager)
- false -> verifyOnSPlus(adIdManager)
- }
+ verifyOnSPlus(adIdManager)
// Verify that the result of the compat call is correct.
verifyResponse(result)
@@ -162,35 +147,6 @@
)
}
- private fun setupResponseR(adIdManager: android.adservices.adid.AdIdManager) {
- // Set up the response that AdIdManager will return when the compat code calls it.
- val adId = android.adservices.adid.AdId("1234", false)
- val answer = { args: InvocationOnMock ->
- val receiver =
- args.getArgument<
- AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>
- >(
- 1
- )
- receiver.onResult(adId)
- null
- }
- doAnswer(answer)
- .`when`(adIdManager)
- .getAdId(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
- )
- }
-
- private fun verifyOnR(adIdManager: android.adservices.adid.AdIdManager) {
- verify(adIdManager)
- .getAdId(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<android.adservices.adid.AdId, Exception>>()
- )
- }
-
private fun verifyOnSPlus(adIdManager: android.adservices.adid.AdIdManager) {
verify(adIdManager)
.getAdId(
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
index 77ef543..dd323de 100644
--- a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
@@ -16,7 +16,6 @@
package androidx.privacysandbox.ads.adservices.measurement
-import android.adservices.common.AdServicesOutcomeReceiver
import android.adservices.measurement.MeasurementManager
import android.content.Context
import android.net.Uri
@@ -63,15 +62,12 @@
private var mSession: StaticMockitoSession? = null
private val mValidAdServicesSdkExtVersion = AdServicesInfo.adServicesVersion() >= 5
private val mValidAdExtServicesSdkExtVersionS = AdServicesInfo.extServicesVersionS() >= 9
- private val mValidAdExtServicesSdkExtVersionR = AdServicesInfo.extServicesVersionR() >= 11
- private val mValidExtServicesVersion =
- mValidAdExtServicesSdkExtVersionS || mValidAdExtServicesSdkExtVersionR
@Before
fun setUp() {
mContext = spy(ApplicationProvider.getApplicationContext<Context>())
- if (mValidExtServicesVersion) {
+ if (mValidAdExtServicesSdkExtVersionS) {
// setup a mockitoSession to return the mocked manager
// when the static method .get() is called
mSession =
@@ -92,16 +88,12 @@
fun testMeasurementOlderVersions() {
Assume.assumeTrue("maxSdkVersion = API 33 ext 4", !mValidAdServicesSdkExtVersion)
Assume.assumeTrue("maxSdkVersion = API 31/32 ext 8", !mValidAdExtServicesSdkExtVersionS)
- Assume.assumeTrue("maxSdkVersion = API 30 ext 10", !mValidAdExtServicesSdkExtVersionR)
assertThat(obtain(mContext)).isNull()
}
@Test
fun testMeasurementManagerNoClassDefFoundError() {
- Assume.assumeTrue(
- "minSdkVersion = API 31/32 ext 9 or API 30 ext 11",
- mValidExtServicesVersion
- )
+ Assume.assumeTrue("minSdkVersion = API 31/32 ext 9", mValidAdExtServicesSdkExtVersionS)
`when`(MeasurementManager.get(any())).thenThrow(NoClassDefFoundError())
assertThat(obtain(mContext)).isNull()
@@ -503,379 +495,6 @@
)
}
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testDeleteRegistrationsOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = obtain(mContext)
-
- // Set up the request.
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(measurementManager)
- .deleteRegistrations(
- any<android.adservices.measurement.DeletionRequest>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, java.lang.Exception>>()
- )
-
- // Actually invoke the compat code.
- runBlocking {
- val request =
- DeletionRequest(
- DeletionRequest.DELETION_MODE_ALL,
- DeletionRequest.MATCH_BEHAVIOR_DELETE,
- Instant.now(),
- Instant.now(),
- listOf(uri1),
- listOf(uri1)
- )
-
- managerCompat!!.deleteRegistrations(request)
- }
-
- // Verify that the compat code was invoked correctly.
- val captor =
- ArgumentCaptor.forClass(android.adservices.measurement.DeletionRequest::class.java)
- verify(measurementManager)
- .deleteRegistrations(
- captor.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, java.lang.Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- verifyDeletionRequest(captor.value)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterSourceOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val inputEvent = mock(InputEvent::class.java)
- val managerCompat = obtain(mContext)
-
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(measurementManager)
- .registerSource(
- any<Uri>(),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Actually invoke the compat code.
- runBlocking { managerCompat!!.registerSource(uri1, inputEvent) }
-
- // Verify that the compat code was invoked correctly.
- val captor1 = ArgumentCaptor.forClass(Uri::class.java)
- val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
- verify(measurementManager)
- .registerSource(
- captor1.capture(),
- captor2.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- assertThat(captor1.value == uri1)
- assertThat(captor2.value == inputEvent)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterTriggerOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = obtain(mContext)
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(measurementManager)
- .registerTrigger(
- any<Uri>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Actually invoke the compat code.
- runBlocking { managerCompat!!.registerTrigger(uri1) }
-
- // Verify that the compat code was invoked correctly.
- val captor1 = ArgumentCaptor.forClass(Uri::class.java)
- verify(measurementManager)
- .registerTrigger(
- captor1.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- assertThat(captor1.value).isEqualTo(uri1)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterWebSourceOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = obtain(mContext)
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(measurementManager)
- .registerWebSource(
- any<android.adservices.measurement.WebSourceRegistrationRequest>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- val request =
- WebSourceRegistrationRequest.Builder(listOf(WebSourceParams(uri1, false)), uri1)
- .setAppDestination(appDestination)
- .build()
-
- // Actually invoke the compat code.
- runBlocking { managerCompat!!.registerWebSource(request) }
-
- // Verify that the compat code was invoked correctly.
- val captor1 =
- ArgumentCaptor.forClass(
- android.adservices.measurement.WebSourceRegistrationRequest::class.java
- )
- verify(measurementManager)
- .registerWebSource(
- captor1.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- val actualRequest = captor1.value
- assertThat(actualRequest.topOriginUri == uri1)
- assertThat(actualRequest.sourceParams.size == 1)
- assertThat(actualRequest.appDestination == appDestination)
- assertThat(actualRequest.sourceParams[0].registrationUri == uri1)
- assertThat(!actualRequest.sourceParams[0].isDebugKeyAllowed)
- }
-
- @ExperimentalFeatures.RegisterSourceOptIn
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterSource_allSuccessOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val mockInputEvent = mock(InputEvent::class.java)
- val managerCompat = obtain(mContext)
-
- val successCallback = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onResult(Object())
- null
- }
- doAnswer(successCallback)
- .`when`(measurementManager)
- .registerSource(
- any<Uri>(),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- val request = SourceRegistrationRequest(listOf(uri1, uri2), mockInputEvent)
-
- // Actually invoke the compat code.
- runBlocking { managerCompat!!.registerSource(request) }
-
- // Verify that the compat code was invoked correctly.
- verify(measurementManager, times(2))
- .registerSource(
- any(),
- eq(mockInputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
-
- @ExperimentalFeatures.RegisterSourceOptIn
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterSource_15thOf20Fails_remaining5DoNotExecuteOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val mockInputEvent = mock(InputEvent::class.java)
- val managerCompat = obtain(mContext)
-
- val successCallback = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onResult(Object())
- null
- }
-
- val errorMessage = "some error occurred"
- val errorCallback = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(3)
- receiver.onError(IllegalArgumentException(errorMessage))
- null
- }
- val uris =
- (0..20)
- .map { i ->
- val uri = Uri.parse("www.uri$i.com")
- if (i == 15) {
- doAnswer(errorCallback)
- .`when`(measurementManager)
- .registerSource(
- eq(uri),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- } else {
- doAnswer(successCallback)
- .`when`(measurementManager)
- .registerSource(
- eq(uri),
- any<InputEvent>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
- uri
- }
- .toList()
-
- val request = SourceRegistrationRequest(uris, mockInputEvent)
-
- // Actually invoke the compat code.
- runBlocking {
- try {
- managerCompat!!.registerSource(request)
- fail("Expected failure.")
- } catch (e: IllegalArgumentException) {
- assertThat(e.message).isEqualTo(errorMessage)
- }
- }
-
- // Verify that the compat code was invoked correctly.
- (0..15).forEach { i ->
- verify(measurementManager)
- .registerSource(
- eq(Uri.parse("www.uri$i.com")),
- eq(mockInputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
- (16..20).forEach { i ->
- verify(measurementManager, never())
- .registerSource(
- eq(Uri.parse("www.uri$i.com")),
- eq(mockInputEvent),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
- }
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testRegisterWebTriggerOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- val managerCompat = obtain(mContext)
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Any, Exception>>(2)
- receiver.onResult(Object())
- null
- }
- doAnswer(answer)
- .`when`(measurementManager)
- .registerWebTrigger(
- any<android.adservices.measurement.WebTriggerRegistrationRequest>(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- val request = WebTriggerRegistrationRequest(listOf(WebTriggerParams(uri1, false)), uri2)
-
- // Actually invoke the compat code.
- runBlocking { managerCompat!!.registerWebTrigger(request) }
-
- // Verify that the compat code was invoked correctly.
- val captor1 =
- ArgumentCaptor.forClass(
- android.adservices.measurement.WebTriggerRegistrationRequest::class.java
- )
- verify(measurementManager)
- .registerWebTrigger(
- captor1.capture(),
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Any, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- val actualRequest = captor1.value
- assertThat(actualRequest.destination).isEqualTo(uri2)
- assertThat(actualRequest.triggerParams.size == 1)
- assertThat(actualRequest.triggerParams[0].registrationUri == uri1)
- assertThat(!actualRequest.triggerParams[0].isDebugKeyAllowed)
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testMeasurementApiStatusOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
- callAndVerifyGetMeasurementApiStatusOnR(
- measurementManager,
- /* state= */ MeasurementManager.MEASUREMENT_API_STATE_ENABLED,
- /* expectedResult= */ MeasurementManager.MEASUREMENT_API_STATE_ENABLED
- )
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = 30, minSdkVersion = 30)
- fun testMeasurementApiStatusUnknownOnR() {
- Assume.assumeTrue("minSdkVersion = API 30 ext 11", mValidAdExtServicesSdkExtVersionR)
-
- val measurementManager = mockMeasurementManager(mContext, mValidAdExtServicesSdkExtVersionR)
-
- // Call with a value greater than values returned in SdkExtensions.AD_SERVICES = 5
- // Since the compat code does not know the returned state, it sets it to UNKNOWN.
- callAndVerifyGetMeasurementApiStatusOnR(
- measurementManager,
- /* state= */ 6,
- /* expectedResult= */ 5
- )
- }
-
@SdkSuppress(minSdkVersion = 30)
companion object {
@@ -925,38 +544,6 @@
assertThat(actualResult == expectedResult)
}
- private fun callAndVerifyGetMeasurementApiStatusOnR(
- measurementManager: android.adservices.measurement.MeasurementManager,
- state: Int,
- expectedResult: Int
- ) {
- val managerCompat = obtain(mContext)
- val answer = { args: InvocationOnMock ->
- val receiver = args.getArgument<AdServicesOutcomeReceiver<Int, Exception>>(1)
- receiver.onResult(state)
- null
- }
- doAnswer(answer)
- .`when`(measurementManager)
- .getMeasurementApiStatus(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Int, Exception>>()
- )
-
- // Actually invoke the compat code.
- val actualResult = runBlocking { managerCompat!!.getMeasurementApiStatus() }
-
- // Verify that the compat code was invoked correctly.
- verify(measurementManager)
- .getMeasurementApiStatus(
- any<Executor>(),
- any<AdServicesOutcomeReceiver<Int, Exception>>()
- )
-
- // Verify that the request that the compat code makes to the platform is correct.
- assertThat(actualResult == expectedResult)
- }
-
private fun verifyDeletionRequest(request: android.adservices.measurement.DeletionRequest) {
// Set up the request that we expect the compat code to invoke.
val expectedRequest =
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 a0eb9df..df5782a 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
@@ -57,10 +57,6 @@
BackCompatManager.getManager(context, "AdIdManager") {
AdIdManagerApi31Ext9Impl(context)
}
- } else if (AdServicesInfo.extServicesVersionR() >= 11) {
- BackCompatManager.getManager(context, "AdIdManager") {
- AdIdManagerApi30Ext11Impl(context)
- }
} else {
null
}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi30Ext11Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi30Ext11Impl.kt
deleted file mode 100644
index 5cd2bf6..0000000
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerApi30Ext11Impl.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.privacysandbox.ads.adservices.adid
-
-import android.adservices.common.AdServicesPermissions
-import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Build
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
-import androidx.annotation.RequiresPermission
-import androidx.annotation.RestrictTo
-import androidx.privacysandbox.ads.adservices.internal.asAdServicesOutcomeReceiver
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("ClassVerificationFailure", "NewApi")
-@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
-open class AdIdManagerApi30Ext11Impl(context: Context) : AdIdManager() {
- private val mAdIdManager: android.adservices.adid.AdIdManager =
- android.adservices.adid.AdIdManager.get(context)
-
- @DoNotInline
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
- override suspend fun getAdId(): AdId {
- return convertResponse(getAdIdAsyncInternal())
- }
-
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
- private suspend fun getAdIdAsyncInternal(): android.adservices.adid.AdId =
- suspendCancellableCoroutine { continuation ->
- mAdIdManager.getAdId(Runnable::run, continuation.asAdServicesOutcomeReceiver())
- }
-
- private fun convertResponse(response: android.adservices.adid.AdId): AdId {
- return AdId(response.adId, response.isLimitAdTrackingEnabled)
- }
-}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
index a981294..b28fef0 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
@@ -38,14 +38,6 @@
}
}
- fun extServicesVersionR(): Int {
- return if (Build.VERSION.SDK_INT == 30) {
- Extensions30ExtImpl.getAdExtServicesVersionR()
- } else {
- 0
- }
- }
-
@RequiresApi(30)
private object Extensions30Impl {
@DoNotInline
@@ -55,12 +47,8 @@
@RequiresApi(30)
private object Extensions30ExtImpl {
// For ExtServices, there is no AD_SERVICES extension version, so we need to check
- // for the build version. Use S for now, but this can be changed to R when we add
- // support for R later.
+ // for the build version for S.
@DoNotInline
fun getAdExtServicesVersionS() = SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S)
-
- @DoNotInline
- fun getAdExtServicesVersionR() = SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R)
}
}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesOutcomeReceiver.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesOutcomeReceiver.kt
deleted file mode 100644
index 6f87e40..0000000
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesOutcomeReceiver.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.privacysandbox.ads.adservices.internal
-
-import android.adservices.common.AdServicesOutcomeReceiver
-import android.annotation.SuppressLint
-import android.os.Build
-import androidx.annotation.RequiresExtension
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-
-/*
- This file is a modified version OutcomeReceiver.kt in androidx.core.os, designed to provide the same
- functionality with the AdServicesOutcomeReceiver, to keep the implementation of the backward compatible
- classes as close to identical as possible.
-*/
-
-@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
-fun <R, E : Throwable> Continuation<R>.asAdServicesOutcomeReceiver():
- AdServicesOutcomeReceiver<R, E> = ContinuationOutcomeReceiver(this)
-
-@SuppressLint("NewApi")
-@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
-private class ContinuationOutcomeReceiver<R, E : Throwable>(
- private val continuation: Continuation<R>
-) : AdServicesOutcomeReceiver<R, E>, AtomicBoolean(false) {
- @Suppress("WRONG_NULLABILITY_FOR_JAVA_OVERRIDE")
- override fun onResult(result: R) {
- // Do not attempt to resume more than once, even if the caller of the returned
- // OutcomeReceiver is buggy and tries anyway.
- if (compareAndSet(false, true)) {
- continuation.resume(result)
- }
- }
-
- override fun onError(error: E) {
- // Do not attempt to resume more than once, even if the caller of the returned
- // OutcomeReceiver is buggy and tries anyway.
- if (compareAndSet(false, true)) {
- continuation.resumeWithException(error)
- }
- }
-
- override fun toString() = "ContinuationOutcomeReceiver(outcomeReceived = ${get()})"
-}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt
index f0f6005..f796274 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/BackCompatManager.kt
@@ -29,7 +29,6 @@
Log.d(
tag,
"Unable to find adservices code, check manifest for uses-library tag, " +
- "versionR=${AdServicesInfo.extServicesVersionR()}, " +
"versionS=${AdServicesInfo.extServicesVersionS()}"
)
return null
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
index a482855..14782a9 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
@@ -100,7 +100,6 @@
@SuppressLint("ClassVerificationFailure", "NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
- @RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
internal fun convertToAdServices(): android.adservices.measurement.DeletionRequest {
return android.adservices.measurement.DeletionRequest.Builder()
.setDeletionMode(deletionMode)
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 ebfdc16..ea8584b 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
@@ -149,10 +149,6 @@
BackCompatManager.getManager(context, "MeasurementManager") {
MeasurementManagerApi31Ext9Impl(context)
}
- } else if (AdServicesInfo.extServicesVersionR() >= 11) {
- BackCompatManager.getManager(context, "MeasurementManager") {
- MeasurementManagerApi30Ext11Impl(context)
- }
} else {
null
}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi30Ext11Impl.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi30Ext11Impl.kt
deleted file mode 100644
index 86ed67c..0000000
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerApi30Ext11Impl.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.privacysandbox.ads.adservices.measurement
-
-import android.adservices.common.AdServicesPermissions
-import android.annotation.SuppressLint
-import android.content.Context
-import android.net.Uri
-import android.os.Build
-import android.view.InputEvent
-import androidx.annotation.DoNotInline
-import androidx.annotation.RequiresExtension
-import androidx.annotation.RequiresPermission
-import androidx.annotation.RestrictTo
-import androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures
-import androidx.privacysandbox.ads.adservices.internal.asAdServicesOutcomeReceiver
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("ClassVerificationFailure", "NewApi")
-@RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
-class MeasurementManagerApi30Ext11Impl(context: Context) : MeasurementManager() {
- private val mMeasurementManager: android.adservices.measurement.MeasurementManager =
- android.adservices.measurement.MeasurementManager.get(context)
-
- @DoNotInline
- override suspend fun deleteRegistrations(deletionRequest: DeletionRequest) {
- suspendCancellableCoroutine<Any> { continuation ->
- mMeasurementManager.deleteRegistrations(
- deletionRequest.convertToAdServices(),
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
- }
-
- @DoNotInline
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
- override suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?) {
- suspendCancellableCoroutine<Any> { continuation ->
- mMeasurementManager.registerSource(
- attributionSource,
- inputEvent,
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
- }
-
- @DoNotInline
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
- override suspend fun registerTrigger(trigger: Uri) {
- suspendCancellableCoroutine<Any> { continuation ->
- mMeasurementManager.registerTrigger(
- trigger,
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
- }
-
- @DoNotInline
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
- override suspend fun registerWebSource(request: WebSourceRegistrationRequest) {
- suspendCancellableCoroutine<Any> { continuation ->
- mMeasurementManager.registerWebSource(
- request.convertToAdServices(),
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
- }
-
- @DoNotInline
- @ExperimentalFeatures.RegisterSourceOptIn
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
- override suspend fun registerSource(request: SourceRegistrationRequest): Unit = coroutineScope {
- request.registrationUris.forEach { uri ->
- launch {
- suspendCancellableCoroutine<Any> { continuation ->
- mMeasurementManager.registerSource(
- uri,
- request.inputEvent,
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
- }
- }
- }
-
- @DoNotInline
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
- override suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest) {
- suspendCancellableCoroutine<Any> { continuation ->
- mMeasurementManager.registerWebTrigger(
- request.convertToAdServices(),
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
- }
-
- @DoNotInline
- @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
- override suspend fun getMeasurementApiStatus(): Int =
- suspendCancellableCoroutine { continuation ->
- mMeasurementManager.getMeasurementApiStatus(
- Runnable::run,
- continuation.asAdServicesOutcomeReceiver()
- )
- }
-}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
index c7bc6fe..1d3218f 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
@@ -54,7 +54,6 @@
@SuppressLint("ClassVerificationFailure", "NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
- @RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
internal fun convertWebSourceParams(
request: List<WebSourceParams>
): List<android.adservices.measurement.WebSourceParams> {
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
index 5d48214..6499e73 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
@@ -95,7 +95,6 @@
@SuppressLint("ClassVerificationFailure", "NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
- @RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
internal fun convertToAdServices():
android.adservices.measurement.WebSourceRegistrationRequest {
return android.adservices.measurement.WebSourceRegistrationRequest.Builder(
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
index 2163701..bc3eefc 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
@@ -54,7 +54,6 @@
@SuppressLint("ClassVerificationFailure", "NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
- @RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
internal fun convertWebTriggerParams(
request: List<WebTriggerParams>
): List<android.adservices.measurement.WebTriggerParams> {
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
index f521339..7384929 100644
--- a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
@@ -52,7 +52,6 @@
@SuppressLint("ClassVerificationFailure", "NewApi")
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 9)
- @RequiresExtension(extension = Build.VERSION_CODES.R, version = 11)
internal fun convertToAdServices():
android.adservices.measurement.WebTriggerRegistrationRequest {
return android.adservices.measurement.WebTriggerRegistrationRequest.Builder(
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index bb5cec2..fb5e7c0 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -75,7 +75,7 @@
}
androidComponents {
- onVariants(selector().withBuildType("debug")) {
+ onVariants(selector().withBuildType("release")) {
androidTest.sources.assets.addGeneratedSourceDirectory(
bundleTestSdkDexTaskProvider,
BundleTestSdkDexTask::getOutputDir
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index 1e0e45f..25f5c7b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -59,6 +59,22 @@
) {
logger.error("Error in $name: annotated interfaces cannot declare companion objects.")
}
+ if (
+ interfaceDeclaration.declarations
+ .filterIsInstance<KSClassDeclaration>()
+ .filter {
+ listOf(
+ ClassKind.OBJECT,
+ ClassKind.INTERFACE,
+ ClassKind.ENUM_CLASS,
+ ClassKind.CLASS
+ )
+ .contains(it.classKind)
+ }
+ .any { !it.isCompanionObject }
+ ) {
+ logger.error("Error in $name: annotated interfaces cannot declare objects or classes.")
+ }
val invalidModifiers =
interfaceDeclaration.modifiers.filterNot(validInterfaceModifiers::contains)
if (invalidModifiers.isNotEmpty()) {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
index 8811255..6f548dc 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
@@ -51,6 +51,7 @@
logger.error("Error in $name: annotated values should be public.")
}
ensureNoCompanion(value, name)
+ ensureNoObject(value, name)
ensureNoTypeParameters(value, name)
ensureNoSuperTypes(value, name)
@@ -86,6 +87,25 @@
}
}
+ private fun ensureNoObject(classDeclaration: KSClassDeclaration, name: String) {
+ if (
+ classDeclaration.declarations
+ .filterIsInstance<KSClassDeclaration>()
+ .filter {
+ listOf(
+ ClassKind.OBJECT,
+ ClassKind.INTERFACE,
+ ClassKind.ENUM_CLASS,
+ ClassKind.CLASS
+ )
+ .contains(it.classKind)
+ }
+ .any { !it.isCompanionObject }
+ ) {
+ logger.error("Error in $name: annotated values cannot declare objects or classes.")
+ }
+ }
+
private fun ensureNoTypeParameters(classDeclaration: KSClassDeclaration, name: String) {
if (classDeclaration.typeParameters.isNotEmpty()) {
logger.error(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index 9cb0abf..975ee9a 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -256,6 +256,71 @@
}
@Test
+ fun interfaceWithObject_fails() {
+ checkSourceFails(
+ serviceInterface(
+ """public interface MySdk {
+ | object MyObject {
+ | }
+ |}
+ """
+ .trimMargin()
+ )
+ )
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdk: annotated interfaces cannot declare objects or classes."
+ )
+ }
+
+ @Test
+ fun interfaceWithEnumClass_fails() {
+ checkSourceFails(
+ serviceInterface(
+ """public interface MySdk {
+ | enum class MyEnumClass {}
+ |}
+ """
+ .trimMargin()
+ )
+ )
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdk: annotated interfaces cannot declare objects or classes."
+ )
+ }
+
+ @Test
+ fun interfaceWithInterface_fails() {
+ checkSourceFails(
+ serviceInterface(
+ """public interface MySdk {
+ | private interface MyInterface {}
+ |}
+ """
+ .trimMargin()
+ )
+ )
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdk: annotated interfaces cannot declare objects or classes."
+ )
+ }
+
+ @Test
+ fun interfaceWithInnerClass_fails() {
+ checkSourceFails(
+ serviceInterface(
+ """public interface MySdk {
+ | class MyInnerClass {}
+ |}
+ """
+ .trimMargin()
+ )
+ )
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdk: annotated interfaces cannot declare objects or classes."
+ )
+ }
+
+ @Test
fun interfaceWithInvalidModifier_fails() {
checkSourceFails(
serviceInterface(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index 3a486ea..88949a2 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -218,6 +218,64 @@
}
@Test
+ fun dataClassWithObject_fails() {
+ val dataClass =
+ annotatedValue(
+ """
+ |data class MySdkRequest(val id: Int) {
+ | object Constants {
+ | val someConstant = 12
+ | }
+ |}
+ """
+ .trimMargin()
+ )
+ checkSourceFails(dataClass)
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdkRequest: annotated values cannot declare objects or " +
+ "classes."
+ )
+ }
+
+ @Test
+ fun dataClassWithInnerClass_fails() {
+ val dataClass =
+ annotatedValue(
+ """
+ |data class MySdkRequest(val id: Int) {
+ | class MyClass {
+ | val someConstant = 12
+ | }
+ |}
+ """
+ .trimMargin()
+ )
+ checkSourceFails(dataClass)
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdkRequest: annotated values cannot declare objects or " +
+ "classes."
+ )
+ }
+
+ @Test
+ fun dataClassWithEnumClass_fails() {
+ val dataClass =
+ annotatedValue(
+ """
+ |data class MySdkRequest(val id: Int) {
+ | enum class MyClass { RED, GREEN }
+ |}
+ """
+ .trimMargin()
+ )
+ checkSourceFails(dataClass)
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdkRequest: annotated values cannot declare objects or " +
+ "classes."
+ )
+ }
+
+ @Test
fun dataClassWithTypeParameters_fails() {
val dataClass = annotatedValue("data class MySdkRequest<T>(val id: Int, val data: T)")
checkSourceFails(dataClass)
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle b/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
index 2a9b90d9c..323d9a0 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
@@ -29,4 +29,7 @@
implementation project(':privacysandbox:ui:ui-provider')
implementation project(':privacysandbox:ui:integration-tests:testaidl')
implementation project(':webkit:webkit')
+ implementation(libs.media3Ui)
+ implementation(libs.media3Common)
+ implementation(libs.media3Exoplayer)
}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/MediateeSdkApiImpl.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/MediateeSdkApiImpl.kt
index e647c7a..c19d38b 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/MediateeSdkApiImpl.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/MediateeSdkApiImpl.kt
@@ -40,8 +40,9 @@
): Bundle {
val adapter: SandboxedUiAdapter =
when (adType) {
- AdType.WEBVIEW -> loadWebViewBannerAd()
+ AdType.BASIC_WEBVIEW -> loadWebViewBannerAd()
AdType.WEBVIEW_FROM_LOCAL_ASSETS -> loadWebViewBannerAdFromLocalAssets()
+ AdType.NON_WEBVIEW_VIDEO -> loadVideoAd()
else -> loadNonWebViewBannerAd(mediationDescription, waitInsideOnDraw)
}
ViewabilityHandler.addObserverFactoryToAdapter(adapter, drawViewability)
@@ -56,6 +57,13 @@
return testAdapters.WebViewAdFromLocalAssets()
}
+ private fun loadVideoAd(): SandboxedUiAdapter {
+ val playerViewProvider = PlayerViewProvider()
+ val adapter = testAdapters.VideoBannerAd(playerViewProvider)
+ PlayerViewabilityHandler.addObserverFactoryToAdapter(adapter, playerViewProvider)
+ return adapter
+ }
+
private fun loadNonWebViewBannerAd(
text: String,
waitInsideOnDraw: Boolean
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/PlayerViewProvider.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/PlayerViewProvider.kt
new file mode 100644
index 0000000..efa13eb
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/PlayerViewProvider.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.integration.sdkproviderutils
+
+import android.content.Context
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.View
+import androidx.media3.common.AudioAttributes
+import androidx.media3.common.C
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.PlayerView
+import java.util.WeakHashMap
+
+/** Create PlayerView with Player and controlling playback based on player visibility. */
+class PlayerViewProvider {
+
+ private val handler = Handler(Looper.getMainLooper())
+ private val createdViews = WeakHashMap<PlayerView, PlayerWithState>()
+
+ fun createPlayerView(windowContext: Context, videoUrl: String): View {
+ val viewId = View.generateViewId()
+
+ val view = PlayerView(windowContext)
+ view.id = viewId
+
+ val playerWithState = PlayerWithState(windowContext, videoUrl)
+ createdViews[view] = playerWithState
+
+ return view
+ }
+
+ fun onPlayerVisible(id: Int) {
+ Log.i(TAG, "onPlayerVisible: $id")
+ handler.post {
+ for ((view: PlayerView, state: PlayerWithState) in createdViews) {
+ if (view.player == null) {
+ val player = state.initializePlayer()
+ view.setPlayer(player)
+ }
+ if (view.id == id && view.player?.isPlaying == false) {
+ Log.i(TAG, "onPlayerVisible: resuming $id")
+ view.player?.play()
+ }
+ }
+ }
+ }
+
+ fun onPlayerInvisible(id: Int) {
+ Log.i(TAG, "onPlayerInVisible: $id")
+ handler.post {
+ for ((view: PlayerView, _: PlayerWithState) in createdViews) {
+ if (view.id == id) {
+ Log.i(TAG, "onPlayerInVisible: pausing $id")
+ view.player?.pause()
+ }
+ }
+ }
+ }
+
+ fun onSessionClosed() {
+ Log.i(TAG, "onSessionClosed, releasing player resources")
+ handler.post {
+ for ((view: PlayerView, state: PlayerWithState) in createdViews) {
+ state.releasePlayer()
+ view.setPlayer(null)
+ }
+ }
+ createdViews.clear()
+ }
+
+ inner class PlayerWithState(private val context: Context, private val videoUrl: String) {
+ private var player: ExoPlayer? = null
+ private var autoPlay = true
+ private var autoPlayPosition: Long
+
+ init {
+ autoPlayPosition = C.TIME_UNSET
+ }
+
+ fun initializePlayer(): Player? {
+ player?.let {
+ return it
+ }
+
+ val audioAttributes =
+ AudioAttributes.Builder()
+ .setUsage(C.USAGE_MEDIA)
+ .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
+ .build()
+
+ player = ExoPlayer.Builder(context).setAudioAttributes(audioAttributes, true).build()
+ player?.apply {
+ setPlayWhenReady(autoPlay)
+ setMediaItem(MediaItem.fromUri(Uri.parse(videoUrl)))
+ val hasStartPosition = autoPlayPosition != C.TIME_UNSET
+ if (hasStartPosition) {
+ seekTo(0, autoPlayPosition)
+ }
+ prepare()
+ }
+
+ return player
+ }
+
+ fun releasePlayer() {
+ player?.run {
+ autoPlay = playWhenReady
+ autoPlayPosition = contentPosition
+ release()
+ }
+ player = null
+ }
+ }
+
+ private companion object {
+ const val TAG = "PlayerViewProvider"
+ }
+}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/PlayerViewabilityHandler.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/PlayerViewabilityHandler.kt
new file mode 100644
index 0000000..25a3851
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/PlayerViewabilityHandler.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.integration.sdkproviderutils
+
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
+
+class PlayerViewabilityHandler {
+
+ private class SessionObserverFactoryImpl(val playerViewProvider: PlayerViewProvider) :
+ SessionObserverFactory {
+
+ override fun create(): SessionObserver {
+ return SessionObserverImpl(playerViewProvider)
+ }
+
+ private inner class SessionObserverImpl(val playerViewProvider: PlayerViewProvider) :
+ SessionObserver {
+ lateinit var view: View
+ var isPlayerVisible = false
+
+ override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
+ Log.i(TAG, "onSessionOpened $sessionObserverContext")
+ view = checkNotNull(sessionObserverContext.view)
+ }
+
+ override fun onUiContainerChanged(uiContainerInfo: Bundle) {
+ val sandboxedSdkViewUiInfo = SandboxedSdkViewUiInfo.fromBundle(uiContainerInfo)
+ Log.i(TAG, "onUiContainerChanged $sandboxedSdkViewUiInfo")
+
+ val updatedVisibility = !sandboxedSdkViewUiInfo.onScreenGeometry.isEmpty
+ if (updatedVisibility != isPlayerVisible) {
+ Log.i(
+ TAG,
+ "Video player previous visibility $isPlayerVisible, updated visibility $updatedVisibility"
+ )
+ isPlayerVisible = updatedVisibility
+ if (isPlayerVisible) {
+ playerViewProvider.onPlayerVisible(view.id)
+ } else {
+ playerViewProvider.onPlayerInvisible(view.id)
+ }
+ }
+ }
+
+ override fun onSessionClosed() {
+ Log.i(TAG, "session closed")
+ playerViewProvider.onSessionClosed()
+ }
+ }
+ }
+
+ companion object {
+
+ private val TAG = PlayerViewabilityHandler::class.simpleName
+
+ fun addObserverFactoryToAdapter(
+ adapter: SandboxedUiAdapter,
+ playerViewProvider: PlayerViewProvider
+ ) {
+ return adapter.addObserverFactory(SessionObserverFactoryImpl(playerViewProvider))
+ }
+ }
+}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt
index 19b8b01..66da2c5 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt
@@ -21,9 +21,10 @@
companion object {
annotation class AdType {
companion object {
- const val NON_WEBVIEW = 0
- const val WEBVIEW = 1
+ const val BASIC_NON_WEBVIEW = 0
+ const val BASIC_WEBVIEW = 1
const val WEBVIEW_FROM_LOCAL_ASSETS = 2
+ const val NON_WEBVIEW_VIDEO = 3
}
}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
index 7c41a22..b613689 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
@@ -126,6 +126,16 @@
}
}
+ inner class VideoBannerAd(private val playerViewProvider: PlayerViewProvider) : BannerAd() {
+
+ override fun buildAdView(sessionContext: Context): View {
+ return playerViewProvider.createPlayerView(
+ sessionContext,
+ "https://html5demos.com/assets/dizzy.mp4"
+ )
+ }
+ }
+
inner class WebViewAdFromLocalAssets : BannerAd() {
override fun buildAdView(sessionContext: Context): View {
val webView = WebView(sessionContext)
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 47a3592..86fed41 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -135,7 +135,7 @@
"androidx.privacysandbox.ui.integration.mediateesdkprovider"
const val TAG = "TestSandboxClient"
var isZOrderOnTop = true
- @AdType var currentAdType = AdType.NON_WEBVIEW
+ @AdType var currentAdType = AdType.BASIC_NON_WEBVIEW
@MediationOption var currentMediationOption = MediationOption.NON_MEDIATED
var shouldDrawViewabilityLayer = false
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index a54d6f1..02d4cec 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -49,12 +49,13 @@
private lateinit var navigationView: NavigationView
private lateinit var currentFragment: BaseFragment
private lateinit var triggerSandboxDeathButton: Button
- private lateinit var webViewToggleButton: SwitchMaterial
private lateinit var zOrderToggleButton: SwitchMaterial
- private lateinit var contentFromAssetsToggleButton: SwitchMaterial
private lateinit var viewabilityToggleButton: SwitchMaterial
private lateinit var mediationDropDownMenu: Spinner
- @AdType private var adType = AdType.NON_WEBVIEW
+ private lateinit var adTypeDropDownMenu: Spinner
+
+ @AdType private var adType = AdType.BASIC_NON_WEBVIEW
+
@MediationOption private var mediationOption = MediationOption.NON_MEDIATED
private var drawViewabilityLayer = false
@@ -65,12 +66,11 @@
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawer)
navigationView = findViewById(R.id.navigation_view)
- contentFromAssetsToggleButton = findViewById(R.id.content_from_assets_switch)
zOrderToggleButton = findViewById(R.id.zorder_below_switch)
- webViewToggleButton = findViewById(R.id.load_webview)
viewabilityToggleButton = findViewById(R.id.display_viewability_switch)
triggerSandboxDeathButton = findViewById(R.id.trigger_sandbox_death)
mediationDropDownMenu = findViewById(R.id.mediation_dropdown_menu)
+ adTypeDropDownMenu = findViewById(R.id.ad_type_dropdown_menu)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// there is no sandbox to kill on T-
@@ -139,59 +139,26 @@
}
private fun initializeToggles() {
- initializeWebViewToggleSwitch()
- initializeContentFromAssetsToggleButton()
initializeViewabilityToggleButton()
initializeMediationDropDown()
+ initializeAdTypeDropDown()
initializeZOrderToggleButton()
}
private fun disableAllControls() {
- webViewToggleButton.isEnabled = false
- contentFromAssetsToggleButton.isEnabled = false
mediationDropDownMenu.isEnabled = false
+ adTypeDropDownMenu.isEnabled = false
viewabilityToggleButton.isEnabled = false
zOrderToggleButton.isEnabled = false
}
private fun enableAllControls() {
- webViewToggleButton.isEnabled = true
- contentFromAssetsToggleButton.isEnabled = webViewToggleButton.isChecked
mediationDropDownMenu.isEnabled = true
+ adTypeDropDownMenu.isEnabled = true
viewabilityToggleButton.isEnabled = true
zOrderToggleButton.isEnabled = true
}
- private fun initializeWebViewToggleSwitch() {
- contentFromAssetsToggleButton.isEnabled = false
- webViewToggleButton.setOnCheckedChangeListener { _, isChecked ->
- contentFromAssetsToggleButton.isEnabled = isChecked
- adType =
- if (isChecked) {
- if (contentFromAssetsToggleButton.isChecked) {
- AdType.WEBVIEW_FROM_LOCAL_ASSETS
- } else {
- AdType.WEBVIEW
- }
- } else {
- AdType.NON_WEBVIEW
- }
- loadAllAds()
- }
- }
-
- private fun initializeContentFromAssetsToggleButton() {
- contentFromAssetsToggleButton.setOnCheckedChangeListener { _, isChecked ->
- adType =
- if (isChecked) {
- AdType.WEBVIEW_FROM_LOCAL_ASSETS
- } else {
- AdType.WEBVIEW
- }
- loadAllAds()
- }
- }
-
private fun initializeViewabilityToggleButton() {
viewabilityToggleButton.setOnCheckedChangeListener { _, isChecked ->
drawViewabilityLayer = isChecked
@@ -252,6 +219,46 @@
}
}
+ private fun initializeAdTypeDropDown() {
+ ArrayAdapter.createFromResource(
+ applicationContext,
+ R.array.ad_type_dropdown_menu_array,
+ android.R.layout.simple_spinner_item
+ )
+ .also { adapter ->
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ adTypeDropDownMenu.adapter = adapter
+ }
+
+ adTypeDropDownMenu.onItemSelectedListener =
+ object : AdapterView.OnItemSelectedListener {
+ var isCalledOnStartingApp = true
+
+ override fun onItemSelected(
+ parent: AdapterView<*>?,
+ view: View?,
+ position: Int,
+ selectedAdOptionId: Long
+ ) {
+ if (isCalledOnStartingApp) {
+ isCalledOnStartingApp = false
+ return
+ }
+ adType =
+ when (position) {
+ 0 -> AdType.BASIC_NON_WEBVIEW
+ 1 -> AdType.BASIC_WEBVIEW
+ 2 -> AdType.WEBVIEW_FROM_LOCAL_ASSETS
+ 3 -> AdType.NON_WEBVIEW_VIDEO
+ else -> AdType.BASIC_NON_WEBVIEW
+ }
+ loadAllAds()
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {}
+ }
+ }
+
private fun initializeZOrderToggleButton() {
zOrderToggleButton.setOnCheckedChangeListener { _, isChecked ->
BaseFragment.isZOrderOnTop = !isChecked
@@ -264,7 +271,6 @@
if (drawerLayout.isOpen) {
drawerLayout.closeDrawers()
} else {
- currentFragment.handleDrawerStateChange(true)
drawerLayout.open()
}
}
@@ -273,14 +279,20 @@
private fun initializeDrawer() {
drawerLayout.addDrawerListener(
object : DrawerListener {
- override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
+ private var isDrawerOpen = false
- override fun onDrawerOpened(drawerView: View) {
- // we handle this in the button onClick instead
+ override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
+ if (!isDrawerOpen) {
+ isDrawerOpen = true
+ currentFragment.handleDrawerStateChange(isDrawerOpen = true)
+ }
}
+ override fun onDrawerOpened(drawerView: View) {}
+
override fun onDrawerClosed(drawerView: View) {
- currentFragment.handleDrawerStateChange(false)
+ isDrawerOpen = false
+ currentFragment.handleDrawerStateChange(isDrawerOpen = false)
}
override fun onDrawerStateChanged(newState: Int) {}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 0779727..6b72533 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -16,33 +16,38 @@
-->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
+
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
+
<Button
+ android:id="@+id/toggle_drawer_button"
android:layout_width="match_parent"
android:layout_height="50dp"
- android:id="@+id/toggle_drawer_button"
- android:text="Open Options"/>
+ android:text="Open Options" />
+
<FrameLayout
android:id="@+id/content_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_weight="1"/>
+ android:layout_weight="1" />
</LinearLayout>
+
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@layout/action_menu">
+
<Spinner
android:id="@+id/mediation_dropdown_menu"
android:layout_width="match_parent"
@@ -50,44 +55,40 @@
android:layout_gravity="center"
android:background="@android:drawable/btn_dropdown"
android:spinnerMode="dropdown" />
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/load_webview"
- android:layout_width="wrap_content"
+
+ <Spinner
+ android:id="@+id/ad_type_dropdown_menu"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_gravity="center"
android:layout_marginTop="50dp"
- android:layout_gravity="center"
- android:text="@string/webview_switch"
- android:checked="false" />
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/content_from_assets_switch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="100dp"
- android:layout_gravity="center"
- android:text="@string/content_from_assets_switch"
- android:checked="false" />
+ android:background="@android:drawable/btn_dropdown"
+ android:spinnerMode="dropdown" />
+
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/display_viewability_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="150dp"
android:layout_gravity="center"
- android:text="@string/display_viewability_switch"
- android:checked="false"/>
+ android:layout_marginTop="100dp"
+ android:checked="false"
+ android:text="@string/display_viewability_switch" />
+
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/zorder_below_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="200dp"
android:layout_gravity="center"
- android:text="@string/zorder_below_switch"
- android:checked="false" />
+ android:layout_marginTop="150dp"
+ android:checked="false"
+ android:text="@string/zorder_below_switch" />
+
<Button
+ android:id="@+id/trigger_sandbox_death"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="250dp"
android:layout_gravity="center"
- android:id="@+id/trigger_sandbox_death"
- android:text="@string/trigger_sandbox_death_button"/>
+ android:layout_marginTop="200dp"
+ android:text="@string/trigger_sandbox_death_button" />
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/pdf/pdf-viewer/src/main/res/values/styles.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/ad_type_options.xml
similarity index 68%
rename from pdf/pdf-viewer/src/main/res/values/styles.xml
rename to privacysandbox/ui/integration-tests/testapp/src/main/res/values/ad_type_options.xml
index e752dd2..be23644 100644
--- a/pdf/pdf-viewer/src/main/res/values/styles.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/ad_type_options.xml
@@ -14,12 +14,11 @@
limitations under the License.
-->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="Label">
- <item name="android:textColor">@color/text_default</item>
- </style>
-
- <style name="TextField" />
-
+<resources>
+ <string-array name="ad_type_dropdown_menu_array">
+ <item>@string/basic_non_webview_switch</item>
+ <item>@string/basic_webview_switch</item>
+ <item>@string/content_from_assets_switch</item>
+ <item>@string/video_switch</item>
+ </string-array>
</resources>
\ No newline at end of file
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
index ff7300f..3bddd28 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/strings.xml
@@ -25,7 +25,9 @@
<string name="zorder_below_switch">Z Order Below</string>
<string name="mediation_switch">Mediation</string>
<string name="app_owned_mediatee_switch">AppOwnedMediatee</string>
- <string name="webview_switch">Webview</string>
+ <string name="basic_webview_switch">Basic Webview</string>
<string name="trigger_sandbox_death_button">Trigger Sandbox Death</string>
<string name="display_viewability_switch">Display Viewability Geometry</string>
+ <string name="video_switch">Video</string>
+ <string name="basic_non_webview_switch">Basic Non-Webview</string>
</resources>
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index 054ec2d..53534f9 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -21,6 +21,8 @@
import android.os.Process
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.integration.sdkproviderutils.PlayerViewProvider
+import androidx.privacysandbox.ui.integration.sdkproviderutils.PlayerViewabilityHandler
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.MediationOption
import androidx.privacysandbox.ui.integration.sdkproviderutils.TestAdapters
@@ -52,15 +54,16 @@
}
val adapter: SandboxedUiAdapter =
when (adType) {
- AdType.NON_WEBVIEW -> {
+ AdType.BASIC_NON_WEBVIEW -> {
loadNonWebViewBannerAd("Simple Ad", waitInsideOnDraw)
}
- AdType.WEBVIEW -> {
+ AdType.BASIC_WEBVIEW -> {
loadWebViewBannerAd()
}
AdType.WEBVIEW_FROM_LOCAL_ASSETS -> {
loadWebViewBannerAdFromLocalAssets()
}
+ AdType.NON_WEBVIEW_VIDEO -> loadVideoAd()
else -> {
loadNonWebViewBannerAd("Ad type not present", waitInsideOnDraw)
}
@@ -88,6 +91,13 @@
return testAdapters.TestBannerAd(text, waitInsideOnDraw)
}
+ private fun loadVideoAd(): SandboxedUiAdapter {
+ val playerViewProvider = PlayerViewProvider()
+ val adapter = testAdapters.VideoBannerAd(playerViewProvider)
+ PlayerViewabilityHandler.addObserverFactoryToAdapter(adapter, playerViewProvider)
+ return adapter
+ }
+
override fun requestResize(width: Int, height: Int) {}
private fun maybeGetMediateeBannerAdBundle(
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index 5bec1b4..9287951 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -181,7 +181,7 @@
view.layoutParams = LinearLayout.LayoutParams(initialWidth, initialHeight)
}
- fun requestSizeChange(width: Int, height: Int) {
+ fun requestResize(width: Int, height: Int) {
internalClient?.onResizeRequested(width, height)
}
@@ -481,7 +481,7 @@
layout.addView(view)
}
testSandboxedUiAdapter.assertSessionOpened()
- testSandboxedUiAdapter.testSession?.requestSizeChange(layout.width, layout.height)
+ testSandboxedUiAdapter.testSession?.requestResize(layout.width, layout.height)
val observer = view.viewTreeObserver
observer.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
@@ -617,10 +617,10 @@
@Ignore("b/307829956")
@Test
- fun requestSizeWithMeasureSpecAtMost_withinParentBounds() {
+ fun requestResizeWithMeasureSpecAtMost_withinParentBounds() {
view.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
addViewToLayoutAndWaitToBeActive()
- requestSizeAndVerifyLayout(
+ requestResizeAndVerifyLayout(
/* requestedWidth=*/ mainLayoutWidth - 100,
/* requestedHeight=*/ mainLayoutHeight - 100,
/* expectedWidth=*/ mainLayoutWidth - 100,
@@ -629,11 +629,11 @@
}
@Test
- fun requestSizeWithMeasureSpecAtMost_exceedsParentBounds() {
+ fun requestResizeWithMeasureSpecAtMost_exceedsParentBounds() {
view.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
addViewToLayoutAndWaitToBeActive()
// the resize is constrained by the parent's size
- requestSizeAndVerifyLayout(
+ requestResizeAndVerifyLayout(
/* requestedWidth=*/ mainLayoutWidth + 100,
/* requestedHeight=*/ mainLayoutHeight + 100,
/* expectedWidth=*/ mainLayoutWidth,
@@ -642,13 +642,13 @@
}
@Test
- fun requestSizeWithMeasureSpecExactly() {
+ fun requestResizeWithMeasureSpecExactly() {
view.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addViewToLayoutAndWaitToBeActive()
val currentWidth = view.width
val currentHeight = view.height
// the request is a no-op when the MeasureSpec is EXACTLY
- requestSizeAndVerifyLayout(
+ requestResizeAndVerifyLayout(
/* requestedWidth=*/ currentWidth - 100,
/* requestedHeight=*/ currentHeight - 100,
/* expectedWidth=*/ currentWidth,
@@ -842,7 +842,7 @@
addViewToLayout(true, viewToAdd)
}
- private fun requestSizeAndVerifyLayout(
+ private fun requestResizeAndVerifyLayout(
requestedWidth: Int,
requestedHeight: Int,
expectedWidth: Int,
@@ -856,7 +856,7 @@
height = bottom - top
layoutLatch.countDown()
}
- activityScenarioRule.withActivity { view.requestSize(requestedWidth, requestedHeight) }
+ activityScenarioRule.withActivity { view.requestResize(requestedWidth, requestedHeight) }
assertThat(layoutLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
assertThat(width).isEqualTo(expectedWidth)
assertThat(height).isEqualTo(expectedHeight)
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/RemoteCallManager.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/RemoteCallManager.kt
new file mode 100644
index 0000000..6570440
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/RemoteCallManager.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.client
+
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.privacysandbox.ui.core.IRemoteSessionController
+
+/** Utility class for remote objects called by the UI library adapter factories. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object RemoteCallManager {
+
+ const val TAG = "PrivacySandboxUiLib"
+
+ fun addBinderDeathListener(
+ remoteSessionController: IRemoteSessionController,
+ recipient: IBinder.DeathRecipient
+ ) {
+ tryToCallRemoteObject(remoteSessionController) { this.asBinder().linkToDeath(recipient, 0) }
+ }
+
+ fun closeRemoteSession(remoteSessionController: IRemoteSessionController) {
+ tryToCallRemoteObject(remoteSessionController) { close() }
+ }
+
+ /** Tries to call the remote object and handles exceptions if the remote object has died. */
+ inline fun <RemoteObject> tryToCallRemoteObject(
+ remoteObject: RemoteObject,
+ function: RemoteObject.() -> Unit
+ ) {
+ try {
+ remoteObject.function()
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Calling remote object failed: $e")
+ }
+ }
+}
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
index 7504162..09be59b 100644
--- 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
@@ -23,7 +23,6 @@
import android.os.Build
import android.os.Bundle
import android.os.IBinder
-import android.os.RemoteException
import android.util.Log
import android.view.Display
import android.view.SurfaceControlViewHost
@@ -31,6 +30,9 @@
import android.view.View
import android.window.SurfaceSyncGroup
import androidx.annotation.RequiresApi
+import androidx.privacysandbox.ui.client.RemoteCallManager.addBinderDeathListener
+import androidx.privacysandbox.ui.client.RemoteCallManager.closeRemoteSession
+import androidx.privacysandbox.ui.client.RemoteCallManager.tryToCallRemoteObject
import androidx.privacysandbox.ui.core.IRemoteSessionClient
import androidx.privacysandbox.ui.core.IRemoteSessionController
import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
@@ -259,8 +261,8 @@
context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displayId = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).displayId
- tryToCallRemoteObject {
- adapterInterface.openRemoteSession(
+ tryToCallRemoteObject(adapterInterface) {
+ this.openRemoteSession(
windowInputToken,
displayId,
initialWidth,
@@ -299,8 +301,8 @@
override fun onViewAttachedToWindow(v: View) {
if (hasViewBeenPreviouslyAttached) {
- tryToCallRemoteObject {
- remoteSessionController.notifyFetchUiForSession()
+ tryToCallRemoteObject(remoteSessionController) {
+ this.notifyFetchUiForSession()
}
} else {
hasViewBeenPreviouslyAttached = true
@@ -321,10 +323,8 @@
)
)
}
- tryToCallRemoteObject {
- remoteSessionController
- .asBinder()
- .linkToDeath({ onRemoteSessionError("Remote process died") }, 0)
+ addBinderDeathListener(remoteSessionController) {
+ onRemoteSessionError("Remote process died")
}
}
@@ -361,8 +361,8 @@
}
override fun notifyConfigurationChanged(configuration: Configuration) {
- tryToCallRemoteObject {
- remoteSessionController.notifyConfigurationChanged(configuration)
+ tryToCallRemoteObject(remoteSessionController) {
+ this.notifyConfigurationChanged(configuration)
}
}
@@ -379,7 +379,9 @@
}
val providerResizeRunnable = Runnable {
- tryToCallRemoteObject { remoteSessionController.notifyResized(width, height) }
+ tryToCallRemoteObject(remoteSessionController) {
+ this.notifyResized(width, height)
+ }
}
val syncGroup = SurfaceSyncGroup("AppAndSdkViewsSurfaceSync")
@@ -391,29 +393,19 @@
override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
surfaceView.setZOrderOnTop(isZOrderOnTop)
- tryToCallRemoteObject { remoteSessionController.notifyZOrderChanged(isZOrderOnTop) }
+ tryToCallRemoteObject(remoteSessionController) {
+ this.notifyZOrderChanged(isZOrderOnTop)
+ }
}
override fun notifyUiChanged(uiContainerInfo: Bundle) {
- tryToCallRemoteObject { remoteSessionController.notifyUiChanged(uiContainerInfo) }
+ tryToCallRemoteObject(remoteSessionController) {
+ this.notifyUiChanged(uiContainerInfo)
+ }
}
override fun close() {
- tryToCallRemoteObject { remoteSessionController.close() }
- }
- }
-
- private companion object {
-
- /**
- * Tries to call the remote object and handles exceptions if the remote object has died.
- */
- private inline fun tryToCallRemoteObject(function: () -> Unit) {
- try {
- function()
- } catch (e: RemoteException) {
- Log.e(TAG, "Calling remote object failed: $e")
- }
+ closeRemoteSession(remoteSessionController)
}
}
}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index 6113489..a131ca8 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -226,7 +226,7 @@
}
}
- internal fun requestSize(width: Int, height: Int) {
+ internal fun requestResize(width: Int, height: Int) {
if (width == this.width && height == this.height) return
requestedWidth = width
requestedHeight = height
@@ -566,7 +566,7 @@
override fun onResizeRequested(width: Int, height: Int) {
if (sandboxedSdkView == null) return
- sandboxedSdkView?.requestSize(width, height)
+ sandboxedSdkView?.requestResize(width, height)
}
}
diff --git a/profileinstaller/integration-tests/profile-verification/build.gradle b/profileinstaller/integration-tests/profile-verification/build.gradle
index cf670e9..416e4a0 100644
--- a/profileinstaller/integration-tests/profile-verification/build.gradle
+++ b/profileinstaller/integration-tests/profile-verification/build.gradle
@@ -122,5 +122,5 @@
// It makes sure that the apks are generated before the assets are packed.
afterEvaluate {
- tasks.named("generateDebugAndroidTestAssets").configure { it.dependsOn(prepareAssetsTaskProvider) }
+ tasks.named("generateReleaseAndroidTestAssets").configure { it.dependsOn(prepareAssetsTaskProvider) }
}
diff --git a/remotecallback/remotecallback-processor/src/main/java/androidx/remotecallback/compiler/CallableMethod.java b/remotecallback/remotecallback-processor/src/main/java/androidx/remotecallback/compiler/CallableMethod.java
index 1f1c2b6..ed4e882 100644
--- a/remotecallback/remotecallback-processor/src/main/java/androidx/remotecallback/compiler/CallableMethod.java
+++ b/remotecallback/remotecallback-processor/src/main/java/androidx/remotecallback/compiler/CallableMethod.java
@@ -117,7 +117,7 @@
private AnnotationMirror findAnnotation(VariableElement element, String cls) {
for (AnnotationMirror mirror: element.getAnnotationMirrors()) {
- if (mirror.getAnnotationType().toString().equals(cls)) {
+ if (typeString(mirror.getAnnotationType()).equals(cls)) {
return mirror;
}
}
@@ -147,13 +147,13 @@
ProcessingEnvironment env, Messager messager) {
// Validate types
for (int i = 0; i < mTypes.size(); i++) {
- if (checkType(mTypes.get(i).toString(), messager)) {
+ if (checkType(typeString(mTypes.get(i)), messager)) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Invalid type " + mTypes.get(i));
return;
}
}
- if (!"androidx.remotecallback.RemoteCallback".equals(mReturnType.toString())) {
+ if (!"androidx.remotecallback.RemoteCallback".equals(typeString(mReturnType))) {
messager.printMessage(Diagnostic.Kind.ERROR,
"RemoteCallable methods must return RemoteCallback.LOCAL.");
return;
@@ -183,20 +183,22 @@
methodCall.append(mElement.getSimpleName());
methodCall.append("(");
for (int i = 0; i < mNames.size(); i++) {
+ TypeMirror type = mTypes.get(i);
+ String typeString = typeString(type);
// Pass the parameter to the method call.
if (i != 0) {
methodCall.append(", ");
}
methodCall.append("p" + i);
- if (mTypes.get(i).toString().equals(context.toString())) {
+ if (typeString.equals(context.toString())) {
code.addStatement("$L p" + i + " = context", mTypes.get(i));
continue;
}
- code.addStatement("$L p" + i, mTypes.get(i));
+ code.addStatement("$L p" + i, type);
String key = mExtInputKeys.get(i) != null ? mExtInputKeys.get(i) : getBundleKey(i);
// Generate code to extract the value.
- code.addStatement("p$L = $L", i, getBundleParam(mTypes.get(i).toString(), key));
+ code.addStatement("p$L = $L", i, getBundleParam(typeString, key));
}
methodCall.append(")");
// Add the method call as the last thing.
@@ -218,18 +220,20 @@
code.addStatement("$L b = new $L()", bundle, bundle);
for (int i = 0; i < mNames.size(); i++) {
- builder.addParameter(TypeName.get(mTypes.get(i)), "p" + i);
- if (mTypes.get(i).toString().equals(context.toString())) {
+ TypeMirror type = mTypes.get(i);
+ String typeString = typeString(type);
+ builder.addParameter(TypeName.get(type), "p" + i);
+ if (typeString.equals(context.toString())) {
continue;
}
- boolean isNative = isNative(mTypes.get(i).toString());
+ boolean isNative = isNative(typeString);
// Only fill in value if the argument has a value.
if (!isNative) code.beginControlFlow("if (p$L != null)", i);
// Otherwise just need to place the arg value.
code.addStatement("b.put$L($L, ($L) p$L)",
- getTypeMethod(mTypes.get(i).toString()),
- getBundleKey(i), mTypes.get(i), i);
+ getTypeMethod(typeString),
+ getBundleKey(i), type, i);
// No value present, need an explicit null for security.
if (!isNative) code.nextControlFlow("else");
@@ -249,7 +253,7 @@
private int countArgs(ClassName context) {
int ct = 0;
for (int i = 0; i < mTypes.size(); i++) {
- if (mTypes.get(i).toString().equals(context.toString())) {
+ if (typeString(mTypes.get(i)).equals(context.toString())) {
continue;
}
ct++;
@@ -400,4 +404,9 @@
return true;
}
}
+
+ /** Returns a simple string version of the type, with no annotations. */
+ private String typeString(TypeMirror type) {
+ return TypeName.get(type).toString();
+ }
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
index ca3ada7..dddae7e 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
@@ -371,7 +371,7 @@
}
}
- @FlakyTest(bugId = 261205680)
+ @Ignore // b/261205680
@Test
fun appendWithDelayedInvalidation() {
val items = createItems(startId = 0, count = 90)
@@ -387,7 +387,7 @@
// to the data source. it should not crash :)
queryExecutor.filterFunction = {
// TODO(b/): Avoid relying on function name, very brittle.
- !it.toString().contains("refreshInvalidationAsync")
+ !it.toString().contains("refreshInvalidation")
}
db.getDao().deleteItems(items.subList(0, 80).map { it.id })
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
index cf236fe..f9fbdbe 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
@@ -31,8 +31,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import java.util.concurrent.CopyOnWriteArrayList
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -82,11 +82,10 @@
@After
fun tearDown() {
database.close()
- testCoroutineScope.cancel()
}
@Test
- fun testInsert() {
+ fun testInsert() = runTest {
database
.queryInterceptorDao()
.insert(QueryInterceptorEntity("Insert", "Inserted a placeholder query"))
@@ -100,14 +99,14 @@
}
@Test
- fun testDelete() {
+ fun testDelete() = runTest {
database.queryInterceptorDao().delete("Insert")
assertQueryLogged("DELETE FROM queryInterceptorTestDatabase WHERE id=?", listOf("Insert"))
assertTransactionQueries()
}
@Test
- fun testUpdate() {
+ fun testUpdate() = runTest {
database
.queryInterceptorDao()
.insert(QueryInterceptorEntity("Insert", "Inserted a placeholder query"))
@@ -125,7 +124,7 @@
}
@Test
- fun testCompileStatement() {
+ fun testCompileStatement() = runTest {
assertEquals(queryAndArgs.size, 0)
database
.queryInterceptorDao()
@@ -137,7 +136,7 @@
}
@Test
- fun testLoggingSupportSQLiteQuery() {
+ fun testLoggingSupportSQLiteQuery() = runTest {
database.openHelper.writableDatabase.query(
SimpleSQLiteQuery(
"INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
@@ -153,7 +152,7 @@
}
@Test
- fun testExecSQLWithBindArgs() {
+ fun testExecSQLWithBindArgs() = runTest {
database.openHelper.writableDatabase.execSQL(
"INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
"VALUES (?,?)",
@@ -167,7 +166,7 @@
}
@Test
- fun testNullBindArgument() {
+ fun testNullBindArgument() = runTest {
database.openHelper.writableDatabase.query(
SimpleSQLiteQuery(
"INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
@@ -183,7 +182,7 @@
}
@Test
- fun testNullBindArgumentCompileStatement() {
+ fun testNullBindArgumentCompileStatement() = runTest {
val sql =
"INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
"VALUES (?,?)"
@@ -203,7 +202,7 @@
}
@Test
- fun testCallbackCalledOnceAfterCloseAndReOpen() {
+ fun testCallbackCalledOnceAfterCloseAndReOpen() = runTest {
val dbBuilder =
Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
@@ -218,8 +217,6 @@
dbBuilder.build().close()
- database = dbBuilder.build()
-
database
.queryInterceptorDao()
.insert(QueryInterceptorEntity("Insert", "Inserted a placeholder query"))
@@ -232,6 +229,12 @@
assertTransactionQueries()
}
+ private fun runTest(testBody: suspend TestScope.() -> Unit) =
+ testCoroutineScope.runTest {
+ testBody.invoke(this)
+ database.close()
+ }
+
private fun assertQueryLogged(query: String, expectedArgs: List<String?>) {
testCoroutineScope.testScheduler.advanceUntilIdle()
val filteredQueries = queryAndArgs.filter { it.first == query }
diff --git a/room/integration-tests/multiplatformtestapp/build.gradle b/room/integration-tests/multiplatformtestapp/build.gradle
index 95b2df0..4ba2f01 100644
--- a/room/integration-tests/multiplatformtestapp/build.gradle
+++ b/room/integration-tests/multiplatformtestapp/build.gradle
@@ -39,8 +39,10 @@
implementation(libs.kotlinStdlib)
implementation(project(":room:room-runtime"))
implementation(project(":room:room-testing"))
+ implementation(project(":room:room-paging"))
implementation(project(":sqlite:sqlite-bundled"))
implementation(project(":kruth:kruth"))
+ implementation(project(":paging:paging-common"))
implementation(libs.kotlinTest)
implementation(libs.kotlinCoroutinesTest)
}
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
index 78e59dc..760c416 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseQueryTest.kt
@@ -18,6 +18,8 @@
import androidx.kruth.assertThat
import androidx.kruth.assertThrows
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadResult
import androidx.room.RoomRawQuery
import androidx.room.execSQL
import androidx.room.immediateTransaction
@@ -503,4 +505,67 @@
.hasMessageThat()
.contains("Only bind*() calls are allowed")
}
+
+ @Test
+ fun simplePagingQuery() = runTest {
+ val entity1 = SampleEntity(1, 1)
+ val entity2 = SampleEntity(2, 2)
+ val sampleEntities = listOf(entity1, entity2)
+ val dao = db.dao()
+
+ dao.insertSampleEntityList(sampleEntities)
+ val pagingSource = dao.getAllIds()
+
+ val onlyLoadFirst =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 1,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(onlyLoadFirst.data).containsExactly(entity1)
+
+ val loadAll =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 2,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(loadAll.data).containsExactlyElementsIn(sampleEntities)
+ }
+
+ @Test
+ fun pagingQueryWithParams() = runTest {
+ val entity1 = SampleEntity(1, 1)
+ val entity2 = SampleEntity(2, 2)
+ val entity3 = SampleEntity(3, 3)
+ val sampleEntities = listOf(entity1, entity2, entity3)
+ val dao = db.dao()
+
+ dao.insertSampleEntityList(sampleEntities)
+ val pagingSource = dao.getAllIdsWithArgs(1)
+
+ val onlyLoadFirst =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 1,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(onlyLoadFirst.data).containsExactly(entity2)
+
+ val loadAll =
+ pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = 2,
+ placeholdersEnabled = true
+ )
+ ) as LoadResult.Page
+ assertThat(loadAll.data).containsExactlyElementsIn(listOf(entity2, entity3))
+ }
}
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
index e511fc3..a2a3a6b 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SampleDatabase.kt
@@ -203,6 +203,12 @@
@Query("SELECT * FROM StringSampleEntity1")
suspend fun getSampleManyToMany(): SampleManyAndMany
+ @Query("SELECT * FROM SampleEntity")
+ fun getAllIds(): androidx.paging.PagingSource<Int, SampleEntity>
+
+ @Query("SELECT * FROM SampleEntity WHERE pk > :gt ORDER BY pk ASC")
+ fun getAllIdsWithArgs(gt: Long): androidx.paging.PagingSource<Int, SampleEntity>
+
data class Sample1And2(
@Embedded val sample1: SampleEntity,
@Relation(parentColumn = "pk", entityColumn = "pk2") val sample2: SampleEntity2
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/InternalModifierTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/InternalModifierTest.kt
index 57d8a36..54daf29 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/InternalModifierTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/InternalModifierTest.kt
@@ -142,7 +142,7 @@
runKspTest(
sources = listOf(source),
classpath = classpath,
- // TODO(b/314151707): find root cause
+ // https://github.com/google/ksp/issues/1640
kotlincArguments = KOTLINC_LANGUAGE_1_9_ARGS,
config = config,
) { invocation ->
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 49eb1ae..ab1e12d 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -107,7 +107,7 @@
runKspTest(
sources = sources,
classpath = classpath,
- // TODO(b/314151707): find root cause
+ // https://github.com/google/ksp/issues/1930
kotlincArguments = KOTLINC_LANGUAGE_1_9_ARGS
) { invocation ->
collectSignaturesInto(invocation, ksp)
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 c73de92..832e27b 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
@@ -337,7 +337,8 @@
RxJava3TypeNames.COMPLETABLE,
GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE,
KotlinTypeNames.FLOW,
- ReactiveStreamsTypeNames.PUBLISHER
+ ReactiveStreamsTypeNames.PUBLISHER,
+ PagingTypeNames.PAGING_SOURCE
)
fun XTypeName.defaultValue(): String {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt
index edd1307..384b238 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/MultiTypedPagingSourceQueryResultBinderProvider.kt
@@ -21,6 +21,7 @@
import androidx.room.compiler.processing.XRawType
import androidx.room.compiler.processing.XType
import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.PagingTypeNames
import androidx.room.parser.ParsedQuery
import androidx.room.processor.Context
import androidx.room.processor.ProcessorErrors
@@ -33,7 +34,7 @@
class MultiTypedPagingSourceQueryResultBinderProvider(
private val context: Context,
private val roomPagingClassName: XClassName,
- pagingSourceTypeName: XClassName,
+ private val pagingSourceTypeName: XClassName,
) : QueryResultBinderProvider {
private val pagingSourceType: XRawType? by lazy {
@@ -60,7 +61,8 @@
return MultiTypedPagingSourceQueryResultBinder(
listAdapter = listAdapter,
tableNames = tableNames,
- className = roomPagingClassName
+ className = roomPagingClassName,
+ isBasePagingSource = pagingSourceTypeName == PagingTypeNames.PAGING_SOURCE
)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt
index 41c114e..8eea718 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineFlowResultBinder.kt
@@ -91,7 +91,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -128,7 +128,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
index 2a82c10..e9ba2ce 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
@@ -158,7 +158,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -194,7 +194,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
index 647d801..26826b7 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
@@ -95,7 +95,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -130,7 +130,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
index 5f1e385..9205913 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
@@ -85,7 +85,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -120,7 +120,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
index e4898ee..683be46 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -93,7 +93,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -139,7 +139,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
index 8b3f9a2..055401a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
@@ -16,15 +16,21 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.VisibilityModifier
import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XFunSpec
import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
import androidx.room.compiler.codegen.XPropertySpec
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.XTypeSpec
-import androidx.room.ext.AndroidTypeNames.CURSOR
+import androidx.room.ext.AndroidTypeNames
import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.Function1TypeSpec
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.RoomTypeNames.RAW_QUERY
+import androidx.room.ext.SQLiteDriverTypeNames
import androidx.room.solver.CodeGenScope
/**
@@ -35,13 +41,16 @@
class MultiTypedPagingSourceQueryResultBinder(
private val listAdapter: ListQueryResultAdapter?,
private val tableNames: Set<String>,
- className: XClassName
+ className: XClassName,
+ val isBasePagingSource: Boolean
) : QueryResultBinder(listAdapter) {
private val itemTypeName: XTypeName =
listAdapter?.rowAdapters?.firstOrNull()?.out?.asTypeName() ?: XTypeName.ANY_OBJECT
private val pagingSourceTypeName: XTypeName = className.parametrizedBy(itemTypeName)
+ override fun isMigratedToDriver(): Boolean = isBasePagingSource
+
override fun convertAndReturn(
roomSQLiteQueryVar: String,
canReleaseQuery: Boolean,
@@ -61,14 +70,134 @@
)
.apply {
superclass(pagingSourceTypeName)
- addFunction(createConvertRowsMethod(scope))
+ addFunction(
+ createConvertRowsMethod(
+ scope = scope,
+ stmtParamName = "cursor",
+ stmtParamTypeName = AndroidTypeNames.CURSOR,
+ rawQueryParamName = null
+ )
+ )
}
.build()
addStatement("return %L", pagingSourceSpec)
}
}
- private fun createConvertRowsMethod(scope: CodeGenScope): XFunSpec {
+ override fun convertAndReturn(
+ sqlQueryVar: String,
+ dbProperty: XPropertySpec,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
+ returnTypeName: XTypeName,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ check(isBasePagingSource) {
+ "This version of `convertAndReturn` should only be called when the binder is for the " +
+ "base PagingSource. "
+ }
+ val rawQueryVarName = scope.getTmpVar("_rawQuery")
+ val stmtVarName = scope.getTmpVar("_stmt")
+
+ when (scope.language) {
+ CodeLanguage.JAVA -> {
+ val assignExpr =
+ if (bindStatement != null) {
+ XCodeBlock.ofNewInstance(
+ language = scope.language,
+ typeName = RAW_QUERY,
+ "%L, %L",
+ sqlQueryVar,
+ Function1TypeSpec(
+ language = scope.language,
+ parameterTypeName = SQLiteDriverTypeNames.STATEMENT,
+ parameterName = stmtVarName,
+ returnTypeName = KotlinTypeNames.UNIT
+ ) {
+ val functionScope = scope.fork()
+ functionScope.builder
+ .apply { bindStatement.invoke(functionScope, stmtVarName) }
+ .build()
+ addCode(functionScope.generate())
+ addStatement("return %T.INSTANCE", KotlinTypeNames.UNIT)
+ }
+ )
+ } else {
+ XCodeBlock.ofNewInstance(
+ language = scope.language,
+ typeName = RAW_QUERY,
+ "%L",
+ sqlQueryVar
+ )
+ }
+ scope.builder.addLocalVariable(
+ name = rawQueryVarName,
+ typeName = RAW_QUERY,
+ assignExpr = assignExpr
+ )
+ }
+ CodeLanguage.KOTLIN ->
+ scope.builder.apply {
+ if (bindStatement != null) {
+ beginControlFlow(
+ "val %L: %T = %T(%N) { %L ->",
+ rawQueryVarName,
+ RAW_QUERY,
+ RAW_QUERY,
+ sqlQueryVar,
+ stmtVarName
+ )
+ bindStatement.invoke(scope, stmtVarName)
+ endControlFlow()
+ } else {
+ addLocalVariable(
+ name = rawQueryVarName,
+ typeName = RAW_QUERY,
+ assignExpr =
+ XCodeBlock.ofNewInstance(
+ language = scope.language,
+ typeName = RAW_QUERY,
+ argsFormat = "%N",
+ sqlQueryVar
+ )
+ )
+ }
+ }
+ }
+
+ scope.builder.apply {
+ val tableNamesList = tableNames.joinToString(", ") { "\"$it\"" }
+ val statementParamName = "statement"
+ val pagingSourceSpec =
+ XTypeSpec.anonymousClassBuilder(
+ language = language,
+ argsFormat = "%L, %N, %L",
+ rawQueryVarName,
+ dbProperty,
+ tableNamesList
+ )
+ .apply {
+ superclass(pagingSourceTypeName)
+ addFunction(
+ createConvertRowsMethod(
+ scope = scope,
+ stmtParamName = statementParamName,
+ stmtParamTypeName = SQLiteDriverTypeNames.STATEMENT,
+ rawQueryParamName = rawQueryVarName
+ )
+ )
+ }
+ .build()
+ addStatement("return %L", pagingSourceSpec)
+ }
+ }
+
+ private fun createConvertRowsMethod(
+ scope: CodeGenScope,
+ stmtParamName: String,
+ stmtParamTypeName: XTypeName,
+ rawQueryParamName: String?
+ ): XFunSpec {
return XFunSpec.builder(
language = scope.language,
name = "convertRows",
@@ -76,12 +205,24 @@
isOverride = true
)
.apply {
- val cursorParamName = "cursor"
returns(CommonTypeNames.LIST.parametrizedBy(itemTypeName))
- addParameter(typeName = CURSOR, name = cursorParamName)
+ addParameter(typeName = stmtParamTypeName, name = stmtParamName)
+ if (stmtParamTypeName == SQLiteDriverTypeNames.STATEMENT) {
+ // The SQLiteStatement version requires a second parameter for backwards
+ // compatibility for delegating to CursorSQLiteStatement.
+ addParameter(typeName = XTypeName.PRIMITIVE_INT, name = "itemCount")
+ }
val resultVar = scope.getTmpVar("_result")
val rowsScope = scope.fork()
- listAdapter?.convert(resultVar, cursorParamName, rowsScope)
+ if (stmtParamTypeName == SQLiteDriverTypeNames.STATEMENT) {
+ checkNotNull(rawQueryParamName)
+ addStatement(
+ "%L.getBindingFunction().invoke(%L)",
+ rawQueryParamName,
+ stmtParamName,
+ )
+ }
+ listAdapter?.convert(resultVar, stmtParamName, rowsScope)
addCode(rowsScope.generate())
addStatement("return %L", resultVar)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
index b39c993..286394d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
@@ -51,7 +51,7 @@
open fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxLambdaQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxLambdaQueryResultBinder.kt
index 10973f3..57a43cb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxLambdaQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxLambdaQueryResultBinder.kt
@@ -171,7 +171,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -206,7 +206,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
index bc8877e..716fc2c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
@@ -90,7 +90,7 @@
override fun convertAndReturn(
sqlQueryVar: String,
dbProperty: XPropertySpec,
- bindStatement: CodeGenScope.(String) -> Unit,
+ bindStatement: (CodeGenScope.(String) -> Unit)?,
returnTypeName: XTypeName,
inTransaction: Boolean,
scope: CodeGenScope
@@ -134,7 +134,7 @@
sqlQueryVar
)
beginControlFlow("try")
- bindStatement(scope, statementVar)
+ bindStatement?.invoke(scope, statementVar)
val outVar = scope.getTmpVar("_result")
adapter?.convert(outVar, statementVar, scope)
addStatement("$returnPrefix%L", outVar)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index 8ff5d2d..45a0481 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -685,7 +685,12 @@
method.queryResultBinder.convertAndReturn(
sqlQueryVar = sqlVar,
dbProperty = dbProperty,
- bindStatement = { stmtVar -> queryWriter.bindArgs(stmtVar, listSizeArgs, this) },
+ bindStatement =
+ if (queryWriter.parameters.isNotEmpty()) {
+ { stmtVar -> queryWriter.bindArgs(stmtVar, listSizeArgs, this) }
+ } else {
+ null
+ },
returnTypeName = method.returnType.asTypeName(),
inTransaction = method.inTransaction,
scope = scope
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 a4afa61..63d5be5 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
@@ -784,6 +784,9 @@
@Query("SELECT pk FROM MyEntity")
abstract fun getAllIds(): androidx.paging.PagingSource<Int, MyEntity>
+ @Query("SELECT * FROM MyEntity WHERE pk > :gt ORDER BY pk ASC")
+ abstract fun getAllIdsWithArgs(gt: Long): androidx.paging.PagingSource<Int, MyEntity>
+
@Query("SELECT pk FROM MyEntity")
abstract fun getAllIdsRx2(): androidx.paging.rxjava2.RxPagingSource<Int, MyEntity>
@@ -1041,7 +1044,6 @@
"MyDao.kt",
"""
import androidx.room.*
- import androidx.sqlite.db.SupportSQLiteQuery
@Dao
interface MyDao {
@@ -1075,7 +1077,6 @@
"MyDao.kt",
"""
import androidx.room.*
- import androidx.sqlite.db.SupportSQLiteQuery
interface BaseDao<T> {
fun getEntity(id: T): MyEntity
@@ -1117,7 +1118,6 @@
"MyDao.kt",
"""
import androidx.room.*
- import androidx.sqlite.db.SupportSQLiteQuery
interface BaseDao {
@Transaction
@@ -1186,7 +1186,6 @@
"MyDao.kt",
"""
import androidx.room.*
- import androidx.sqlite.db.SupportSQLiteQuery
interface BaseDao {
@Transaction
diff --git a/room/room-compiler/src/test/test-data/common/input/LimitOffsetPagingSource.kt b/room/room-compiler/src/test/test-data/common/input/LimitOffsetPagingSource.kt
index 7b72548..a9921a2 100644
--- a/room/room-compiler/src/test/test-data/common/input/LimitOffsetPagingSource.kt
+++ b/room/room-compiler/src/test/test-data/common/input/LimitOffsetPagingSource.kt
@@ -15,14 +15,14 @@
*/
package androidx.room.paging
-import android.database.Cursor
import androidx.paging.PagingState
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomRawQuery
+import androidx.sqlite.SQLiteStatement
@Suppress("UNUSED_PARAMETER")
abstract class LimitOffsetPagingSource<T : Any>(
- private val sourceQuery: RoomSQLiteQuery,
+ private val sourceQuery: RoomRawQuery,
private val db: RoomDatabase,
vararg tables: String
) : androidx.paging.PagingSource<Int, T>() {
@@ -33,5 +33,5 @@
override public suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
return LoadResult.Invalid()
}
- protected abstract fun convertRows(cursor: Cursor): List<T>
+ protected abstract fun convertRows(statement: SQLiteStatement, itemCount: Int): List<T>
}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt
index 386d206..eab9d14 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/multiTypedPagingSourceResultBinder.kt
@@ -2,12 +2,16 @@
import androidx.paging.ListenableFuturePagingSource
import androidx.paging.PagingSource
import androidx.room.RoomDatabase
+import androidx.room.RoomRawQuery
import androidx.room.RoomSQLiteQuery
import androidx.room.RoomSQLiteQuery.Companion.acquire
import androidx.room.paging.LimitOffsetPagingSource
import androidx.room.paging.guava.LimitOffsetListenableFuturePagingSource
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.sqlite.SQLiteStatement
import javax.`annotation`.processing.Generated
import kotlin.Int
+import kotlin.Long
import kotlin.String
import kotlin.Suppress
import kotlin.collections.List
@@ -31,15 +35,41 @@
public override fun getAllIds(): PagingSource<Int, MyEntity> {
val _sql: String = "SELECT pk FROM MyEntity"
- val _statement: RoomSQLiteQuery = acquire(_sql, 0)
- return object : LimitOffsetPagingSource<MyEntity>(_statement, __db, "MyEntity") {
- protected override fun convertRows(cursor: Cursor): List<MyEntity> {
+ val _rawQuery: RoomRawQuery = RoomRawQuery(_sql)
+ return object : LimitOffsetPagingSource<MyEntity>(_rawQuery, __db, "MyEntity") {
+ protected override fun convertRows(statement: SQLiteStatement, itemCount: Int):
+ List<MyEntity> {
+ _rawQuery.getBindingFunction().invoke(statement)
val _cursorIndexOfPk: Int = 0
val _result: MutableList<MyEntity> = mutableListOf()
- while (cursor.moveToNext()) {
+ while (statement.step()) {
val _item: MyEntity
val _tmpPk: Int
- _tmpPk = cursor.getInt(_cursorIndexOfPk)
+ _tmpPk = statement.getLong(_cursorIndexOfPk).toInt()
+ _item = MyEntity(_tmpPk)
+ _result.add(_item)
+ }
+ return _result
+ }
+ }
+ }
+
+ public override fun getAllIdsWithArgs(gt: Long): PagingSource<Int, MyEntity> {
+ val _sql: String = "SELECT * FROM MyEntity WHERE pk > ? ORDER BY pk ASC"
+ val _rawQuery: RoomRawQuery = RoomRawQuery(_sql) { _stmt ->
+ var _argIndex: Int = 1
+ _stmt.bindLong(_argIndex, gt)
+ }
+ return object : LimitOffsetPagingSource<MyEntity>(_rawQuery, __db, "MyEntity") {
+ protected override fun convertRows(statement: SQLiteStatement, itemCount: Int):
+ List<MyEntity> {
+ _rawQuery.getBindingFunction().invoke(statement)
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(statement, "pk")
+ val _result: MutableList<MyEntity> = mutableListOf()
+ while (statement.step()) {
+ val _item: MyEntity
+ val _tmpPk: Int
+ _tmpPk = statement.getLong(_cursorIndexOfPk).toInt()
_item = MyEntity(_tmpPk)
_result.add(_item)
}
diff --git a/room/room-paging/bcv/native/current.txt b/room/room-paging/bcv/native/current.txt
index 5d6f9ec9..0fc19cc 100644
--- a/room/room-paging/bcv/native/current.txt
+++ b/room/room-paging/bcv/native/current.txt
@@ -6,3 +6,28 @@
// - Show declarations: true
// Library unique name: <androidx.room:room-paging>
+abstract class <#A: kotlin/Any> androidx.room.paging/LimitOffsetPagingSource : androidx.paging/PagingSource<kotlin/Int, #A> { // androidx.room.paging/LimitOffsetPagingSource|null[0]
+ constructor <init>(androidx.room/RoomRawQuery, androidx.room/RoomDatabase, kotlin/Array<out kotlin/String>...) // androidx.room.paging/LimitOffsetPagingSource.<init>|<init>(androidx.room.RoomRawQuery;androidx.room.RoomDatabase;kotlin.Array<out|kotlin.String>...){}[0]
+
+ final val db // androidx.room.paging/LimitOffsetPagingSource.db|{}db[0]
+ final fun <get-db>(): androidx.room/RoomDatabase // androidx.room.paging/LimitOffsetPagingSource.db.<get-db>|<get-db>(){}[0]
+ final val itemCount // androidx.room.paging/LimitOffsetPagingSource.itemCount|{}itemCount[0]
+ final fun <get-itemCount>(): kotlin/Int // androidx.room.paging/LimitOffsetPagingSource.itemCount.<get-itemCount>|<get-itemCount>(){}[0]
+ final val sourceQuery // androidx.room.paging/LimitOffsetPagingSource.sourceQuery|{}sourceQuery[0]
+ final fun <get-sourceQuery>(): androidx.room/RoomRawQuery // androidx.room.paging/LimitOffsetPagingSource.sourceQuery.<get-sourceQuery>|<get-sourceQuery>(){}[0]
+ open val jumpingSupported // androidx.room.paging/LimitOffsetPagingSource.jumpingSupported|{}jumpingSupported[0]
+ open fun <get-jumpingSupported>(): kotlin/Boolean // androidx.room.paging/LimitOffsetPagingSource.jumpingSupported.<get-jumpingSupported>|<get-jumpingSupported>(){}[0]
+
+ open fun convertRows(androidx.sqlite/SQLiteStatement, kotlin/Int): kotlin.collections/List<#A> // androidx.room.paging/LimitOffsetPagingSource.convertRows|convertRows(androidx.sqlite.SQLiteStatement;kotlin.Int){}[0]
+ open fun getRefreshKey(androidx.paging/PagingState<kotlin/Int, #A>): kotlin/Int? // androidx.room.paging/LimitOffsetPagingSource.getRefreshKey|getRefreshKey(androidx.paging.PagingState<kotlin.Int,1:0>){}[0]
+ open suspend fun load(androidx.paging/PagingSource.LoadParams<kotlin/Int>): androidx.paging/PagingSource.LoadResult<kotlin/Int, #A> // androidx.room.paging/LimitOffsetPagingSource.load|load(androidx.paging.PagingSource.LoadParams<kotlin.Int>){}[0]
+}
+
+final const val androidx.room.paging.util/INITIAL_ITEM_COUNT // androidx.room.paging.util/INITIAL_ITEM_COUNT|{}INITIAL_ITEM_COUNT[0]
+ final fun <get-INITIAL_ITEM_COUNT>(): kotlin/Int // androidx.room.paging.util/INITIAL_ITEM_COUNT.<get-INITIAL_ITEM_COUNT>|<get-INITIAL_ITEM_COUNT>(){}[0]
+
+final fun <#A: kotlin/Any> (androidx.paging/PagingState<kotlin/Int, #A>).androidx.room.paging.util/getClippedRefreshKey(): kotlin/Int? // androidx.room.paging.util/getClippedRefreshKey|[email protected]<kotlin.Int,0:0>(){0§<kotlin.Any>}[0]
+final fun androidx.room.paging.util/getLimit(androidx.paging/PagingSource.LoadParams<kotlin/Int>, kotlin/Int): kotlin/Int // androidx.room.paging.util/getLimit|getLimit(androidx.paging.PagingSource.LoadParams<kotlin.Int>;kotlin.Int){}[0]
+final fun androidx.room.paging.util/getOffset(androidx.paging/PagingSource.LoadParams<kotlin/Int>, kotlin/Int, kotlin/Int): kotlin/Int // androidx.room.paging.util/getOffset|getOffset(androidx.paging.PagingSource.LoadParams<kotlin.Int>;kotlin.Int;kotlin.Int){}[0]
+final suspend fun <#A: kotlin/Any> androidx.room.paging.util/queryDatabase(androidx.paging/PagingSource.LoadParams<kotlin/Int>, androidx.room/RoomRawQuery, androidx.room/RoomDatabase, kotlin/Int, kotlin/Function2<androidx.sqlite/SQLiteStatement, kotlin/Int, kotlin.collections/List<#A>>): androidx.paging/PagingSource.LoadResult<kotlin/Int, #A> // androidx.room.paging.util/queryDatabase|queryDatabase(androidx.paging.PagingSource.LoadParams<kotlin.Int>;androidx.room.RoomRawQuery;androidx.room.RoomDatabase;kotlin.Int;kotlin.Function2<androidx.sqlite.SQLiteStatement,kotlin.Int,kotlin.collections.List<0:0>>){0§<kotlin.Any>}[0]
+final suspend fun androidx.room.paging.util/queryItemCount(androidx.room/RoomRawQuery, androidx.room/RoomDatabase): kotlin/Int // androidx.room.paging.util/queryItemCount|queryItemCount(androidx.room.RoomRawQuery;androidx.room.RoomDatabase){}[0]
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index d457b62..c84fe70 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -16,6 +16,7 @@
import androidx.build.PlatformIdentifier
import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
plugins {
id("AndroidXPlugin")
@@ -38,6 +39,7 @@
api(libs.kotlinStdlib)
api("androidx.paging:paging-common:3.3.2")
api(project(":room:room-runtime"))
+ implementation(libs.atomicFu)
}
}
@@ -52,6 +54,18 @@
dependsOn(commonMain)
}
+ jvmNativeMain {
+ dependsOn(commonMain)
+ }
+
+ jvmMain {
+ dependsOn(jvmNativeMain)
+ }
+
+ nativeMain {
+ dependsOn(jvmNativeMain)
+ }
+
androidInstrumentedTest {
dependsOn(commonTest)
dependencies {
@@ -65,6 +79,13 @@
implementation("androidx.paging:paging-testing:3.3.2")
}
}
+ targets.configureEach { target ->
+ if (target.platformType == KotlinPlatformType.native) {
+ target.compilations["main"].defaultSourceSet {
+ dependsOn(nativeMain)
+ }
+ }
+ }
}
}
@@ -82,4 +103,5 @@
inceptionYear = "2021"
description = "Room Paging integration"
legacyDisableKotlinStrictApiMode = true
+ metalavaK2UastEnabled = false // Due to b/360195094
}
diff --git a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index e4ccfc8..e2ff237 100644
--- a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -22,11 +22,14 @@
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadResult
+import androidx.paging.PagingState
import androidx.paging.testing.TestPager
import androidx.room.Room
import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomRawQuery
+import androidx.room.paging.util.getClippedRefreshKey
import androidx.room.util.getColumnIndexOrThrow
+import androidx.sqlite.SQLiteStatement
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -125,7 +128,7 @@
dao.addAllItems(ITEMS_LIST)
// count query is executed on first load
pager.refresh()
- assertThat(pagingSource.itemCount.get()).isEqualTo(100)
+ assertThat(pagingSource.itemCount).isEqualTo(100)
}
@Test
@@ -140,7 +143,7 @@
// count query is executed on first load
pager.refresh()
// should be 60 instead of 100
- assertThat(pagingSource.itemCount.get()).isEqualTo(60)
+ assertThat(pagingSource.itemCount).isEqualTo(60)
}
@Test
@@ -243,7 +246,7 @@
// initial loadSize = 15, but limited by id < 50, should only load items 40 - 50
assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(40, 50))
// should have 50 items fulfilling condition of id < 50 (TestItem id 0 - 49)
- assertThat(pagingSource.itemCount.get()).isEqualTo(50)
+ assertThat(pagingSource.itemCount).isEqualTo(50)
}
@Test
@@ -262,7 +265,7 @@
val result = pager.refresh() as LoadResult.Page
assertThat(result.data).containsExactly(ITEMS_LIST[90])
- assertThat(pagingSource.itemCount.get()).isEqualTo(1)
+ assertThat(pagingSource.itemCount).isEqualTo(1)
}
@Test
@@ -281,7 +284,7 @@
assertThat(result.data).isEmpty()
// check that no append/prepend can be triggered
- assertThat(pagingSource.itemCount.get()).isEqualTo(0)
+ assertThat(pagingSource.itemCount).isEqualTo(0)
assertThat(result.nextKey).isNull()
assertThat(result.prevKey).isNull()
assertThat(result.itemsBefore).isEqualTo(0)
@@ -302,7 +305,7 @@
// ensure that it respects SQLite's default behavior for negative LIMIT
assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(0, 15))
// should behave as if no LIMIT were set
- assertThat(pagingSource.itemCount.get()).isEqualTo(100)
+ assertThat(pagingSource.itemCount).isEqualTo(100)
assertThat(result.nextKey).isEqualTo(15)
assertThat(result.prevKey).isNull()
assertThat(result.itemsBefore).isEqualTo(0)
@@ -554,7 +557,7 @@
// database should only have 40 items left. Refresh key is invalid at this point
// (greater than item count after deletion)
- assertThat(pagingSource2.itemCount.get()).isEqualTo(40)
+ assertThat(pagingSource2.itemCount).isEqualTo(40)
// ensure that paging source can handle invalid refresh key properly
// should load last page with items 25 - 40
assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(25, 40))
@@ -584,7 +587,7 @@
runPagingSourceTest { pager, pagingSource ->
dao.addAllItems(ITEMS_LIST)
pager.refresh()
- assertThat(pagingSource.itemCount.get()).isEqualTo(100)
+ assertThat(pagingSource.itemCount).isEqualTo(100)
// items id 0 - 29 deleted (30 items removed)
dao.deleteTestItems(0, 29)
@@ -595,7 +598,7 @@
val result = pager2.refresh(initialKey = 0) as LoadResult.Page
// database should only have 70 items left
- assertThat(pagingSource2.itemCount.get()).isEqualTo(70)
+ assertThat(pagingSource2.itemCount).isEqualTo(70)
// first 30 items deleted, refresh should load starting from pos 31 (item id 30 - 45)
assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(30, 45))
@@ -613,7 +616,7 @@
runPagingSourceTest { pager, pagingSource ->
dao.addAllItems(ITEMS_LIST)
pager.refresh(initialKey = 30)
- assertThat(pagingSource.itemCount.get()).isEqualTo(100)
+ assertThat(pagingSource.itemCount).isEqualTo(100)
// items id 0 - 94 deleted (95 items removed)
dao.deleteTestItems(0, 94)
@@ -624,7 +627,7 @@
val result = pager2.refresh(initialKey = 0) as LoadResult.Page
// database should only have 5 items left
- assertThat(pagingSource2.itemCount.get()).isEqualTo(5)
+ assertThat(pagingSource2.itemCount).isEqualTo(5)
// only 5 items should be loaded with offset = 0
assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(95, 100))
@@ -653,7 +656,6 @@
}
}
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class LimitOffsetPagingSourceTestWithFilteringExecutor {
@@ -759,11 +761,21 @@
queryString: String = "SELECT * FROM $tableName ORDER BY id ASC",
) :
LimitOffsetPagingSource<TestItem>(
- sourceQuery = RoomSQLiteQuery.acquire(queryString, 0),
+ sourceQuery = RoomRawQuery(sql = queryString),
db = db,
- tables = arrayOf("$tableName")
+ tables = arrayOf(tableName)
) {
+ override fun convertRows(statement: SQLiteStatement, itemCount: Int): List<TestItem> {
+ val stmtIndexOfId = getColumnIndexOrThrow(statement, "id")
+ val data = mutableListOf<TestItem>()
+ while (statement.step()) {
+ val tmpId = statement.getInt(stmtIndexOfId)
+ data.add(TestItem(tmpId))
+ }
+ return data
+ }
+
override fun convertRows(cursor: Cursor): List<TestItem> {
val cursorIndexOfId = getColumnIndexOrThrow(cursor, "id")
val data = mutableListOf<TestItem>()
@@ -773,6 +785,13 @@
}
return data
}
+
+ override val jumpingSupported: Boolean
+ get() = true
+
+ override fun getRefreshKey(state: PagingState<Int, TestItem>): Int? {
+ return state.getClippedRefreshKey()
+ }
}
private val CONFIG = PagingConfig(pageSize = 5, enablePlaceholders = true, initialLoadSize = 15)
diff --git a/room/room-paging/src/main/AndroidManifest.xml b/room/room-paging/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from room/room-paging/src/main/AndroidManifest.xml
rename to room/room-paging/src/androidMain/AndroidManifest.xml
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/CursorSQLiteStatement.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/CursorSQLiteStatement.android.kt
new file mode 100644
index 0000000..d9389a0
--- /dev/null
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/CursorSQLiteStatement.android.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.paging
+
+import android.database.AbstractCursor
+import androidx.sqlite.SQLiteStatement
+
+/** Wrapper class for backwards compatibility in room-paging. */
+internal class SQLiteStatementCursor(
+ private val statement: SQLiteStatement,
+ private val rowCount: Int
+) : AbstractCursor() {
+ override fun getCount(): Int = rowCount
+
+ override fun getColumnNames(): Array<String> = statement.getColumnNames().toTypedArray()
+
+ override fun getString(column: Int): String = statement.getText(column)
+
+ override fun getShort(column: Int): Short = statement.getLong(column).toShort()
+
+ override fun getInt(column: Int): Int = statement.getInt(column)
+
+ override fun getLong(column: Int): Long = statement.getLong(column)
+
+ override fun getFloat(column: Int): Float = statement.getFloat(column)
+
+ override fun getDouble(column: Int): Double = statement.getDouble(column)
+
+ override fun isNull(column: Int): Boolean = statement.isNull(column)
+
+ override fun onMove(oldPosition: Int, newPosition: Int): Boolean {
+ check(oldPosition + 1 == newPosition) {
+ "Compat cursor can only move forward one position at a time."
+ }
+ return statement.step()
+ }
+}
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
index 316fe34..fb7d965 100644
--- a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
@@ -17,22 +17,16 @@
package androidx.room.paging
import android.database.Cursor
-import androidx.annotation.NonNull
import androidx.annotation.RestrictTo
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.room.RoomDatabase
+import androidx.room.RoomRawQuery
import androidx.room.RoomSQLiteQuery
-import androidx.room.paging.util.INITIAL_ITEM_COUNT
-import androidx.room.paging.util.INVALID
-import androidx.room.paging.util.ThreadSafeInvalidationObserver
+import androidx.room.paging.CommonLimitOffsetImpl.Companion.BUG_LINK
import androidx.room.paging.util.getClippedRefreshKey
-import androidx.room.paging.util.queryDatabase
-import androidx.room.paging.util.queryItemCount
-import androidx.room.withTransaction
+import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteQuery
-import java.util.concurrent.atomic.AtomicInteger
-import kotlinx.coroutines.withContext
/**
* An implementation of [PagingSource] to perform a LIMIT OFFSET query
@@ -42,11 +36,22 @@
* when data changes.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class LimitOffsetPagingSource<Value : Any>(
- private val sourceQuery: RoomSQLiteQuery,
- private val db: RoomDatabase,
+actual abstract class LimitOffsetPagingSource<Value : Any>
+actual constructor(
+ actual val sourceQuery: RoomRawQuery,
+ actual val db: RoomDatabase,
vararg tables: String,
) : PagingSource<Int, Value>() {
+ constructor(
+ sourceQuery: RoomSQLiteQuery,
+ db: RoomDatabase,
+ vararg tables: String,
+ ) : this(
+ sourceQuery =
+ RoomRawQuery(sql = sourceQuery.sql, onBindStatement = { sourceQuery.bindTo(it) }),
+ db = db,
+ tables = tables
+ )
constructor(
supportSQLiteQuery: SupportSQLiteQuery,
@@ -58,77 +63,27 @@
tables = tables,
)
- internal val itemCount: AtomicInteger = AtomicInteger(INITIAL_ITEM_COUNT)
+ private val implementation = CommonLimitOffsetImpl(tables, this, ::convertRows)
- private val observer =
- ThreadSafeInvalidationObserver(tables = tables, onInvalidated = ::invalidate)
-
- override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
- return withContext(db.getQueryContext()) {
- observer.registerIfNecessary(db)
- val tempCount = itemCount.get()
- // if itemCount is < 0, then it is initial load
- try {
- if (tempCount == INITIAL_ITEM_COUNT) {
- initialLoad(params)
- } else {
- nonInitialLoad(params, tempCount)
- }
- } catch (e: Exception) {
- LoadResult.Error(e)
- }
- }
- }
-
- /**
- * For the very first time that this PagingSource's [load] is called. Executes the count query
- * (initializes [itemCount]) and db query within a transaction to ensure initial load's data
- * integrity.
- *
- * For example, if the database gets updated after the count query but before the db query
- * completes, the paging source may not invalidate in time, but this method will return data
- * based on the original database that the count was performed on to ensure a valid initial
- * load.
- */
- private suspend fun initialLoad(params: LoadParams<Int>): LoadResult<Int, Value> {
- return db.withTransaction {
- val tempCount = queryItemCount(sourceQuery, db)
- itemCount.set(tempCount)
- queryDatabase(
- params = params,
- sourceQuery = sourceQuery,
- db = db,
- itemCount = tempCount,
- convertRows = ::convertRows
- )
- }
- }
-
- private suspend fun nonInitialLoad(
- params: LoadParams<Int>,
- tempCount: Int,
- ): LoadResult<Int, Value> {
- val loadResult =
- queryDatabase(
- params = params,
- sourceQuery = sourceQuery,
- db = db,
- itemCount = tempCount,
- convertRows = ::convertRows
- )
- // manually check if database has been updated. If so, the observer's
- // invalidation callback will invalidate this paging source
- db.invalidationTracker.refreshVersionsSync()
- @Suppress("UNCHECKED_CAST")
- return if (invalid) INVALID as LoadResult.Invalid<Int, Value> else loadResult
- }
-
- @NonNull protected abstract fun convertRows(cursor: Cursor): List<Value>
-
- override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
- return state.getClippedRefreshKey()
- }
+ actual val itemCount: Int
+ get() = implementation.itemCount.value
override val jumpingSupported: Boolean
get() = true
+
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> =
+ implementation.load(params)
+
+ override fun getRefreshKey(state: PagingState<Int, Value>): Int? = state.getClippedRefreshKey()
+
+ protected open fun convertRows(cursor: Cursor): List<Value> {
+ throw NotImplementedError(
+ "Unexpected call to a function with no implementation that Room is suppose to " +
+ "generate. Please file a bug at: $BUG_LINK."
+ )
+ }
+
+ protected actual open fun convertRows(statement: SQLiteStatement, itemCount: Int): List<Value> {
+ return convertRows(SQLiteStatementCursor(statement, itemCount))
+ }
}
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/util/RoomPagingUtil.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/util/RoomPagingUtil.android.kt
index f2a4462..525f87e 100644
--- a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/util/RoomPagingUtil.android.kt
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/util/RoomPagingUtil.android.kt
@@ -13,20 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@file:JvmName("RoomPagingUtil")
+@file:JvmMultifileClass
package androidx.room.paging.util
import android.database.Cursor
import android.os.CancellationSignal
import androidx.annotation.RestrictTo
-import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadParams
-import androidx.paging.PagingSource.LoadParams.Append
-import androidx.paging.PagingSource.LoadParams.Prepend
-import androidx.paging.PagingSource.LoadParams.Refresh
import androidx.paging.PagingSource.LoadResult
-import androidx.paging.PagingState
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
@@ -35,65 +31,10 @@
*
* Any loaded data or queued loads prior to returning INVALID will be discarded
*/
+@get:Suppress("AcronymName")
+@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val INVALID = LoadResult.Invalid<Any, Any>()
-/** The default itemCount value */
-const val INITIAL_ITEM_COUNT = -1
-
-/**
- * Calculates query limit based on LoadType.
- *
- * Prepend: If requested loadSize is larger than available number of items to prepend, it will query
- * with OFFSET = 0, LIMIT = prevKey
- */
-fun getLimit(params: LoadParams<Int>, key: Int): Int {
- return when (params) {
- is Prepend ->
- if (key < params.loadSize) {
- key
- } else {
- params.loadSize
- }
- else -> params.loadSize
- }
-}
-
-/**
- * calculates query offset amount based on loadtype
- *
- * Prepend: OFFSET is calculated by counting backwards the number of items that needs to be loaded
- * before [key]. For example, if key = 30 and loadSize = 5, then offset = 25 and items in db
- * position 26-30 are loaded. If requested loadSize is larger than the number of available items to
- * prepend, OFFSET clips to 0 to prevent negative OFFSET.
- *
- * Refresh: If initialKey is supplied through Pager, Paging 3 will now start loading from initialKey
- * with initialKey being the first item. If key is supplied by [getClippedRefreshKey], the key has
- * already been adjusted to load half of the requested items before anchorPosition and the other
- * half after anchorPosition. See comments on [getClippedRefreshKey] for more details. If key
- * (regardless if from initialKey or [getClippedRefreshKey]) is larger than available items, the
- * last page will be loaded by counting backwards the loadSize before last item in database. For
- * example, this can happen if invalidation came from a large number of items dropped. i.e. in items
- * 0 - 100, items 41-80 are dropped. Depending on last viewed item, hypothetically
- * [getClippedRefreshKey] may return key = 60. If loadSize = 10, then items 31-40 will be loaded.
- */
-fun getOffset(params: LoadParams<Int>, key: Int, itemCount: Int): Int {
- return when (params) {
- is Prepend ->
- if (key < params.loadSize) {
- 0
- } else {
- key - params.loadSize
- }
- is Append -> key
- is Refresh ->
- if (key >= itemCount) {
- maxOf(0, itemCount - params.loadSize)
- } else {
- key
- }
- }
-}
-
/**
* calls RoomDatabase.query() to return a cursor and then calls convertRows() to extract and return
* list of data
@@ -108,6 +49,7 @@
* @param cancellationSignal the signal to cancel the query if the query hasn't yet completed
* @param convertRows the function to iterate data with provided [Cursor] to return List<Value>
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun <Value : Any> queryDatabase(
params: LoadParams<Int>,
sourceQuery: RoomSQLiteQuery,
@@ -155,6 +97,7 @@
* throws error when the column value is null, the column type is not an integral type, or the
* integer value is outside the range [Integer.MIN_VALUE, Integer.MAX_VALUE]
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun queryItemCount(sourceQuery: RoomSQLiteQuery, db: RoomDatabase): Int {
val countQuery = "SELECT COUNT(*) FROM ( ${sourceQuery.sql} )"
val sqLiteQuery: RoomSQLiteQuery = RoomSQLiteQuery.acquire(countQuery, sourceQuery.argCount)
@@ -170,22 +113,3 @@
sqLiteQuery.release()
}
}
-
-/**
- * Returns the key for [PagingSource] for a non-initial REFRESH load.
- *
- * To prevent a negative key, key is clipped to 0 when the number of items available before
- * anchorPosition is less than the requested amount of initialLoadSize / 2.
- */
-fun <Value : Any> PagingState<Int, Value>.getClippedRefreshKey(): Int? {
- return when (val anchorPosition = anchorPosition) {
- null -> null
- /**
- * It is unknown whether anchorPosition represents the item at the top of the screen or item
- * at the bottom of the screen. To ensure the number of items loaded is enough to fill up
- * the screen, half of loadSize is loaded before the anchorPosition and the other half is
- * loaded after the anchorPosition -- anchorPosition becomes the middle item.
- */
- else -> maxOf(0, anchorPosition - (config.initialLoadSize / 2))
- }
-}
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
new file mode 100644
index 0000000..2e166bc5
--- /dev/null
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.paging
+
+import androidx.annotation.RestrictTo
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSource.LoadResult
+import androidx.paging.PagingSource.LoadResult.Invalid
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomRawQuery
+import androidx.room.immediateTransaction
+import androidx.room.paging.util.INITIAL_ITEM_COUNT
+import androidx.room.paging.util.queryDatabase
+import androidx.room.paging.util.queryItemCount
+import androidx.room.useReaderConnection
+import androidx.sqlite.SQLiteStatement
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * An implementation of [PagingSource] to perform a LIMIT OFFSET query
+ *
+ * This class is used for Paging3 to perform Query and RawQuery in Room to return a PagingSource for
+ * Pager's consumption. Registers observers on tables lazily and automatically invalidates itself
+ * when data changes.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+expect abstract class LimitOffsetPagingSource<Value : Any>(
+ sourceQuery: RoomRawQuery,
+ db: RoomDatabase,
+ vararg tables: String
+) : PagingSource<Int, Value> {
+ val sourceQuery: RoomRawQuery
+ val db: RoomDatabase
+
+ val itemCount: Int
+
+ protected open fun convertRows(statement: SQLiteStatement, itemCount: Int): List<Value>
+}
+
+internal class CommonLimitOffsetImpl<Value : Any>(
+ tables: Array<out String>,
+ val pagingSource: LimitOffsetPagingSource<Value>,
+ private val convertRows: (SQLiteStatement, Int) -> List<Value>
+) {
+ private val db = pagingSource.db
+ private val sourceQuery = pagingSource.sourceQuery
+ internal val itemCount = atomic(INITIAL_ITEM_COUNT)
+ private val registered = atomic(false)
+ private val observer =
+ object : InvalidationTracker.Observer(tables) {
+ override fun onInvalidated(tables: Set<String>) {
+ pagingSource.invalidate()
+ }
+ }
+
+ init {
+ pagingSource.registerInvalidatedCallback {
+ db.getCoroutineScope().launch { db.invalidationTracker.unsubscribe(observer) }
+ }
+ }
+
+ suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
+ return withContext(db.getCoroutineScope().coroutineContext) {
+ if (!pagingSource.invalid && registered.compareAndSet(expect = false, update = true)) {
+ db.invalidationTracker.subscribe(observer)
+ }
+ val tempCount = itemCount.value
+ // if itemCount is < 0, then it is initial load
+ try {
+ if (tempCount == INITIAL_ITEM_COUNT) {
+ initialLoad(params)
+ } else {
+ nonInitialLoad(params, tempCount)
+ }
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+ }
+
+ /**
+ * For the very first time that this PagingSource's [load] is called. Executes the count query
+ * (initializes [itemCount]) and db query within a transaction to ensure initial load's data
+ * integrity.
+ *
+ * For example, if the database gets updated after the count query but before the db query
+ * completes, the paging source may not invalidate in time, but this method will return data
+ * based on the original database that the count was performed on to ensure a valid initial
+ * load.
+ */
+ private suspend fun initialLoad(params: LoadParams<Int>): LoadResult<Int, Value> {
+ return db.useReaderConnection { connection ->
+ connection.immediateTransaction {
+ val tempCount = queryItemCount(sourceQuery, db)
+ itemCount.value = tempCount
+ queryDatabase(
+ params = params,
+ sourceQuery = sourceQuery,
+ db = db,
+ itemCount = tempCount,
+ convertRows = convertRows,
+ )
+ }
+ }
+ }
+
+ private suspend fun nonInitialLoad(
+ params: LoadParams<Int>,
+ tempCount: Int,
+ ): LoadResult<Int, Value> {
+ val loadResult =
+ queryDatabase(
+ params = params,
+ sourceQuery = sourceQuery,
+ db = db,
+ itemCount = tempCount,
+ convertRows = convertRows
+ )
+ // manually check if database has been updated. If so, the observer's
+ // invalidation callback will invalidate this paging source
+ db.invalidationTracker.refreshInvalidation()
+
+ @Suppress("UNCHECKED_CAST")
+ return if (pagingSource.invalid) INVALID as Invalid<Int, Value> else loadResult
+ }
+
+ companion object {
+ /**
+ * A [LoadResult] that can be returned to trigger a new generation of PagingSource
+ *
+ * Any loaded data or queued loads prior to returning INVALID will be discarded
+ */
+ val INVALID = Invalid<Any, Any>()
+
+ const val BUG_LINK =
+ "https://issuetracker.google.com/issues/new?component=413107&template=1096568"
+ }
+}
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
new file mode 100644
index 0000000..1d51cc2
--- /dev/null
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("RoomPagingUtil")
+@file:JvmMultifileClass
+
+package androidx.room.paging.util
+
+import androidx.annotation.RestrictTo
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSource.LoadParams.Append
+import androidx.paging.PagingSource.LoadParams.Prepend
+import androidx.paging.PagingSource.LoadParams.Refresh
+import androidx.paging.PagingSource.LoadResult
+import androidx.paging.PagingState
+import androidx.room.RoomDatabase
+import androidx.room.RoomRawQuery
+import androidx.room.useReaderConnection
+import androidx.sqlite.SQLiteStatement
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
+/** The default itemCount value */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) const val INITIAL_ITEM_COUNT = -1
+
+/**
+ * Calculates query limit based on LoadType.
+ *
+ * Prepend: If requested loadSize is larger than available number of items to prepend, it will query
+ * with OFFSET = 0, LIMIT = prevKey.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun getLimit(params: LoadParams<Int>, key: Int): Int {
+ return when (params) {
+ is Prepend ->
+ if (key < params.loadSize) {
+ key
+ } else {
+ params.loadSize
+ }
+ else -> params.loadSize
+ }
+}
+
+/**
+ * Calculates query offset amount based on load type.
+ *
+ * Prepend: OFFSET is calculated by counting backwards the number of items that needs to be loaded
+ * before [key]. For example, if key = 30 and loadSize = 5, then offset = 25 and items in db
+ * position 26-30 are loaded. If requested loadSize is larger than the number of available items to
+ * prepend, OFFSET clips to 0 to prevent negative OFFSET.
+ *
+ * Refresh: If initialKey is supplied through Pager, Paging 3 will now start loading from initialKey
+ * with initialKey being the first item. If key is supplied by [getClippedRefreshKey], the key has
+ * already been adjusted to load half of the requested items before anchorPosition and the other
+ * half after anchorPosition. See comments on [getClippedRefreshKey] for more details. If key
+ * (regardless if from initialKey or [getClippedRefreshKey]) is larger than available items, the
+ * last page will be loaded by counting backwards the loadSize before last item in database. For
+ * example, this can happen if invalidation came from a large number of items dropped. i.e. in items
+ * 0 - 100, items 41-80 are dropped. Depending on last viewed item, hypothetically
+ * [getClippedRefreshKey] may return key = 60. If loadSize = 10, then items 31-40 will be loaded.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun getOffset(params: LoadParams<Int>, key: Int, itemCount: Int): Int {
+ return when (params) {
+ is Prepend ->
+ if (key < params.loadSize) {
+ 0
+ } else {
+ key - params.loadSize
+ }
+ is Append -> key
+ is Refresh ->
+ if (key >= itemCount) {
+ maxOf(0, itemCount - params.loadSize)
+ } else {
+ key
+ }
+ }
+}
+
+/**
+ * Calls RoomDatabase.query() to return a cursor and then calls convertRows() to extract and return
+ * list of data.
+ *
+ * Throws [IllegalArgumentException] from CursorUtil if column does not exist.
+ *
+ * @param params load params to calculate query limit and offset
+ * @param sourceQuery user provided database query
+ * @param db the [RoomDatabase] to query from
+ * @param itemCount the db row count, triggers a new PagingSource generation if itemCount changes,
+ * i.e. items are added / removed
+ * @param convertRows the function to iterate data with provided [androidx.sqlite.SQLiteStatement]
+ * to return List<Value>
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+suspend fun <Value : Any> queryDatabase(
+ params: LoadParams<Int>,
+ sourceQuery: RoomRawQuery,
+ db: RoomDatabase,
+ itemCount: Int,
+ convertRows: (SQLiteStatement, Int) -> List<Value>
+): LoadResult<Int, Value> {
+ val key = params.key ?: 0
+ val limit = getLimit(params, key)
+ val offset = getOffset(params, key, itemCount)
+ val rowsCount =
+ if (limit + offset > itemCount) {
+ itemCount - offset
+ } else {
+ limit
+ }
+ val limitOffsetQuery = "SELECT * FROM ( ${sourceQuery.sql} ) LIMIT $limit OFFSET $offset"
+
+ val data: List<Value> =
+ db.useReaderConnection { connection ->
+ connection.usePrepared(limitOffsetQuery) { stmt ->
+ sourceQuery.getBindingFunction().invoke(stmt)
+ convertRows(stmt, rowsCount)
+ }
+ }
+
+ val nextPosToLoad = offset + data.size
+ val nextKey =
+ if (data.isEmpty() || data.size < limit || nextPosToLoad >= itemCount) {
+ null
+ } else {
+ nextPosToLoad
+ }
+ val prevKey = if (offset <= 0 || data.isEmpty()) null else offset
+ return LoadResult.Page(
+ data = data,
+ prevKey = prevKey,
+ nextKey = nextKey,
+ itemsBefore = offset,
+ itemsAfter = maxOf(0, itemCount - nextPosToLoad)
+ )
+}
+
+/**
+ * Returns count of requested items to calculate itemsAfter and itemsBefore for use in creating
+ * LoadResult.Page<>.
+ *
+ * Throws error when the column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [Integer.MIN_VALUE, Integer.MAX_VALUE].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+suspend fun queryItemCount(sourceQuery: RoomRawQuery, db: RoomDatabase): Int {
+ val countQuery = "SELECT COUNT(*) FROM ( ${sourceQuery.sql} )"
+ return db.useReaderConnection { connection ->
+ connection.usePrepared(countQuery) { stmt ->
+ sourceQuery.getBindingFunction().invoke(stmt)
+ if (stmt.step()) {
+ stmt.getInt(0)
+ } else {
+ 0
+ }
+ }
+ }
+}
+
+/**
+ * Returns the key for [PagingSource] for a non-initial REFRESH load.
+ *
+ * To prevent a negative key, key is clipped to 0 when the number of items available before
+ * anchorPosition is less than the requested amount of initialLoadSize / 2.
+ */
+@Suppress("AutoBoxing")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun <Value : Any> PagingState<Int, Value>.getClippedRefreshKey(): Int? {
+ return when (val anchorPosition = anchorPosition) {
+ null -> null
+ /**
+ * It is unknown whether anchorPosition represents the item at the top of the screen or item
+ * at the bottom of the screen. To ensure the number of items loaded is enough to fill up
+ * the screen, half of loadSize is loaded before the anchorPosition and the other half is
+ * loaded after the anchorPosition -- anchorPosition becomes the middle item.
+ */
+ else -> maxOf(0, anchorPosition - (config.initialLoadSize / 2))
+ }
+}
diff --git a/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt b/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
new file mode 100644
index 0000000..368afb0
--- /dev/null
+++ b/room/room-paging/src/jvmNativeMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.jvmNative.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.paging
+
+import androidx.annotation.RestrictTo
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import androidx.room.RoomDatabase
+import androidx.room.RoomRawQuery
+import androidx.room.paging.CommonLimitOffsetImpl.Companion.BUG_LINK
+import androidx.room.paging.util.getClippedRefreshKey
+import androidx.sqlite.SQLiteStatement
+
+/**
+ * An implementation of [PagingSource] to perform a LIMIT OFFSET query
+ *
+ * This class is used for Paging3 to perform Query and RawQuery in Room to return a PagingSource for
+ * Pager's consumption. Registers observers on tables lazily and automatically invalidates itself
+ * when data changes.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+actual abstract class LimitOffsetPagingSource<Value : Any>
+actual constructor(
+ actual val sourceQuery: RoomRawQuery,
+ actual val db: RoomDatabase,
+ vararg tables: String,
+) : PagingSource<Int, Value>() {
+ private val implementation = CommonLimitOffsetImpl(tables, this, ::convertRows)
+
+ actual val itemCount: Int
+ get() = implementation.itemCount.value
+
+ override val jumpingSupported: Boolean
+ get() = true
+
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> =
+ implementation.load(params)
+
+ override fun getRefreshKey(state: PagingState<Int, Value>): Int? = state.getClippedRefreshKey()
+
+ protected actual open fun convertRows(statement: SQLiteStatement, itemCount: Int): List<Value> {
+ throw NotImplementedError(
+ "Unexpected call to a function with no implementation that Room is suppose to " +
+ "generate. Please file a bug at: $BUG_LINK."
+ )
+ }
+}
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 2799d77..80ef901 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -349,6 +349,7 @@
method public void bindNull(int index);
method public void bindString(int index, String value);
method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
+ method public void bindTo(androidx.sqlite.SQLiteStatement statement);
method public void clearBindings();
method public void close();
method public void copyArgumentsFrom(androidx.room.RoomSQLiteQuery other);
diff --git a/room/room-runtime/bcv/native/current.txt b/room/room-runtime/bcv/native/current.txt
index 1f54a37..b7f3618 100644
--- a/room/room-runtime/bcv/native/current.txt
+++ b/room/room-runtime/bcv/native/current.txt
@@ -381,6 +381,7 @@
final fun refreshAsync() // androidx.room/InvalidationTracker.refreshAsync|refreshAsync(){}[0]
final fun stop() // androidx.room/InvalidationTracker.stop|stop(){}[0]
+ final suspend fun refreshInvalidation() // androidx.room/InvalidationTracker.refreshInvalidation|refreshInvalidation(){}[0]
final suspend fun subscribe(androidx.room/InvalidationTracker.Observer) // androidx.room/InvalidationTracker.subscribe|subscribe(androidx.room.InvalidationTracker.Observer){}[0]
final suspend fun unsubscribe(androidx.room/InvalidationTracker.Observer) // androidx.room/InvalidationTracker.unsubscribe|unsubscribe(androidx.room.InvalidationTracker.Observer){}[0]
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoCloserTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoCloserTest.kt
index bd36947..f525bd5 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoCloserTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoCloserTest.kt
@@ -27,8 +27,8 @@
import androidx.testutils.assertThrows
import java.io.IOException
import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -87,14 +87,12 @@
@After
fun cleanUp() {
- testWatch.step()
// At the end of all tests we always expect to auto-close the database
assertWithMessage("Database was not closed").that(autoCloser.delegateDatabase).isNull()
- testCoroutineScope.cancel()
}
@Test
- fun refCountsCounted() {
+ fun refCountsCounted() = runTest {
autoCloser.incrementCountAndEnsureDbIsOpen()
assertThat(autoCloser.refCountForTest).isEqualTo(1)
@@ -114,14 +112,14 @@
}
@Test
- fun executeRefCountingFunctionPropagatesFailure() {
+ fun executeRefCountingFunctionPropagatesFailure() = runTest {
assertThrows<IOException> { autoCloser.executeRefCountingFunction { throw IOException() } }
assertThat(autoCloser.refCountForTest).isEqualTo(0)
}
@Test
- fun dbNotClosedWithRefCountIncremented() {
+ fun dbNotClosedWithRefCountIncremented() = runTest {
autoCloser.incrementCountAndEnsureDbIsOpen()
testWatch.step()
@@ -132,7 +130,7 @@
}
@Test
- fun getDelegatedDatabaseReturnsUnwrappedDatabase() {
+ fun getDelegatedDatabaseReturnsUnwrappedDatabase() = runTest {
assertThat(autoCloser.delegateDatabase).isNull()
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
@@ -152,7 +150,7 @@
}
@Test
- fun refCountStaysIncrementedWhenErrorIsEncountered() {
+ fun refCountStaysIncrementedWhenErrorIsEncountered() = runTest {
callback.throwOnOpen = true
assertThrows<IOException> { autoCloser.incrementCountAndEnsureDbIsOpen() }
@@ -163,7 +161,7 @@
}
@Test
- fun testDbCanBeManuallyClosed() {
+ fun testDbCanBeManuallyClosed() = runTest {
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
assertThat(db.isOpen).isTrue()
@@ -180,4 +178,10 @@
assertThrows<IllegalStateException> { autoCloser.incrementCountAndEnsureDbIsOpen() }
}
+
+ private fun runTest(testBody: suspend TestScope.() -> Unit) =
+ testCoroutineScope.runTest {
+ testBody.invoke(this)
+ testWatch.step()
+ }
}
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperFactoryTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperFactoryTest.kt
index 7bb7cd1..373abbf 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperFactoryTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperFactoryTest.kt
@@ -25,8 +25,8 @@
import androidx.test.core.app.ApplicationProvider
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -65,14 +65,12 @@
@After
fun cleanUp() {
- testWatch.step()
// At the end of all tests we always expect to auto-close the database
assertWithMessage("Database was not closed").that(autoCloser.delegateDatabase).isNull()
- testCoroutineScope.cancel()
}
@Test
- fun testCallbacksCalled() {
+ fun testCallbacksCalled() = runTest {
val callbackCount = AtomicInteger()
val countingCallback =
@@ -121,7 +119,7 @@
}
@Test
- fun testDatabaseIsOpenForSlowCallbacks() {
+ fun testDatabaseIsOpenForSlowCallbacks() = runTest {
val refCountCheckingCallback =
object : SupportSQLiteOpenHelper.Callback(1) {
@SuppressLint("BanThreadSleep")
@@ -162,4 +160,10 @@
val db = autoClosingRoomOpenHelper.writableDatabase
assertTrue(db.isOpen)
}
+
+ private fun runTest(testBody: suspend TestScope.() -> Unit) =
+ testCoroutineScope.runTest {
+ testBody.invoke(this)
+ testWatch.step()
+ }
}
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperTest.kt
index 4781635..0f0474c 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/support/AutoClosingRoomOpenHelperTest.kt
@@ -28,8 +28,8 @@
import androidx.testutils.assertThrows
import java.io.IOException
import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -89,14 +89,12 @@
@After
fun cleanUp() {
- testWatch.step()
// At the end of all tests we always expect to auto-close the database
assertWithMessage("Database was not closed").that(autoCloser.delegateDatabase).isNull()
- testCoroutineScope.cancel()
}
@Test
- fun testQueryFailureDecrementsRefCount() {
+ fun testQueryFailureDecrementsRefCount() = runTest {
assertThrows<SQLiteException> {
autoClosingRoomOpenHelper.writableDatabase.query("select * from nonexistanttable")
}
@@ -105,7 +103,7 @@
}
@Test
- fun testCursorKeepsDbAlive() {
+ fun testCursorKeepsDbAlive() = runTest {
autoClosingRoomOpenHelper.writableDatabase.execSQL("create table user (idk int)")
val cursor = autoClosingRoomOpenHelper.writableDatabase.query("select * from user")
@@ -115,7 +113,7 @@
}
@Test
- fun testTransactionKeepsDbAlive() {
+ fun testTransactionKeepsDbAlive() = runTest {
autoClosingRoomOpenHelper.writableDatabase.beginTransaction()
assertThat(autoClosingRoomOpenHelper.autoCloser.refCountForTest).isEqualTo(1)
autoClosingRoomOpenHelper.writableDatabase.endTransaction()
@@ -123,7 +121,7 @@
}
@Test
- fun enableWriteAheadLogging_onOpenHelper() {
+ fun enableWriteAheadLogging_onOpenHelper() = runTest {
autoClosingRoomOpenHelper.setWriteAheadLoggingEnabled(true)
assertThat(autoClosingRoomOpenHelper.writableDatabase.isWriteAheadLoggingEnabled).isTrue()
@@ -133,7 +131,7 @@
}
@Test
- fun testEnableWriteAheadLogging_onSupportSqliteDatabase_throwsUnsupportedOperation() {
+ fun testEnableWriteAheadLogging_onSupportSqliteDatabase_throwsUnsupportedOperation() = runTest {
assertThrows<UnsupportedOperationException> {
autoClosingRoomOpenHelper.writableDatabase.enableWriteAheadLogging()
}
@@ -144,7 +142,7 @@
}
@Test
- fun testStatementReturnedByCompileStatement_doesNotKeepDatabaseOpen() {
+ fun testStatementReturnedByCompileStatement_doesNotKeepDatabaseOpen() = runTest {
val db = autoClosingRoomOpenHelper.writableDatabase
db.execSQL("create table user (idk int)")
@@ -157,7 +155,7 @@
}
@Test
- fun testStatementReturnedByCompileStatement_reOpensDatabase() {
+ fun testStatementReturnedByCompileStatement_reOpensDatabase() = runTest {
val db = autoClosingRoomOpenHelper.writableDatabase
db.execSQL("create table user (idk int)")
@@ -173,7 +171,7 @@
}
@Test
- fun testStatementReturnedByCompileStatement_worksWithBinds() {
+ fun testStatementReturnedByCompileStatement_worksWithBinds() = runTest {
val db = autoClosingRoomOpenHelper.writableDatabase
db.execSQL("create table users (i int, d double, b blob, n int, s string)")
@@ -213,7 +211,7 @@
}
@Test
- fun testGetDelegate() {
+ fun testGetDelegate() = runTest {
val delegateOpenHelper =
FrameworkSQLiteOpenHelperFactory()
.create(
@@ -236,4 +234,10 @@
assertThat(autoClosing.delegate).isSameInstanceAs(delegateOpenHelper)
}
+
+ private fun runTest(testBody: suspend TestScope.() -> Unit) =
+ testCoroutineScope.runTest {
+ testBody.invoke(this)
+ testWatch.step()
+ }
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationLiveDataContainer.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationLiveDataContainer.android.kt
index 1673b96..20995b3 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationLiveDataContainer.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationLiveDataContainer.android.kt
@@ -36,13 +36,12 @@
inTransaction: Boolean,
callableFunction: Callable<T?>
): LiveData<T> {
- return RoomTrackingLiveData(
+ return RoomCallableTrackingLiveData(
database = database,
container = this,
inTransaction = inTransaction,
- callableFunction = callableFunction,
- lambdaFunction = null,
- tableNames = tableNames
+ tableNames = tableNames,
+ callableFunction = callableFunction
)
}
@@ -51,13 +50,12 @@
inTransaction: Boolean,
lambdaFunction: (SQLiteConnection) -> T?
): LiveData<T> {
- return RoomTrackingLiveData(
+ return RoomLambdaTrackingLiveData(
database = database,
container = this,
inTransaction = inTransaction,
- callableFunction = null,
- lambdaFunction = lambdaFunction,
- tableNames = tableNames
+ tableNames = tableNames,
+ lambdaFunction = lambdaFunction
)
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
index b030dab..6c3b88d 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
@@ -272,6 +272,11 @@
implementation.refreshInvalidationAsync(onRefreshScheduled, onRefreshCompleted)
}
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun refreshInvalidation() {
+ implementation.refreshInvalidation(onRefreshScheduled, onRefreshCompleted)
+ }
+
/** Check versions for tables, and run observers synchronously if tables have been updated. */
@WorkerThread
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index 2cd882f..b63a06e 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -549,9 +549,6 @@
* Once a [RoomDatabase] is closed it should no longer be used.
*/
actual open fun close() {
- if (inCompatibilityMode() && !isOpen) {
- return
- }
closeBarrier.close()
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
index 404d8e1..b0a6438 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
@@ -19,6 +19,7 @@
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
+import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteProgram
import androidx.sqlite.db.SupportSQLiteQuery
import java.util.TreeMap
@@ -92,6 +93,18 @@
}
}
+ fun bindTo(statement: SQLiteStatement) {
+ for (index in 1..argCount) {
+ when (bindingTypes[index]) {
+ NULL -> statement.bindNull(index)
+ LONG -> statement.bindLong(index, longBindings[index])
+ DOUBLE -> statement.bindDouble(index, doubleBindings[index])
+ STRING -> statement.bindText(index, requireNotNull(stringBindings[index]))
+ BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index]))
+ }
+ }
+ }
+
override fun bindNull(index: Int) {
bindingTypes[index] = NULL
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 9450a2b..5115d6f 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -15,6 +15,8 @@
*/
package androidx.room
+import androidx.annotation.MainThread
+import androidx.arch.core.executor.ArchTaskExecutor
import androidx.lifecycle.LiveData
import androidx.room.util.performSuspending
import androidx.sqlite.SQLiteConnection
@@ -35,18 +37,16 @@
* This [LiveData] keeps a weak observer to the [InvalidationTracker] but it is hold strongly by the
* [InvalidationTracker] as long as it is active.
*/
-internal class RoomTrackingLiveData<T>(
- private val database: RoomDatabase,
+internal sealed class RoomTrackingLiveData<T>(
+ protected val database: RoomDatabase,
private val container: InvalidationLiveDataContainer,
- private val inTransaction: Boolean,
- private val callableFunction: Callable<T?>?,
- private val lambdaFunction: ((SQLiteConnection) -> T?)?,
+ protected val inTransaction: Boolean,
tableNames: Array<out String>
) : LiveData<T>() {
private val observer: InvalidationTracker.Observer =
object : InvalidationTracker.Observer(tableNames) {
override fun onInvalidated(tables: Set<String>) {
- database.getCoroutineScope().launch { invalidated() }
+ ArchTaskExecutor.getInstance().executeOnMainThread { invalidated() }
}
}
private val invalid = AtomicBoolean(true)
@@ -74,22 +74,7 @@
while (invalid.compareAndSet(true, false)) {
computed = true
try {
- value =
- if (callableFunction != null) {
- withContext(
- if (inTransaction) {
- database.getTransactionContext()
- } else {
- database.getQueryContext()
- }
- ) {
- callableFunction.call()
- }
- } else if (lambdaFunction != null) {
- performSuspending(database, true, inTransaction, lambdaFunction)
- } else {
- error("Both callable and lambda functions are null")
- }
+ value = compute()
} catch (e: Exception) {
throw RuntimeException(
"Exception while computing database live data.",
@@ -115,15 +100,18 @@
} while (computed && invalid.get())
}
- private suspend fun invalidated() {
+ @MainThread
+ private fun invalidated() {
val isActive = hasActiveObservers()
if (invalid.compareAndSet(false, true)) {
if (isActive) {
- refresh()
+ database.getCoroutineScope().launch { refresh() }
}
}
}
+ abstract suspend fun compute(): T?
+
override fun onActive() {
super.onActive()
container.onActive(this)
@@ -135,3 +123,33 @@
container.onInactive(this)
}
}
+
+internal class RoomCallableTrackingLiveData<T>(
+ database: RoomDatabase,
+ container: InvalidationLiveDataContainer,
+ inTransaction: Boolean,
+ tableNames: Array<out String>,
+ private val callableFunction: Callable<T?>
+) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
+ override suspend fun compute(): T? {
+ val queryContext =
+ if (inTransaction) {
+ database.getTransactionContext()
+ } else {
+ database.getQueryContext()
+ }
+ return withContext(queryContext) { callableFunction.call() }
+ }
+}
+
+internal class RoomLambdaTrackingLiveData<T>(
+ database: RoomDatabase,
+ container: InvalidationLiveDataContainer,
+ inTransaction: Boolean,
+ tableNames: Array<out String>,
+ private val lambdaFunction: ((SQLiteConnection) -> T?)
+) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
+ override suspend fun compute(): T? {
+ return performSuspending(database, true, inTransaction, lambdaFunction)
+ }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
index d7538dc..bd5d28e 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
@@ -92,6 +92,8 @@
*/
fun refreshAsync()
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) suspend fun refreshInvalidation()
+
/** Stops invalidation tracker operations. */
internal fun stop()
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
index 578843ad..b1fee15 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
@@ -97,6 +97,11 @@
implementation.refreshInvalidationAsync()
}
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ actual suspend fun refreshInvalidation() {
+ implementation.refreshInvalidation()
+ }
+
/** Stops invalidation tracker operations. */
actual fun stop() {}
diff --git a/settings.gradle b/settings.gradle
index c02141b..ded0605 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -26,7 +26,7 @@
dependencies {
// upgrade protobuf to be compatible with AGP
classpath("com.google.protobuf:protobuf-java:3.22.3")
- classpath("com.gradle:develocity-gradle-plugin:3.17.2")
+ classpath("com.gradle:develocity-gradle-plugin:3.18")
classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
@@ -419,6 +419,8 @@
includeProject(":camera:camera-camera2-pipe", [BuildType.CAMERA])
includeProject(":camera:camera-camera2-pipe-integration", [BuildType.CAMERA])
includeProject(":camera:camera-camera2-pipe-testing", [BuildType.CAMERA])
+includeProject(":camera:camera-compose", [BuildType.CAMERA])
+includeProject(":camera:camera-compose:camera-compose-samples", "camera/camera-compose/samples", [BuildType.CAMERA])
includeProject(":camera:camera-core", [BuildType.CAMERA])
includeProject(":camera:camera-effects", [BuildType.CAMERA])
includeProject(":camera:camera-effects-still-portrait", [BuildType.CAMERA])
@@ -506,6 +508,7 @@
includeProject(":compose:material3:adaptive:adaptive", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-layout", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-navigation", [BuildType.COMPOSE])
+includeProject(":compose:material3:adaptive:adaptive-render-strategy", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-samples", "compose/material3/adaptive/samples", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-benchmark", "compose/material3/adaptive/benchmark", [BuildType.COMPOSE])
includeProject(":compose:material3:material3", [BuildType.COMPOSE])
@@ -596,6 +599,7 @@
includeProject(":contentpager:contentpager", [BuildType.MAIN])
includeProject(":coordinatorlayout:coordinatorlayout", [BuildType.MAIN])
includeProject(":core:core", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":core:core:core-samples", "core/core/samples", [BuildType.MAIN])
includeProject(":core:core-testing", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":core:core:integration-tests:publishing", [BuildType.MAIN])
includeProject(":core:core-animation", [BuildType.MAIN])
@@ -654,6 +658,7 @@
includeProject(":datastore:datastore-rxjava2", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":datastore:datastore-rxjava3", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
includeProject(":datastore:datastore-sampleapp", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
+includeProject(":datastore:integration-tests:testapp", [BuildType.MAIN])
includeProject(":documentfile:documentfile", [BuildType.MAIN])
includeProject(":draganddrop:draganddrop", [BuildType.MAIN])
includeProject(":draganddrop:integration-tests:sampleapp", [BuildType.MAIN])
@@ -971,7 +976,6 @@
includeProject(":tv:tv-foundation", [BuildType.COMPOSE])
includeProject(":tv:tv-material", [BuildType.COMPOSE])
includeProject(":tv:integration-tests:playground", [BuildType.COMPOSE])
-includeProject(":tv:integration-tests:presentation", [BuildType.COMPOSE])
includeProject(":tv:integration-tests:macrobenchmark", [BuildType.COMPOSE])
includeProject(":tv:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
includeProject(":tv:tv-material-samples", "tv/tv-material/samples", [BuildType.COMPOSE])
diff --git a/slice/slice-core/api/restricted_current.txt b/slice/slice-core/api/restricted_current.txt
index bbe0f72..45a6740 100644
--- a/slice/slice-core/api/restricted_current.txt
+++ b/slice/slice-core/api/restricted_current.txt
@@ -243,7 +243,7 @@
method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int parseImageMode(androidx.slice.SliceItem);
method @Deprecated public void setActivity(boolean);
method @Deprecated public androidx.slice.core.SliceActionImpl setChecked(boolean);
- method @Deprecated public androidx.slice.core.SliceAction? setContentDescription(CharSequence);
+ method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
method @Deprecated public androidx.slice.core.SliceActionImpl setKey(String);
method @Deprecated public androidx.slice.core.SliceActionImpl setPriority(@IntRange(from=0) int);
}
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
index 42ae49f..92d1c47 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
@@ -263,7 +263,6 @@
* @param description the content description for this action.
* @return
*/
- @Nullable
@Override
public @NonNull SliceAction setContentDescription(@NonNull CharSequence description) {
mContentDescription = description;
diff --git a/slice/slice-test/src/main/java/androidx/slice/test/SampleSliceProvider.java b/slice/slice-test/src/main/java/androidx/slice/test/SampleSliceProvider.java
index 379b763..150979e 100644
--- a/slice/slice-test/src/main/java/androidx/slice/test/SampleSliceProvider.java
+++ b/slice/slice-test/src/main/java/androidx/slice/test/SampleSliceProvider.java
@@ -1680,7 +1680,7 @@
private SetHostExtraApi21Impl() {}
static void setHostExtra(ListBuilder listBuilder, String key, String value) {
PersistableBundle extras = new PersistableBundle();
- extras.putString("tts", "hello world");
+ extras.putString(key, value);
// Attach additional information for host. Depending on the host apps, this
// information might or might not be used.
// In this case, SliceBrowser is customized to play TTS when binding the slice.
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java b/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
index e05be8d..4287f34 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
@@ -354,7 +354,6 @@
*
* @return the current value of a progress bar or input range associated with this slice.
*/
- @NonNull
public int getRangeValue() {
if (mTemplateType == ROW_TYPE_SLIDER
|| mTemplateType == ROW_TYPE_PROGRESS) {
diff --git a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt
index a09be1b..86583ab 100644
--- a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt
+++ b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt
@@ -361,44 +361,6 @@
}
assertWithMessage("non-transparent pixels were drawn").that(hasNonTransparentPixel).isTrue()
}
-
- @Test
- fun skippedMeasurePassIsCorrected() {
- val context = InstrumentationRegistry.getInstrumentation().context
- val spl = createTestSpl(context, collapsibleContentViews = true)
-
- fun assertAdjacentSiblings(message: String) {
- val (leftChild, rightChild) = spl.leftAndRightViews()
- assertWithMessage("adjacent view edges: $message")
- .that(rightChild.left)
- .isEqualTo(leftChild.right)
- }
-
- assertAdjacentSiblings("initial layout")
-
- spl.splitDividerPosition = 0
- spl.measureAndLayoutForTest()
-
- assertAdjacentSiblings("with splitDividerPosition = 0")
-
- val (left, right) = spl.leftAndRightViews()
- assertWithMessage("left child width").that(left.width).isEqualTo(0)
- assertWithMessage("right child width").that(right.width).isEqualTo(100)
- }
-}
-
-private fun SlidingPaneLayout.leftAndRightViews(): Pair<View, View> {
- val isRtl = this.layoutDirection == View.LAYOUT_DIRECTION_RTL
- val leftChild: View
- val rightChild: View
- if (isRtl) {
- leftChild = this[1]
- rightChild = this[0]
- } else {
- leftChild = this[0]
- rightChild = this[1]
- }
- return leftChild to rightChild
}
private fun View.drawToBitmap(): Bitmap {
@@ -411,40 +373,31 @@
private fun createTestSpl(
context: Context,
setDividerDrawable: Boolean = true,
- childPanesAcceptTouchEvents: Boolean = false,
- collapsibleContentViews: Boolean = false
+ childPanesAcceptTouchEvents: Boolean = false
): SlidingPaneLayout =
SlidingPaneLayout(context).apply {
addView(
TestPaneView(context).apply {
- val lpWidth: Int
- if (collapsibleContentViews) {
- lpWidth = 0
- } else {
- minimumWidth = 30
- lpWidth = LayoutParams.WRAP_CONTENT
- }
+ minimumWidth = 30
acceptTouchEvents = childPanesAcceptTouchEvents
layoutParams =
- SlidingPaneLayout.LayoutParams(lpWidth, LayoutParams.MATCH_PARENT).apply {
- weight = 1f
- }
+ SlidingPaneLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT
+ )
+ .apply { weight = 1f }
}
)
addView(
TestPaneView(context).apply {
- val lpWidth: Int
- if (collapsibleContentViews) {
- lpWidth = 0
- } else {
- minimumWidth = 30
- lpWidth = LayoutParams.WRAP_CONTENT
- }
+ minimumWidth = 30
acceptTouchEvents = childPanesAcceptTouchEvents
layoutParams =
- SlidingPaneLayout.LayoutParams(lpWidth, LayoutParams.MATCH_PARENT).apply {
- weight = 1f
- }
+ SlidingPaneLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT
+ )
+ .apply { weight = 1f }
}
)
isUserResizingEnabled = true
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt
index e8b3a24..12434f8 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt
@@ -192,11 +192,7 @@
}
private inline val View.spLayoutParams: SlidingPaneLayout.LayoutParams
- get() =
- when (val layoutParams = layoutParams) {
- is SlidingPaneLayout.LayoutParams -> layoutParams
- else -> layoutParamsError(this, layoutParams)
- }
+ get() = layoutParams as SlidingPaneLayout.LayoutParams
/**
* SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level of a UI. A
@@ -986,7 +982,7 @@
if (child.visibility == GONE) return@forEachIndexed
val lp = child.spLayoutParams
val skippedFirstPass = !lp.canInfluenceParentSize || lp.weightOnlyWidth
- val firstPassMeasuredWidth = if (skippedFirstPass) 0 else child.measuredWidth
+ val measuredWidth = if (skippedFirstPass) 0 else child.measuredWidth
val newWidth =
when {
// Child view consumes available space if the combined width cannot fit into
@@ -999,7 +995,7 @@
val widthToDistribute = widthRemaining.coerceAtLeast(0)
val addedWidth =
(lp.weight * widthToDistribute / weightSum).roundToInt()
- firstPassMeasuredWidth + addedWidth
+ measuredWidth + addedWidth
} else { // Explicit dividing line is defined
val clampedPos =
dividerPos
@@ -1019,9 +1015,9 @@
widthAvailable - lp.horizontalMargin - totalMeasuredWidth
}
lp.width > 0 -> lp.width
- else -> firstPassMeasuredWidth
+ else -> measuredWidth
}
- if (newWidth != child.measuredWidth) {
+ if (measuredWidth != newWidth) {
val childWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY)
val childHeightSpec =
getChildHeightMeasureSpec(
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index 1d4f54a..62969ec 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -140,7 +140,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressMenu();
- assertTrue(textView.wait(Until.textEquals("keycode menu pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode menu pressed; "), TIMEOUT_MS));
}
@Test
@@ -149,7 +149,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressBack();
- assertTrue(textView.wait(Until.textEquals("keycode back pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode back pressed; "), TIMEOUT_MS));
}
@Test
@@ -158,7 +158,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressSearch();
- assertTrue(textView.wait(Until.textEquals("keycode search pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode search pressed; "), TIMEOUT_MS));
}
@Test
@@ -167,7 +167,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressDPadCenter();
- assertTrue(textView.wait(Until.textEquals("keycode dpad center pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode dpad center pressed; "), TIMEOUT_MS));
}
@Test
@@ -176,7 +176,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressDPadDown();
- assertTrue(textView.wait(Until.textEquals("keycode dpad down pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode dpad down pressed; "), TIMEOUT_MS));
}
@Test
@@ -185,7 +185,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressDPadUp();
- assertTrue(textView.wait(Until.textEquals("keycode dpad up pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode dpad up pressed; "), TIMEOUT_MS));
}
@Test
@@ -194,7 +194,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressDPadLeft();
- assertTrue(textView.wait(Until.textEquals("keycode dpad left pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode dpad left pressed; "), TIMEOUT_MS));
}
@Test
@@ -203,7 +203,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressDPadRight();
- assertTrue(textView.wait(Until.textEquals("keycode dpad right pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode dpad right pressed; "), TIMEOUT_MS));
}
@Test
@@ -212,7 +212,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressDelete();
- assertTrue(textView.wait(Until.textEquals("keycode delete pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode delete pressed; "), TIMEOUT_MS));
}
@Test
@@ -221,7 +221,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressEnter();
- assertTrue(textView.wait(Until.textEquals("keycode enter pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode enter pressed; "), TIMEOUT_MS));
}
@Test
@@ -230,7 +230,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressKeyCode(KeyEvent.KEYCODE_0);
- assertTrue(textView.wait(Until.textEquals("keycode 0 pressed"), TIMEOUT_MS));
+ assertTrue(textView.wait(Until.textEquals("keycode 0 pressed; "), TIMEOUT_MS));
}
@Test
@@ -240,7 +240,31 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressKeyCode(KeyEvent.KEYCODE_Z,
KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
- assertTrue(textView.wait(Until.textEquals("keycode Z pressed with meta shift left on"),
+ assertTrue(textView.wait(Until.textEquals("keycode Z pressed; with meta shift left on; "),
+ TIMEOUT_MS));
+ }
+
+ @Test
+ public void testPressKeyCodes_withMetaKeyCodes() {
+ launchTestActivity(KeycodeTestActivity.class);
+
+ UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
+ mDevice.pressKeyCodes(new int[]{KeyEvent.KEYCODE_Z,
+ KeyEvent.KEYCODE_SHIFT_LEFT});
+ assertTrue(textView.wait(Until.textEquals(
+ "keycode Z pressed; keycode shift left pressed; with meta shift left on; "),
+ TIMEOUT_MS));
+ }
+
+ @Test
+ public void testPressKeyCodes_withMetaKeyCodesReverseOrder() {
+ launchTestActivity(KeycodeTestActivity.class);
+
+ UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
+ mDevice.pressKeyCodes(new int[]{KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_Z});
+ assertTrue(textView.wait(Until.textEquals(
+ "keycode shift left pressed; with meta shift left on; keycode Z pressed;"
+ + " with meta shift left on; "),
TIMEOUT_MS));
}
@@ -261,7 +285,7 @@
UiObject2 textView = mDevice.findObject(By.res(TEST_APP, "text_view"));
mDevice.pressKeyCodes(new int[]{KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_B});
- assertTrue(textView.wait(Until.textEquals("keycode A and keycode B are pressed"),
+ assertTrue(textView.wait(Until.textEquals("keycode A pressed; keycode B pressed; "),
TIMEOUT_MS));
}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/KeycodeTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/KeycodeTestActivity.java
index a4e91d8..d162bf2 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/KeycodeTestActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/KeycodeTestActivity.java
@@ -31,6 +31,8 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.keycode_test_activity);
+ TextView textView = (TextView) findViewById(R.id.text_view);
+ textView.setText("");
}
@Override
@@ -38,55 +40,54 @@
TextView textView = (TextView) findViewById(R.id.text_view);
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
- textView.setText("keycode back pressed");
+ textView.append("keycode back pressed; ");
break;
case KeyEvent.KEYCODE_DEL:
- textView.setText("keycode delete pressed");
+ textView.append("keycode delete pressed; ");
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
- textView.setText("keycode dpad center pressed");
+ textView.append("keycode dpad center pressed; ");
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
- textView.setText("keycode dpad down pressed");
+ textView.append("keycode dpad down pressed; ");
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
- textView.setText("keycode dpad left pressed");
+ textView.append("keycode dpad left pressed; ");
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
- textView.setText("keycode dpad right pressed");
+ textView.append("keycode dpad right pressed; ");
break;
case KeyEvent.KEYCODE_DPAD_UP:
- textView.setText("keycode dpad up pressed");
+ textView.append("keycode dpad up pressed; ");
break;
case KeyEvent.KEYCODE_ENTER:
- textView.setText("keycode enter pressed");
+ textView.append("keycode enter pressed; ");
break;
case KeyEvent.KEYCODE_MENU:
- textView.setText("keycode menu pressed");
+ textView.append("keycode menu pressed; ");
break;
case KeyEvent.KEYCODE_SEARCH:
- textView.setText("keycode search pressed");
- break;
- case KeyEvent.KEYCODE_Z:
- textView.setText("keycode Z pressed");
- break;
- case KeyEvent.KEYCODE_0:
- textView.setText("keycode 0 pressed");
+ textView.append("keycode search pressed; ");
break;
case KeyEvent.KEYCODE_A:
- if (mLastPressedKeyCode == KeyEvent.KEYCODE_B) {
- textView.setText("keycode A and keycode B are pressed");
- }
+ textView.append("keycode A pressed; ");
break;
case KeyEvent.KEYCODE_B:
- if (mLastPressedKeyCode == KeyEvent.KEYCODE_A) {
- textView.setText("keycode A and keycode B are pressed");
- }
+ textView.append("keycode B pressed; ");
+ break;
+ case KeyEvent.KEYCODE_Z:
+ textView.append("keycode Z pressed; ");
+ break;
+ case KeyEvent.KEYCODE_0:
+ textView.append("keycode 0 pressed; ");
+ break;
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ textView.append("keycode shift left pressed; ");
break;
}
if ((event.getMetaState() & (KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON)) != 0) {
- textView.append(" with meta shift left on");
+ textView.append("with meta shift left on; ");
}
mLastPressedKeyCode = keyCode;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index d2c5eb7..9f71e70 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -34,6 +34,8 @@
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
@@ -61,6 +63,30 @@
// Inserted after each motion event injection.
private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
+ private static final Map<Integer, Integer> KEY_MODIFIER = new HashMap<>();
+
+ static {
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_SHIFT_LEFT,
+ KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_SHIFT_RIGHT,
+ KeyEvent.META_SHIFT_RIGHT_ON | KeyEvent.META_SHIFT_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_ALT_LEFT,
+ KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_ALT_RIGHT,
+ KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_ALT_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_SYM, KeyEvent.META_SYM_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_FUNCTION, KeyEvent.META_FUNCTION_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.META_CTRL_LEFT_ON | KeyEvent.META_CTRL_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_CTRL_RIGHT,
+ KeyEvent.META_CTRL_RIGHT_ON | KeyEvent.META_CTRL_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_META_LEFT, KeyEvent.META_META_LEFT_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_META_RIGHT, KeyEvent.META_META_RIGHT_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_CAPS_LOCK, KeyEvent.META_CAPS_LOCK_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_NUM_LOCK, KeyEvent.META_NUM_LOCK_ON);
+ KEY_MODIFIER.put(KeyEvent.KEYCODE_SCROLL_LOCK, KeyEvent.META_SCROLL_LOCK_ON);
+ }
+
InteractionController(UiDevice device) {
mDevice = device;
}
@@ -403,6 +429,9 @@
public boolean sendKeys(int[] keyCodes, int metaState) {
final long eventTime = SystemClock.uptimeMillis();
for (int keyCode : keyCodes) {
+ if (KEY_MODIFIER.containsKey(keyCode)) {
+ metaState |= KEY_MODIFIER.get(keyCode);
+ }
KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
@@ -417,6 +446,9 @@
if (!injectEventSync(upEvent)) {
return false;
}
+ if (KEY_MODIFIER.containsKey(keyCode)) {
+ metaState &= ~KEY_MODIFIER.get(keyCode);
+ }
}
return true;
}
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 1c189a3..747addd 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
@@ -481,7 +481,8 @@
}
/**
- * Presses one or more keys.
+ * Presses one or more keys. Keys that change meta state are supported, and will apply their
+ * meta state to following keys.
* <br/>
* For example, you can simulate taking a screenshot on the device by pressing both the
* power and volume down keys.
@@ -497,7 +498,8 @@
}
/**
- * Presses one or more keys.
+ * Presses one or more keys. Keys that change meta state are supported, and will apply their
+ * meta state to following keys.
* <br/>
* For example, you can simulate taking a screenshot on the device by pressing both the
* power and volume down keys.
diff --git a/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java b/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java
index c80073a..a4b47e8 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/TransitionManagerTest.java
@@ -20,16 +20,21 @@
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
+import android.os.Build;
+import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
import androidx.testutils.AnimationDurationScaleRule;
import androidx.transition.test.R;
@@ -37,6 +42,9 @@
import org.junit.Rule;
import org.junit.Test;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
@MediumTest
public class TransitionManagerTest extends BaseTest {
@@ -218,4 +226,48 @@
verify(listener, never()).onTransitionEnd(any(Transition.class));
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void endTransitionOnTransitionCancel() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+
+ View[] views = new View[] {
+ new View(root.getContext()),
+ new View(root.getContext()),
+ new View(root.getContext()),
+ new View(root.getContext()),
+ new View(root.getContext())
+ };
+
+ for (View view : views) {
+ CountDownLatch latch = new CountDownLatch(1);
+ rule.runOnUiThread(() -> {
+ Fade fadeIn = new Fade();
+ fadeIn.addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ TransitionManager.endTransitions(root);
+ }
+ });
+ TransitionSeekController seekController =
+ TransitionManager.controlDelayedTransition(root, fadeIn);
+ ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(10, 10);
+ root.addView(view, layoutParams);
+ assert seekController != null;
+ seekController.addOnReadyListener(transitionSeekController -> latch.countDown());
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+ }
+ for (View view : views) {
+ CountDownLatch latch = new CountDownLatch(1);
+ rule.runOnUiThread(() -> {
+ TransitionSeekController seekController =
+ TransitionManager.controlDelayedTransition(root, new Fade());
+ root.removeView(view);
+ assert seekController != null;
+ seekController.addOnReadyListener(transitionSeekController -> latch.countDown());
+ });
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+ }
+ }
}
diff --git a/transition/transition/src/main/java/androidx/transition/Transition.java b/transition/transition/src/main/java/androidx/transition/Transition.java
index f4fc52a..a4b20ea 100644
--- a/transition/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/transition/src/main/java/androidx/transition/Transition.java
@@ -1883,6 +1883,7 @@
ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
WindowId windowId = sceneRoot.getWindowId();
+ ArrayList<Transition> endedTransitions = new ArrayList<>();
for (int i = numOldAnims - 1; i >= 0; i--) {
Animator anim = runningAnimators.keyAt(i);
if (anim != null) {
@@ -1905,14 +1906,9 @@
// a listener
anim.cancel();
transition.mCurrentAnimators.remove(anim);
- runningAnimators.remove(anim);
+ runningAnimators.removeAt(i);
if (transition.mCurrentAnimators.size() == 0) {
- transition.notifyListeners(TransitionNotification.ON_CANCEL, false);
- if (!transition.mEnded) {
- transition.mEnded = true;
- transition.notifyListeners(TransitionNotification.ON_END,
- false);
- }
+ endedTransitions.add(transition);
}
} else if (anim.isRunning() || anim.isStarted()) {
if (DBG) {
@@ -1923,13 +1919,24 @@
if (DBG) {
Log.d(LOG_TAG, "removing anim from info list: " + anim);
}
- runningAnimators.remove(anim);
+ runningAnimators.removeAt(i);
}
}
}
}
}
+ // Don't change the collection we're iterating over while iterating over it.
+ for (int i = 0; i < endedTransitions.size(); i++) {
+ Transition transition = endedTransitions.get(i);
+ transition.notifyListeners(TransitionNotification.ON_CANCEL, false);
+ if (!transition.mEnded) {
+ transition.mEnded = true;
+ transition.notifyListeners(TransitionNotification.ON_END,
+ false);
+ }
+ }
+
createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
if (mSeekController == null) {
runAnimators();
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
index b9cae42..7656d7a 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
@@ -14,22 +14,19 @@
* limitations under the License.
*/
-@file:Suppress("DEPRECATION")
-
package androidx.tv.integration.playground
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
-import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.CollectionItemInfo
@@ -37,27 +34,14 @@
import androidx.compose.ui.semantics.collectionItemInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.list.TvLazyColumn
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.foundation.lazy.list.itemsIndexed
const val rowsCount = 20
const val columnsCount = 100
@Composable
fun LazyRowsAndColumns() {
- var pivotOffset by remember { mutableStateOf(PivotOffsets()) }
- TvLazyColumn(pivotOffsets = pivotOffset, verticalArrangement = Arrangement.spacedBy(20.dp)) {
- items(rowsCount) { rowIndex ->
- SampleLazyRow(
- Modifier.onFocusChanged {
- if (it.hasFocus) {
- pivotOffset = if (rowIndex == 2) PivotOffsets(0f) else PivotOffsets()
- }
- }
- )
- }
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+ items(rowsCount) { SampleLazyRow() }
}
}
@@ -68,7 +52,7 @@
val backgroundColors = List(columnsCount) { colors.random() }
val focusRequester = remember { FocusRequester() }
- TvLazyRow(
+ LazyRow(
modifier = modifier.lazyListSemantics(1, columnsCount).focusRestorer { focusRequester },
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/StickyHeader.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/StickyHeader.kt
index 96e50c1..bcf28a7 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/StickyHeader.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/StickyHeader.kt
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-@file:Suppress("DEPRECATION")
-
package androidx.tv.integration.playground
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
@@ -28,6 +27,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -41,8 +41,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.lazy.list.TvLazyColumn
data class MonthActivity(
val month: String,
@@ -65,10 +63,10 @@
),
)
-@OptIn(ExperimentalTvFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StickyHeaderContent() {
- TvLazyColumn(modifier = Modifier.fillMaxWidth()) {
+ LazyColumn(modifier = Modifier.fillMaxWidth()) {
monthActivities.forEachIndexed { monthIndex, monthActivity ->
val isLastMonth = monthIndex == monthActivities.lastIndex
diff --git a/tv/integration-tests/presentation/README.md b/tv/integration-tests/presentation/README.md
deleted file mode 100644
index 9c46867..0000000
--- a/tv/integration-tests/presentation/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Presentation app
-
-## Setup
-
-* Uncomment the `coil` and `gson` libraries dependency additions from the `build.gradle` file.
-* Uncomment the function content and imports from
- `presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt` file
-* Create the `data.json` file in `presentation/src/main/assets` directory and add the content from
- this link: go/compose-tv-presentation-app-data
-
-> If you are not a Googler and want to use this app for
-> testing, you will have to create the `data.json` file by following the schema mentioned in the
-`Data.kt` file
diff --git a/tv/integration-tests/presentation/build.gradle b/tv/integration-tests/presentation/build.gradle
deleted file mode 100644
index faab995..0000000
--- a/tv/integration-tests/presentation/build.gradle
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.LibraryType
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.application")
- id("AndroidXComposePlugin")
- id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
- implementation(libs.kotlinStdlib)
-
- def composeVersion = "1.6.7"
-
- implementation("androidx.activity:activity-compose:1.9.0")
- implementation("androidx.appcompat:appcompat:1.6.1")
- implementation("androidx.compose.animation:animation:$composeVersion")
- implementation("androidx.compose.foundation:foundation-layout:$composeVersion")
- implementation("androidx.compose.material:material-icons-core:$composeVersion")
- implementation("androidx.compose.material:material-icons-extended:$composeVersion")
- implementation("androidx.compose.material3:material3:1.2.1")
- implementation("androidx.compose.runtime:runtime:$composeVersion")
- implementation("androidx.compose.ui:ui:$composeVersion")
- implementation("androidx.navigation:navigation-runtime:2.7.7")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
- implementation(project(":tv:tv-foundation"))
- implementation(project(":tv:tv-material"))
-}
-
-android {
- compileSdk 35
- defaultConfig {
- minSdkVersion 28
- }
-
- buildTypes {
- release {
- minifyEnabled true
- shrinkResources true
- proguardFiles getDefaultProguardFile('proguard-android.txt')
- signingConfig signingConfigs.debug
- }
- }
- namespace "androidx.tv.integration.presentation"
-}
diff --git a/tv/integration-tests/presentation/lint-baseline.xml b/tv/integration-tests/presentation/lint-baseline.xml
deleted file mode 100644
index f05a7c4..0000000
--- a/tv/integration-tests/presentation/lint-baseline.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
-
- <issue
- id="ModifierParameter"
- message="Modifier parameter should be the first optional parameter"
- errorLine1="fun FeaturedCarousel(movies: List<Movie> = featuredCarouselMovies, modifier: Modifier = Modifier) {"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt"/>
- </issue>
-
- <issue
- id="UnnecessaryLambdaCreation"
- message="Creating an unnecessary lambda to emit a captured lambda"
- errorLine1=" content()"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt"/>
- </issue>
-
- <issue
- id="AutoboxingStateCreation"
- message="Prefer `mutableIntStateOf` instead of `mutableStateOf`"
- errorLine1=" var selectedTabIndex by remember { mutableStateOf(0) }"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/tv/integration/presentation/App.kt"/>
- </issue>
-
- <issue
- id="AutoboxingStateCreation"
- message="Prefer `mutableFloatStateOf` instead of `mutableStateOf`"
- errorLine1=" var height by remember { mutableStateOf(0f) }"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt"/>
- </issue>
-
-</issues>
diff --git a/tv/integration-tests/presentation/src/main/AndroidManifest.xml b/tv/integration-tests/presentation/src/main/AndroidManifest.xml
deleted file mode 100644
index 3a078f4..0000000
--- a/tv/integration-tests/presentation/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools">
-
- <application
- android:allowBackup="true"
- android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
- android:supportsRtl="true"
- android:theme="@style/Theme.Androidx">
- <activity
- android:name=".MainActivity"
- android:banner="@drawable/app_icon_your_company"
- android:exported="true"
- android:icon="@drawable/app_icon_your_company"
- android:label="@string/app_name"
- android:logo="@drawable/app_icon_your_company"
- android:screenOrientation="landscape">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
-
- <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
- </intent-filter>
- </activity>
-
- </application>
-
- <uses-feature
- android:name="android.software.leanback"
- android:required="true" />
- <uses-feature
- android:name="android.hardware.touchscreen"
- android:required="false" />
-
- <uses-permission android:name="android.permission.INTERNET" />
-
-</manifest>
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/assets/.gitignore b/tv/integration-tests/presentation/src/main/assets/.gitignore
deleted file mode 100644
index 114ea57..0000000
--- a/tv/integration-tests/presentation/src/main/assets/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-data.json
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt
deleted file mode 100644
index 9284cd6..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-
-@Composable
-fun AlignmentCenter(
- modifier: Modifier = Modifier,
- horizontalAxis: Boolean = false,
- verticalAxis: Boolean = false,
- content: @Composable RowScope.() -> Unit
-) {
- Row(
- modifier = modifier.fillMaxWidth(),
- horizontalArrangement = if (horizontalAxis) Arrangement.Center else Arrangement.Start,
- verticalAlignment = if (verticalAxis) Alignment.CenterVertically else Alignment.Top,
- ) {
- content()
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/App.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/App.kt
deleted file mode 100644
index 927bbb1..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/App.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-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.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.nativeKeyCode
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
-import androidx.tv.foundation.lazy.list.TvLazyColumn
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.ModalNavigationDrawer
-
-val pageColor = Color(0xff18171a)
-
-enum class Tabs(val displayName: String, val action: @Composable () -> Unit) {
- Home(
- "Home",
- {
- TvLazyColumn(
- modifier = Modifier.fillMaxSize().focusRequester(Home.fr).background(pageColor)
- ) {
- item {
- FeaturedCarousel()
- AppSpacer(height = 50.dp)
- }
- movieCollections.forEach { movieCollection ->
- item {
- AppLazyRow(
- title = movieCollection.label,
- items = movieCollection.items,
- drawItem = { movie, _, modifier ->
- ImageCard(movie, aspectRatio = 2f / 3, modifier = modifier)
- }
- )
- AppSpacer(height = 35.dp)
- }
- }
- }
- }
- ),
- Shows(
- "Shows",
- {
- TvLazyColumn(modifier = Modifier.fillMaxSize().background(pageColor)) {
- item { ShowsGrid(Modifier.focusRequester(Shows.fr)) }
- }
- }
- );
-
- val fr: FocusRequester = FocusRequester()
-}
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun App() {
- val tabs = remember { Tabs.values() }
- var selectedTabIndex by remember { mutableStateOf(0) }
- val activeTab = remember(selectedTabIndex) { tabs[selectedTabIndex] }
-
- val tabRow =
- @Composable {
- AppTabRow(
- tabs = tabs.map { it.displayName },
- selectedTabIndex = selectedTabIndex,
- onSelectedTabIndexChange = { selectedTabIndex = it },
- modifier =
- Modifier.zIndex(100f).onKeyEvent {
- if (it.key.nativeKeyCode == Key.DirectionDown.nativeKeyCode) {
- activeTab.fr.requestFocus()
- true
- } else false
- }
- )
- }
-
- val activePage: MutableState<(@Composable () -> Unit)> =
- remember(selectedTabIndex) { mutableStateOf(activeTab.action) }
-
- ModalNavigationDrawer(
- drawerContent = {
- Sidebar(selectedIndex = selectedTabIndex, onIndexChange = { selectedTabIndex = it })
- }
- ) {
- Box(modifier = Modifier.fillMaxSize()) {
- activePage.value()
- tabRow()
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppButton.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppButton.kt
deleted file mode 100644
index 39af722..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppButton.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Icon
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Text
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun AppButton(
- text: String,
- icon: ImageVector,
- modifier: Modifier = Modifier,
-) {
- var isFocused by remember { mutableStateOf(false) }
-
- Row(
- modifier =
- modifier
- .border(
- 2.dp,
- if (isFocused) Color.White else Color.Transparent,
- RoundedCornerShape(50)
- )
- .padding(4.dp)
- .background(Color.White.copy(alpha = 0.9f), RoundedCornerShape(50))
- .padding(top = 5.dp, bottom = 5.dp, start = 10.dp, end = 15.dp)
- .onFocusChanged { isFocused = it.isFocused }
- .focusable(),
- horizontalArrangement = Arrangement.spacedBy(0.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = icon,
- contentDescription = null,
- modifier = Modifier.size(25.dp),
- tint = Color.Black
- )
- Text(text = text, fontSize = 12.sp)
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppLazyRow.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppLazyRow.kt
deleted file mode 100644
index 411b3b0..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppLazyRow.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.padding
-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.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Text
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun AppLazyRow(
- title: String,
- items: List<Movie>,
- modifier: Modifier = Modifier,
- drawItem: @Composable (movie: Movie, index: Int, modifier: Modifier) -> Unit
-) {
- val paddingLeft = 58.dp
- var hasFocus by remember { mutableStateOf(false) }
-
- Column(modifier = modifier.onFocusChanged { hasFocus = it.hasFocus }) {
- Text(
- text = title,
- color = if (hasFocus) Color.White else Color.White.copy(alpha = 0.8f),
- fontSize = 14.sp,
- modifier = Modifier.padding(start = paddingLeft)
- )
-
- AppSpacer(height = 12.dp)
-
- TvLazyRow(
- contentPadding = PaddingValues(horizontal = paddingLeft),
- horizontalArrangement = Arrangement.spacedBy(20.dp),
- ) {
- items.forEachIndexed { index, movie -> item { drawItem(movie, index, Modifier) } }
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppSpacer.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppSpacer.kt
deleted file mode 100644
index b912325..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppSpacer.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Dp
-
-@Composable
-fun AppSpacer(width: Dp? = null, height: Dp? = null) {
- var modifier: Modifier = Modifier
- if (width != null) {
- modifier = modifier.width(width)
- }
- if (height != null) {
- modifier = modifier.height(height)
- }
- Spacer(modifier)
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppTabRow.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppTabRow.kt
deleted file mode 100644
index c2dae79..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppTabRow.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.key
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.LocalContentColor
-import androidx.tv.material3.Tab
-import androidx.tv.material3.TabDefaults
-import androidx.tv.material3.TabRow
-import androidx.tv.material3.Text
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun AppTabRow(
- tabs: List<String>,
- selectedTabIndex: Int,
- onSelectedTabIndexChange: (Int) -> Unit,
- modifier: Modifier = Modifier
-) {
- AlignmentCenter(horizontalAxis = true) {
- TabRow(
- selectedTabIndex = selectedTabIndex,
- separator = { Spacer(modifier = Modifier.width(4.dp)) },
- modifier = modifier.padding(top = 20.dp),
- // indicator = @Composable { tabPositions ->
- // tabPositions.getOrNull(selectedTabIndex)?.let {
- // TabRowDefaults.PillIndicator(
- // currentTabPosition = it,
- // inactiveColor = Color(0xFFE5E1E6),
- // )
- // }
- // }
- ) {
- tabs.forEachIndexed { index, tabLabel ->
- key(index) {
- Tab(
- selected = selectedTabIndex == index,
- onFocus = { onSelectedTabIndexChange(index) },
- colors =
- TabDefaults.pillIndicatorTabColors(
- inactiveContentColor = LocalContentColor.current,
- // selectedContentColor =
- // Color(0xFF313033),
- ),
- modifier = Modifier,
- ) {
- Text(
- text = tabLabel,
- fontSize = 12.sp,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
- )
- }
- }
- }
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/BringIntoViewIfChildrenAreFocused.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/BringIntoViewIfChildrenAreFocused.kt
deleted file mode 100644
index 486221b..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/BringIntoViewIfChildrenAreFocused.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.relocation.BringIntoViewResponder
-import androidx.compose.foundation.relocation.bringIntoViewResponder
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.debugInspectorInfo
-
-@OptIn(ExperimentalFoundationApi::class)
-internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier =
- composed(
- inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
- factory = {
- var myRect: Rect = Rect.Zero
- this.onSizeChanged {
- myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
- }
- .bringIntoViewResponder(
- remember {
- object : BringIntoViewResponder {
- // return the current rectangle and ignoring the child rectangle
- // received.
- @ExperimentalFoundationApi
- override fun calculateRectForParent(localRect: Rect): Rect = myRect
-
- // The container is not expected to be scrollable. Hence the child is
- // already in view with respect to the container.
- @ExperimentalFoundationApi
- override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
- }
- }
- )
- }
- )
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Data.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Data.kt
deleted file mode 100644
index 422f26f..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Data.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-data class MovieImage(val url: String, val aspect: String)
-
-data class Movie(val name: String, val images: List<MovieImage>, val root: String)
-
-data class MovieCollection(val label: String, val items: List<Movie>)
-
-data class RootData(
- val data: List<MovieCollection>,
- val featuredCarouselMovies: List<String>,
- val commonDescription: String
-)
-
-var movieCollections = listOf<MovieCollection>()
-var topPicksForYou = listOf<Movie>()
-var allMovies = listOf<Movie>()
-var featuredCarouselMovies = listOf<Movie>()
-var commonDescription = ""
-
-val Movie.description: String
- get() = commonDescription
-
-fun getMovieImageUrl(movie: Movie, aspect: String = "orientation/backdrop_16x9"): String =
- movie.images.find { image -> image.aspect == aspect }?.url ?: movie.images.first().url
-
-fun initializeData(rootData: RootData) {
- commonDescription = rootData.commonDescription
- movieCollections = rootData.data
- topPicksForYou = movieCollections[3].items
- allMovies = movieCollections.flatMap { it.items }.reversed()
- featuredCarouselMovies = run {
- val titles = rootData.featuredCarouselMovies
- val previousTitles = mutableListOf<String>()
-
- movieCollections
- .flatMap { it.items }
- .filter {
- if (previousTitles.contains(it.name)) {
- false
- } else {
- previousTitles.add(it.name)
- titles.contains(it.name)
- }
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt
deleted file mode 100644
index 54919ea..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import android.util.Log
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
-
-// import coil.compose.AsyncImage
-// import com.google.gson.Gson
-
-fun getRootDataFromJson(jsonData: String): RootData {
- Log.d("LOL", "getRootDataFromJson: $jsonData")
- // return Gson().fromJson(jsonData, RootData::class.java)
- return RootData(listOf(), listOf(), "")
-}
-
-@Composable
-fun AppAsyncImage(
- imageUrl: String,
- modifier: Modifier = Modifier,
- contentScale: ContentScale = ContentScale.Fit,
- alignment: Alignment = Alignment.Center,
- contentDescription: String? = null
-) {
- Log.d(
- "LOL",
- "AppAsyncImage: $imageUrl, $modifier, $contentScale, $alignment, $contentDescription"
- )
- // AsyncImage(
- // model = imageUrl,
- // contentScale = contentScale,
- // alignment = alignment,
- // modifier = modifier,
- // contentDescription = contentDescription
- // )
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
deleted file mode 100644
index f997d1d..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.animation.AnimatedContentScope
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.outlined.ArrowForward
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.tv.material3.Carousel
-import androidx.tv.material3.CarouselDefaults
-import androidx.tv.material3.CarouselState
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Text
-import androidx.tv.material3.rememberCarouselState
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun FeaturedCarousel(movies: List<Movie> = featuredCarouselMovies, modifier: Modifier = Modifier) {
- val carouselState: CarouselState = rememberCarouselState()
- val slidesCount = movies.size
-
- Carousel(
- itemCount = slidesCount,
- carouselState = carouselState,
- modifier = modifier.height(340.dp).fillMaxWidth(),
- carouselIndicator = {
- CarouselDefaults.IndicatorRow(
- itemCount = slidesCount,
- activeItemIndex = carouselState.activeItemIndex,
- modifier = Modifier.align(Alignment.BottomEnd).padding(end = 58.dp, bottom = 16.dp),
- )
- },
- contentTransformEndToStart = fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))),
- contentTransformStartToEnd = fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000)))
- ) { itemIndex ->
- val movie = movies[itemIndex]
-
- CarouselSlide(
- title = movie.name,
- description = movie.description,
- background = { LandscapeImageBackground(movie) },
- actions = {
- @Suppress("DEPRECATION")
- AppButton(
- text = "Watch on YouTube",
- icon = Icons.AutoMirrored.Outlined.ArrowForward,
- )
- },
- )
- }
-}
-
-@OptIn(ExperimentalAnimationApi::class, ExperimentalTvMaterial3Api::class)
-@Composable
-private fun AnimatedContentScope.CarouselSlide(
- title: String,
- description: String,
- background: @Composable () -> Unit,
- actions: @Composable () -> Unit
-) {
- Box {
- background()
- Column(
- modifier =
- Modifier.padding(start = 58.dp, top = 150.dp)
- .animateEnterExit(
- enter = slideInHorizontally(animationSpec = tween(1000)) { it / 2 },
- exit = slideOutHorizontally(animationSpec = tween(1000))
- )
- ) {
- Text(text = title, color = Color.White, fontSize = 40.sp)
-
- AppSpacer(height = 16.dp)
-
- Text(
- text = description,
- color = Color.White,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- modifier = Modifier.width(500.dp),
- )
-
- AppSpacer(height = 15.dp)
-
- actions()
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ImageCard.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ImageCard.kt
deleted file mode 100644
index 45efe7f..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ImageCard.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-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.draw.clip
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Text
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun ImageCard(
- movie: Movie,
- aspectRatio: Float = 16f / 9,
- customCardWidth: Dp? = null,
- modifier: Modifier = Modifier,
-) {
- val aspect =
- if (aspectRatio == 16f / 9) "orientation/vod_art_16x9" else "orientation/vod_art_2x3"
- val scaleMax = if (aspectRatio == 16f / 9) 1.1f else 1.025f
- val cardWidth = customCardWidth ?: if (aspectRatio == 16f / 9) 200.dp else 172.dp
-
- var isFocused by remember { mutableStateOf(false) }
- val shape = RoundedCornerShape(12.dp)
- val borderColor by
- animateColorAsState(
- targetValue = if (isFocused) Color.White.copy(alpha = 0.8f) else Color.Transparent
- )
- val scale by animateFloatAsState(targetValue = if (isFocused) scaleMax else 1f)
-
- Column(modifier = Modifier.width(cardWidth).scale(scale)) {
- Box(
- modifier =
- modifier
- .fillMaxWidth()
- .aspectRatio(aspectRatio)
- .border(2.dp, borderColor, shape)
- .clip(shape)
- .onFocusChanged { isFocused = it.isFocused }
- .focusable()
- ) {
- AppAsyncImage(imageUrl = getMovieImageUrl(movie, aspect = aspect))
- }
- androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(6.dp))
- Text(
- text = movie.name,
- color = Color.White.copy(alpha = 0.8f),
- fontSize = 12.sp,
- textAlign = TextAlign.Center,
- modifier = Modifier.fillMaxWidth()
- )
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt
deleted file mode 100644
index 1e2c8cb..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.onGloballyPositioned
-
-@Composable
-fun LandscapeImageBackground(movie: Movie, aspect: String = "orientation/iconic_16x9") {
- val navigationGradient =
- Brush.verticalGradient(
- colors = listOf(pageColor, Color.Transparent),
- startY = 0f,
- endY = 200f
- )
- var height by remember { mutableStateOf(0f) }
-
- val navigationGradientBottom =
- Brush.verticalGradient(
- colors = listOf(Color.Transparent, pageColor),
- startY = 50f,
- endY = height,
- )
-
- val horizontalGradient =
- Brush.horizontalGradient(
- colors = listOf(pageColor, Color.Transparent),
- startX = 1400f,
- endX = 900f,
- )
-
- Box(
- modifier = Modifier.fillMaxSize().onGloballyPositioned { height = it.size.height.toFloat() }
- ) {
- AppAsyncImage(
- imageUrl = getMovieImageUrl(movie = movie, aspect = aspect),
- contentScale = ContentScale.FillWidth,
- alignment = Alignment.Center,
- modifier = Modifier.fillMaxWidth(),
- contentDescription = null
- )
-
- Box(
- modifier =
- Modifier.matchParentSize()
- .background(navigationGradientBottom)
- .background(navigationGradient)
- .background(horizontalGradient)
- )
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt
deleted file mode 100644
index 7e03cfd..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- // Create the "data.json" file in "presentation/src/main/assets" directory and add the
- // content from this link: go/compose-tv-presentation-app-data
- val jsonData = assets.readAssetsFile("data.json")
- val deserializedData = getRootDataFromJson(jsonData)
-
- initializeData(deserializedData)
-
- super.onCreate(savedInstanceState)
- setContent { App() }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ShowsGrid.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ShowsGrid.kt
deleted file mode 100644
index 1f69930..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ShowsGrid.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.OutlinedTextFieldDefaults
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.lazy.grid.TvGridCells
-import androidx.tv.foundation.lazy.grid.TvLazyHorizontalGrid
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Text
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun ShowsGrid(modifier: Modifier = Modifier) {
- var keyword by remember { mutableStateOf("") }
- val movies by
- remember(keyword) {
- mutableStateOf(allMovies.filter { movie -> movie.name.contains(keyword) })
- }
- Column(
- modifier = Modifier.height(520.dp).padding(top = 70.dp),
- ) {
- Box(modifier = Modifier.padding(horizontal = 58.dp).fillMaxWidth()) {
- OutlinedTextField(
- value = keyword,
- onValueChange = { keyword = it },
- placeholder = { Text(text = "Search", color = Color.White) },
- modifier = modifier.fillMaxWidth().align(Alignment.Center),
- colors =
- OutlinedTextFieldDefaults.colors(
- focusedTextColor = Color.White,
- unfocusedTextColor = Color.White,
- focusedBorderColor = Color.White,
- unfocusedBorderColor = Color.White.copy(alpha = 0.5f)
- )
- )
- }
-
- AppSpacer(height = 20.dp)
-
- if (movies.isEmpty()) {
- AppSpacer(height = 20.dp)
- Box(modifier = Modifier.fillMaxWidth()) {
- Text(
- text = "No movies matched",
- modifier = Modifier.align(Alignment.Center),
- color = Color.White,
- )
- }
- }
-
- TvLazyHorizontalGrid(
- rows = TvGridCells.Fixed(3),
- contentPadding = PaddingValues(horizontal = 58.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp),
- modifier = Modifier.fillMaxSize().bringIntoViewIfChildrenAreFocused(),
- ) {
- items(movies.size) {
- val movie = movies[it]
-
- Box(modifier = Modifier.padding(end = 30.dp)) {
- ImageCard(
- movie,
- customCardWidth = 150.dp,
- modifier = Modifier,
- )
- }
- }
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Sidebar.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Sidebar.kt
deleted file mode 100644
index da1e056..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Sidebar.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.focusGroup
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Home
-import androidx.compose.material.icons.outlined.Movie
-import androidx.compose.material.icons.outlined.Tv
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
-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.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.dp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Icon
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun Sidebar(
- selectedIndex: Int,
- onIndexChange: (index: Int) -> Unit,
-) {
- val fr = remember { FocusRequester() }
- val drawIcon: @Composable (imageVector: ImageVector, index: Int, modifier: Modifier) -> Unit =
- { imageVector, index, modifier ->
- var isFocused by remember { mutableStateOf(false) }
- val isSelected = selectedIndex == index
-
- IconButton(
- onClick = {},
- modifier =
- modifier
- .onFocusChanged {
- isFocused = it.isFocused
- if (it.isFocused) {
- onIndexChange(index)
- }
- }
- .focusRequester(if (index == 0) fr else FocusRequester()),
- colors =
- IconButtonDefaults.filledIconButtonColors(
- containerColor =
- if (isSelected && isFocused) Color.White else Color.Transparent,
- )
- ) {
- Box(modifier = Modifier) {
- Icon(
- imageVector = imageVector,
- tint = if (isSelected && isFocused) pageColor else Color.White,
- contentDescription = null,
- )
- if (isSelected) {
- Box(
- modifier =
- Modifier.width(10.dp)
- .height(3.dp)
- .offset(y = 4.dp)
- .align(Alignment.BottomCenter)
- .background(Color.Red)
- )
- }
- }
- }
- }
-
- Column(
- modifier = Modifier.width(60.dp).fillMaxHeight().background(pageColor).focusGroup(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- ) {
- key(0) {
- drawIcon(
- Icons.Outlined.Home,
- 0,
- Modifier,
- )
- }
- key(1) {
- drawIcon(
- Icons.Outlined.Movie,
- 1,
- Modifier,
- )
- }
- key(2) {
- drawIcon(
- Icons.Outlined.Tv,
- 2,
- Modifier,
- )
- }
- }
-}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ifElse.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ifElse.kt
deleted file mode 100644
index 826d18b..0000000
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ifElse.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.integration.presentation
-
-import androidx.compose.ui.Modifier
-
-/** Thanks, Plex 🦄 :) */
-fun Modifier.ifElse(
- condition: () -> Boolean,
- ifTrueModifier: Modifier,
- ifFalseModifier: Modifier = Modifier
-): Modifier = then(if (condition()) ifTrueModifier else ifFalseModifier)
-
-fun Modifier.ifElse(
- condition: Boolean,
- ifTrueModifier: Modifier,
- ifFalseModifier: Modifier = Modifier
-): Modifier = ifElse({ condition }, ifTrueModifier, ifFalseModifier)
diff --git a/tv/integration-tests/presentation/src/main/res/drawable/app_icon_your_company.png b/tv/integration-tests/presentation/src/main/res/drawable/app_icon_your_company.png
deleted file mode 100644
index 0a47b01..0000000
--- a/tv/integration-tests/presentation/src/main/res/drawable/app_icon_your_company.png
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/presentation/src/main/res/layout/activity_main.xml b/tv/integration-tests/presentation/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 2a1b45b..0000000
--- a/tv/integration-tests/presentation/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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.
- -->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/main_browse_fragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".MainActivity"
- tools:deviceIds="tv"
- tools:ignore="MergeRootFrame" />
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/res/mipmap-hdpi/ic_launcher.webp b/tv/integration-tests/presentation/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
--- a/tv/integration-tests/presentation/src/main/res/mipmap-hdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/presentation/src/main/res/mipmap-mdpi/ic_launcher.webp b/tv/integration-tests/presentation/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
--- a/tv/integration-tests/presentation/src/main/res/mipmap-mdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/presentation/src/main/res/mipmap-xhdpi/ic_launcher.webp b/tv/integration-tests/presentation/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
--- a/tv/integration-tests/presentation/src/main/res/mipmap-xhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/tv/integration-tests/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
--- a/tv/integration-tests/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/tv/integration-tests/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
--- a/tv/integration-tests/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/tv/integration-tests/presentation/src/main/res/values/colors.xml b/tv/integration-tests/presentation/src/main/res/values/colors.xml
deleted file mode 100644
index 733f3f5..0000000
--- a/tv/integration-tests/presentation/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<resources>
- <color name="background_gradient_start">#000000</color>
- <color name="background_gradient_end">#DDDDDD</color>
- <color name="fastlane_background">#0096a6</color>
- <color name="search_opaque">#ffaa3f</color>
- <color name="selected_background">#ffaa3f</color>
- <color name="default_background">#3d3d3d</color>
-</resources>
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/res/values/strings.xml b/tv/integration-tests/presentation/src/main/res/values/strings.xml
deleted file mode 100644
index 38b8b29..0000000
--- a/tv/integration-tests/presentation/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
- Copyright 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources>
- <string name="app_name">Presentation app</string>
- <string name="browse_title">Presentation app for visitors</string>
- <string name="related_movies">Related Videos</string>
- <string name="grid_view">Grid View</string>
- <string name="error_fragment">Error Fragment</string>
- <string name="personal_settings">Personal Settings</string>
- <string name="watch_trailer_1">Watch trailer</string>
- <string name="watch_trailer_2">FREE</string>
- <string name="rent_1">Rent By Day</string>
- <string name="rent_2">From $1.99</string>
- <string name="buy_1">Buy and Own</string>
- <string name="buy_2">AT $9.99</string>
- <string name="movie">Movie</string>
-
- <!-- Error messages -->
- <string name="error_fragment_message">An error occurred</string>
- <string name="dismiss_error">Dismiss</string>
-</resources>
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/res/values/themes.xml b/tv/integration-tests/presentation/src/main/res/values/themes.xml
deleted file mode 100644
index 8df2c77..0000000
--- a/tv/integration-tests/presentation/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<!--
- Copyright 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources>
- <style name="Theme.Androidx" parent="@style/Theme.AppCompat" />
-</resources>
\ No newline at end of file
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index 936d277..1cbaebd 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -4,254 +4,6 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvFoundationApi {
}
- @Deprecated @androidx.compose.runtime.Immutable public final class PivotOffsets {
- ctor @Deprecated public PivotOffsets();
- ctor @Deprecated public PivotOffsets(optional @FloatRange(from=0.0, to=1.0, fromInclusive=true, toInclusive=true) float parentFraction, optional @FloatRange(from=0.0, to=1.0, fromInclusive=true, toInclusive=true) float childFraction);
- method @Deprecated public float getChildFraction();
- method @Deprecated public float getParentFraction();
- property @Deprecated public final float childFraction;
- property @Deprecated public final float parentFraction;
- }
-
- public final class ScrollableWithPivotKt {
- method @Deprecated @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.Modifier scrollableWithPivot(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
- }
-
-}
-
-package androidx.tv.foundation.lazy.grid {
-
- public final class LazyGridDslKt {
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T[] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T[] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- }
-
- public final class LazyGridSpanKt {
- method @Deprecated public static long TvGridItemSpan(int currentLineSpan);
- }
-
- @Deprecated @androidx.compose.runtime.Stable public interface TvGridCells {
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
- ctor @Deprecated public TvGridCells.Adaptive(float minSize);
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
- ctor @Deprecated public TvGridCells.Fixed(int count);
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated public static final class TvGridCells.FixedSize implements androidx.tv.foundation.lazy.grid.TvGridCells {
- ctor @Deprecated public TvGridCells.FixedSize(float size);
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
- method @Deprecated public int getCurrentLineSpan();
- property @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final int currentLineSpan;
- }
-
- @Deprecated public sealed interface TvLazyGridItemInfo {
- method @Deprecated public int getColumn();
- method @Deprecated public Object? getContentType();
- method @Deprecated public int getIndex();
- method @Deprecated public Object getKey();
- method @Deprecated public long getOffset();
- method @Deprecated public int getRow();
- method @Deprecated public long getSize();
- property @Deprecated public abstract int column;
- property @Deprecated public abstract Object? contentType;
- property @Deprecated public abstract int index;
- property @Deprecated public abstract Object key;
- property @Deprecated public abstract long offset;
- property @Deprecated public abstract int row;
- property @Deprecated public abstract long size;
- field @Deprecated public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
- field @Deprecated public static final int UnknownColumn = -1; // 0xffffffff
- field @Deprecated public static final int UnknownRow = -1; // 0xffffffff
- }
-
- @Deprecated public static final class TvLazyGridItemInfo.Companion {
- field @Deprecated public static final int UnknownColumn = -1; // 0xffffffff
- field @Deprecated public static final int UnknownRow = -1; // 0xffffffff
- }
-
- @Deprecated @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
- method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
- }
-
- @Deprecated @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
- method @Deprecated public int getMaxCurrentLineSpan();
- method @Deprecated public int getMaxLineSpan();
- property @Deprecated public abstract int maxCurrentLineSpan;
- property @Deprecated public abstract int maxLineSpan;
- }
-
- @Deprecated public sealed interface TvLazyGridLayoutInfo {
- method @Deprecated public int getAfterContentPadding();
- method @Deprecated public int getBeforeContentPadding();
- method @Deprecated public int getMainAxisItemSpacing();
- method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
- method @Deprecated public boolean getReverseLayout();
- method @Deprecated public int getTotalItemsCount();
- method @Deprecated public int getViewportEndOffset();
- method @Deprecated public long getViewportSize();
- method @Deprecated public int getViewportStartOffset();
- method @Deprecated public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
- property @Deprecated public abstract int afterContentPadding;
- property @Deprecated public abstract int beforeContentPadding;
- property @Deprecated public abstract int mainAxisItemSpacing;
- property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property @Deprecated public abstract boolean reverseLayout;
- property @Deprecated public abstract int totalItemsCount;
- property @Deprecated public abstract int viewportEndOffset;
- property @Deprecated public abstract long viewportSize;
- property @Deprecated public abstract int viewportStartOffset;
- property @Deprecated public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
- }
-
- @Deprecated @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
- method @Deprecated public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
- method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
- }
-
- @Deprecated @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
- }
-
- @Deprecated @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor @Deprecated public TvLazyGridState();
- ctor @Deprecated public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
- method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public float dispatchRawDelta(float delta);
- method @Deprecated public int getFirstVisibleItemIndex();
- method @Deprecated public int getFirstVisibleItemScrollOffset();
- method @Deprecated public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
- method @Deprecated public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
- method @Deprecated public boolean isScrollInProgress();
- method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property @Deprecated public boolean canScrollBackward;
- property @Deprecated public boolean canScrollForward;
- property @Deprecated public final int firstVisibleItemIndex;
- property @Deprecated public final int firstVisibleItemScrollOffset;
- property @Deprecated public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
- property @Deprecated public boolean isScrollInProgress;
- property @Deprecated public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
- field @Deprecated public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
- }
-
- @Deprecated public static final class TvLazyGridState.Companion {
- method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,? extends java.lang.Object?> getSaver();
- property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,? extends java.lang.Object?> Saver;
- }
-
- public final class TvLazyGridStateKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
- }
-
-}
-
-package androidx.tv.foundation.lazy.list {
-
- public final class LazyDslKt {
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T[] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T[] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- }
-
- public final class LazyListStateKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
- }
-
- @Deprecated public sealed interface TvLazyListItemInfo {
- method @Deprecated public Object? getContentType();
- method @Deprecated public int getIndex();
- method @Deprecated public Object getKey();
- method @Deprecated public int getOffset();
- method @Deprecated public int getSize();
- property @Deprecated public abstract Object? contentType;
- property @Deprecated public abstract int index;
- property @Deprecated public abstract Object key;
- property @Deprecated public abstract int offset;
- property @Deprecated public abstract int size;
- }
-
- @Deprecated @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
- method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
- method @Deprecated public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
- method @Deprecated public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
- method @Deprecated public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
- }
-
- @Deprecated public sealed interface TvLazyListLayoutInfo {
- method @Deprecated public int getAfterContentPadding();
- method @Deprecated public int getBeforeContentPadding();
- method @Deprecated public int getMainAxisItemSpacing();
- method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
- method @Deprecated public boolean getReverseLayout();
- method @Deprecated public int getTotalItemsCount();
- method @Deprecated public int getViewportEndOffset();
- method @Deprecated public long getViewportSize();
- method @Deprecated public int getViewportStartOffset();
- method @Deprecated public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
- property @Deprecated public abstract int afterContentPadding;
- property @Deprecated public abstract int beforeContentPadding;
- property @Deprecated public abstract int mainAxisItemSpacing;
- property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property @Deprecated public abstract boolean reverseLayout;
- property @Deprecated public abstract int totalItemsCount;
- property @Deprecated public abstract int viewportEndOffset;
- property @Deprecated public abstract long viewportSize;
- property @Deprecated public abstract int viewportStartOffset;
- property @Deprecated public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
- }
-
- @Deprecated @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
- method @Deprecated public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
- method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
- method @Deprecated @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
- }
-
- @Deprecated @kotlin.DslMarker public @interface TvLazyListScopeMarker {
- }
-
- @Deprecated @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor @Deprecated public TvLazyListState();
- ctor @Deprecated public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
- method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public float dispatchRawDelta(float delta);
- method @Deprecated public int getFirstVisibleItemIndex();
- method @Deprecated public int getFirstVisibleItemScrollOffset();
- method @Deprecated public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
- method @Deprecated public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
- method @Deprecated public boolean isScrollInProgress();
- method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property @Deprecated public boolean canScrollBackward;
- property @Deprecated public boolean canScrollForward;
- property @Deprecated public final int firstVisibleItemIndex;
- property @Deprecated public final int firstVisibleItemScrollOffset;
- property @Deprecated public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
- property @Deprecated public boolean isScrollInProgress;
- property @Deprecated public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
- field @Deprecated public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
- }
-
- @Deprecated public static final class TvLazyListState.Companion {
- method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,? extends java.lang.Object?> getSaver();
- property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,? extends java.lang.Object?> Saver;
- }
-
}
package androidx.tv.foundation.text {
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index 936d277..1cbaebd 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -4,254 +4,6 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvFoundationApi {
}
- @Deprecated @androidx.compose.runtime.Immutable public final class PivotOffsets {
- ctor @Deprecated public PivotOffsets();
- ctor @Deprecated public PivotOffsets(optional @FloatRange(from=0.0, to=1.0, fromInclusive=true, toInclusive=true) float parentFraction, optional @FloatRange(from=0.0, to=1.0, fromInclusive=true, toInclusive=true) float childFraction);
- method @Deprecated public float getChildFraction();
- method @Deprecated public float getParentFraction();
- property @Deprecated public final float childFraction;
- property @Deprecated public final float parentFraction;
- }
-
- public final class ScrollableWithPivotKt {
- method @Deprecated @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.Modifier scrollableWithPivot(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
- }
-
-}
-
-package androidx.tv.foundation.lazy.grid {
-
- public final class LazyGridDslKt {
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T[] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T[] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- }
-
- public final class LazyGridSpanKt {
- method @Deprecated public static long TvGridItemSpan(int currentLineSpan);
- }
-
- @Deprecated @androidx.compose.runtime.Stable public interface TvGridCells {
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
- ctor @Deprecated public TvGridCells.Adaptive(float minSize);
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
- ctor @Deprecated public TvGridCells.Fixed(int count);
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated public static final class TvGridCells.FixedSize implements androidx.tv.foundation.lazy.grid.TvGridCells {
- ctor @Deprecated public TvGridCells.FixedSize(float size);
- method @Deprecated public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
- }
-
- @Deprecated @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
- method @Deprecated public int getCurrentLineSpan();
- property @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final int currentLineSpan;
- }
-
- @Deprecated public sealed interface TvLazyGridItemInfo {
- method @Deprecated public int getColumn();
- method @Deprecated public Object? getContentType();
- method @Deprecated public int getIndex();
- method @Deprecated public Object getKey();
- method @Deprecated public long getOffset();
- method @Deprecated public int getRow();
- method @Deprecated public long getSize();
- property @Deprecated public abstract int column;
- property @Deprecated public abstract Object? contentType;
- property @Deprecated public abstract int index;
- property @Deprecated public abstract Object key;
- property @Deprecated public abstract long offset;
- property @Deprecated public abstract int row;
- property @Deprecated public abstract long size;
- field @Deprecated public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
- field @Deprecated public static final int UnknownColumn = -1; // 0xffffffff
- field @Deprecated public static final int UnknownRow = -1; // 0xffffffff
- }
-
- @Deprecated public static final class TvLazyGridItemInfo.Companion {
- field @Deprecated public static final int UnknownColumn = -1; // 0xffffffff
- field @Deprecated public static final int UnknownRow = -1; // 0xffffffff
- }
-
- @Deprecated @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
- method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
- }
-
- @Deprecated @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
- method @Deprecated public int getMaxCurrentLineSpan();
- method @Deprecated public int getMaxLineSpan();
- property @Deprecated public abstract int maxCurrentLineSpan;
- property @Deprecated public abstract int maxLineSpan;
- }
-
- @Deprecated public sealed interface TvLazyGridLayoutInfo {
- method @Deprecated public int getAfterContentPadding();
- method @Deprecated public int getBeforeContentPadding();
- method @Deprecated public int getMainAxisItemSpacing();
- method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
- method @Deprecated public boolean getReverseLayout();
- method @Deprecated public int getTotalItemsCount();
- method @Deprecated public int getViewportEndOffset();
- method @Deprecated public long getViewportSize();
- method @Deprecated public int getViewportStartOffset();
- method @Deprecated public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
- property @Deprecated public abstract int afterContentPadding;
- property @Deprecated public abstract int beforeContentPadding;
- property @Deprecated public abstract int mainAxisItemSpacing;
- property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property @Deprecated public abstract boolean reverseLayout;
- property @Deprecated public abstract int totalItemsCount;
- property @Deprecated public abstract int viewportEndOffset;
- property @Deprecated public abstract long viewportSize;
- property @Deprecated public abstract int viewportStartOffset;
- property @Deprecated public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
- }
-
- @Deprecated @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
- method @Deprecated public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
- method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
- }
-
- @Deprecated @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
- }
-
- @Deprecated @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor @Deprecated public TvLazyGridState();
- ctor @Deprecated public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
- method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public float dispatchRawDelta(float delta);
- method @Deprecated public int getFirstVisibleItemIndex();
- method @Deprecated public int getFirstVisibleItemScrollOffset();
- method @Deprecated public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
- method @Deprecated public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
- method @Deprecated public boolean isScrollInProgress();
- method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property @Deprecated public boolean canScrollBackward;
- property @Deprecated public boolean canScrollForward;
- property @Deprecated public final int firstVisibleItemIndex;
- property @Deprecated public final int firstVisibleItemScrollOffset;
- property @Deprecated public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
- property @Deprecated public boolean isScrollInProgress;
- property @Deprecated public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
- field @Deprecated public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
- }
-
- @Deprecated public static final class TvLazyGridState.Companion {
- method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,? extends java.lang.Object?> getSaver();
- property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,? extends java.lang.Object?> Saver;
- }
-
- public final class TvLazyGridStateKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
- }
-
-}
-
-package androidx.tv.foundation.lazy.list {
-
- public final class LazyDslKt {
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
- method @Deprecated @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T[] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @Deprecated public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T[] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- }
-
- public final class LazyListStateKt {
- method @Deprecated @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
- }
-
- @Deprecated public sealed interface TvLazyListItemInfo {
- method @Deprecated public Object? getContentType();
- method @Deprecated public int getIndex();
- method @Deprecated public Object getKey();
- method @Deprecated public int getOffset();
- method @Deprecated public int getSize();
- property @Deprecated public abstract Object? contentType;
- property @Deprecated public abstract int index;
- property @Deprecated public abstract Object key;
- property @Deprecated public abstract int offset;
- property @Deprecated public abstract int size;
- }
-
- @Deprecated @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
- method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
- method @Deprecated public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
- method @Deprecated public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
- method @Deprecated public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
- }
-
- @Deprecated public sealed interface TvLazyListLayoutInfo {
- method @Deprecated public int getAfterContentPadding();
- method @Deprecated public int getBeforeContentPadding();
- method @Deprecated public int getMainAxisItemSpacing();
- method @Deprecated public androidx.compose.foundation.gestures.Orientation getOrientation();
- method @Deprecated public boolean getReverseLayout();
- method @Deprecated public int getTotalItemsCount();
- method @Deprecated public int getViewportEndOffset();
- method @Deprecated public long getViewportSize();
- method @Deprecated public int getViewportStartOffset();
- method @Deprecated public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
- property @Deprecated public abstract int afterContentPadding;
- property @Deprecated public abstract int beforeContentPadding;
- property @Deprecated public abstract int mainAxisItemSpacing;
- property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property @Deprecated public abstract boolean reverseLayout;
- property @Deprecated public abstract int totalItemsCount;
- property @Deprecated public abstract int viewportEndOffset;
- property @Deprecated public abstract long viewportSize;
- property @Deprecated public abstract int viewportStartOffset;
- property @Deprecated public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
- }
-
- @Deprecated @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
- method @Deprecated public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
- method @Deprecated public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
- method @Deprecated @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
- }
-
- @Deprecated @kotlin.DslMarker public @interface TvLazyListScopeMarker {
- }
-
- @Deprecated @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
- ctor @Deprecated public TvLazyListState();
- ctor @Deprecated public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
- method @Deprecated public suspend Object? animateScrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public float dispatchRawDelta(float delta);
- method @Deprecated public int getFirstVisibleItemIndex();
- method @Deprecated public int getFirstVisibleItemScrollOffset();
- method @Deprecated public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
- method @Deprecated public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
- method @Deprecated public boolean isScrollInProgress();
- method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property @Deprecated public boolean canScrollBackward;
- property @Deprecated public boolean canScrollForward;
- property @Deprecated public final int firstVisibleItemIndex;
- property @Deprecated public final int firstVisibleItemScrollOffset;
- property @Deprecated public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
- property @Deprecated public boolean isScrollInProgress;
- property @Deprecated public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
- field @Deprecated public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
- }
-
- @Deprecated public static final class TvLazyListState.Companion {
- method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,? extends java.lang.Object?> getSaver();
- property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,? extends java.lang.Object?> Saver;
- }
-
}
package androidx.tv.foundation.text {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/AutoTestFrameClock.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/AutoTestFrameClock.kt
deleted file mode 100644
index 11dec8c..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/AutoTestFrameClock.kt
+++ /dev/null
@@ -1,28 +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.tv.foundation.lazy
-
-import androidx.compose.runtime.MonotonicFrameClock
-import java.util.concurrent.atomic.AtomicLong
-
-class AutoTestFrameClock : MonotonicFrameClock {
- private val time = AtomicLong(0)
-
- override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
- return onFrame(time.getAndAdd(16_000_000))
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
deleted file mode 100644
index 892e8a6..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
+++ /dev/null
@@ -1,251 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.snap
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.testutils.assertIsEqualTo
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-
-open class BaseLazyGridTestWithOrientation(private val orientation: Orientation) {
-
- @get:Rule val rule = createComposeRule()
-
- val vertical: Boolean
- get() = orientation == Orientation.Vertical
-
- @Stable
- fun Modifier.crossAxisSize(size: Dp) =
- if (vertical) {
- this.width(size)
- } else {
- this.height(size)
- }
-
- @Stable
- fun Modifier.mainAxisSize(size: Dp) =
- if (vertical) {
- this.height(size)
- } else {
- this.width(size)
- }
-
- @Stable
- fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
- if (vertical) {
- this.size(crossAxis, mainAxis)
- } else {
- this.size(mainAxis, crossAxis)
- }
-
- fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertHeightIsEqualTo(expectedSize)
- } else {
- assertWidthIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertWidthIsEqualTo(expectedSize)
- } else {
- assertHeightIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
- val position =
- if (vertical) {
- getUnclippedBoundsInRoot().top
- } else {
- getUnclippedBoundsInRoot().left
- }
- position.assertIsEqualTo(expected, tolerance = 1.dp)
- }
-
- fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertTopPositionInRootIsEqualTo(expectedStart)
- } else {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- } else {
- assertTopPositionInRootIsEqualTo(expectedStart)
- }
-
- fun PaddingValues(mainAxis: Dp = 0.dp, crossAxis: Dp = 0.dp) =
- PaddingValues(
- beforeContent = mainAxis,
- afterContent = mainAxis,
- beforeContentCrossAxis = crossAxis,
- afterContentCrossAxis = crossAxis
- )
-
- fun PaddingValues(
- beforeContent: Dp = 0.dp,
- afterContent: Dp = 0.dp,
- beforeContentCrossAxis: Dp = 0.dp,
- afterContentCrossAxis: Dp = 0.dp,
- ) =
- if (vertical) {
- PaddingValues(
- start = beforeContentCrossAxis,
- top = beforeContent,
- end = afterContentCrossAxis,
- bottom = afterContent
- )
- } else {
- PaddingValues(
- start = beforeContent,
- top = beforeContentCrossAxis,
- end = afterContent,
- bottom = afterContentCrossAxis
- )
- }
-
- fun TvLazyGridState.scrollBy(offset: Dp) {
- runBlocking(Dispatchers.Main) {
- animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
- }
- }
-
- fun TvLazyGridState.scrollTo(index: Int) {
- runBlocking(Dispatchers.Main) { scrollToItem(index) }
- }
-
- fun ComposeContentTestRule.keyPress(numberOfPresses: Int = 1) {
- rule.keyPress(
- if (vertical) NativeKeyEvent.KEYCODE_DPAD_DOWN else NativeKeyEvent.KEYCODE_DPAD_RIGHT,
- numberOfPresses
- )
- }
-
- @Composable
- fun LazyGrid(
- cells: Int,
- modifier: Modifier = Modifier,
- state: TvLazyGridState = rememberTvLazyGridState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- userScrollEnabled: Boolean = true,
- crossAxisSpacedBy: Dp = 0.dp,
- mainAxisSpacedBy: Dp = 0.dp,
- content: TvLazyGridScope.() -> Unit
- ) =
- LazyGrid(
- TvGridCells.Fixed(cells),
- modifier,
- state,
- contentPadding,
- reverseLayout,
- userScrollEnabled,
- crossAxisSpacedBy,
- mainAxisSpacedBy,
- content
- )
-
- @Composable
- fun LazyGrid(
- cells: TvGridCells,
- modifier: Modifier = Modifier,
- state: TvLazyGridState = rememberTvLazyGridState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- userScrollEnabled: Boolean = true,
- crossAxisSpacedBy: Dp = 0.dp,
- mainAxisSpacedBy: Dp = 0.dp,
- content: TvLazyGridScope.() -> Unit
- ) {
- if (vertical) {
- val verticalArrangement =
- when {
- mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
- !reverseLayout -> Arrangement.Top
- else -> Arrangement.Bottom
- }
- val horizontalArrangement =
- when {
- crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
- else -> Arrangement.Start
- }
- TvLazyVerticalGrid(
- columns = cells,
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- verticalArrangement = verticalArrangement,
- horizontalArrangement = horizontalArrangement,
- pivotOffsets = PivotOffsets(parentFraction = 0f),
- content = content
- )
- } else {
- val horizontalArrangement =
- when {
- mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
- !reverseLayout -> Arrangement.Start
- else -> Arrangement.End
- }
- val verticalArrangement =
- when {
- crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
- else -> Arrangement.Top
- }
- TvLazyHorizontalGrid(
- rows = cells,
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- horizontalArrangement = horizontalArrangement,
- verticalArrangement = verticalArrangement,
- pivotOffsets = PivotOffsets(parentFraction = 0f),
- content = content
- )
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyArrangementsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyArrangementsTest.kt
deleted file mode 100644
index 10496e3..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyArrangementsTest.kt
+++ /dev/null
@@ -1,650 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-class LazyArrangementsTest {
-
- private val ContainerTag = "ContainerTag"
-
- @get:Rule val rule = createComposeRule()
-
- private var itemSize: Dp = Dp.Infinity
- private var smallerItemSize: Dp = Dp.Infinity
- private var containerSize: Dp = Dp.Infinity
-
- @Before
- fun before() {
- with(rule.density) { itemSize = 50.toDp() }
- with(rule.density) { smallerItemSize = 40.toDp() }
- containerSize = itemSize * 5
- }
-
- // cases when we have not enough items to fill min constraints:
-
- @Test
- fun vertical_defaultArrangementIsTop() {
- rule.setContent {
- TvLazyVerticalGrid(
- modifier = Modifier.requiredSize(containerSize),
- columns = TvGridCells.Fixed(1)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize).testTag(it.toString())) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Top)
- }
-
- @Test
- fun vertical_centerArrangement() {
- composeVerticalGridWith(Arrangement.Center)
- assertArrangementForTwoItems(Arrangement.Center)
- }
-
- @Test
- fun vertical_bottomArrangement() {
- composeVerticalGridWith(Arrangement.Bottom)
- assertArrangementForTwoItems(Arrangement.Bottom)
- }
-
- @Test
- fun vertical_spacedArrangementNotFillingViewport() {
- val arrangement = Arrangement.spacedBy(10.dp)
- composeVerticalGridWith(arrangement)
- assertArrangementForTwoItems(arrangement)
- }
-
- @Test
- fun horizontal_defaultArrangementIsStart() {
- rule.setContent {
- TvLazyHorizontalGrid(
- modifier = Modifier.requiredSize(containerSize),
- rows = TvGridCells.Fixed(1)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize).testTag(it.toString())) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
- }
-
- @Test
- fun horizontal_centerArrangement() {
- composeHorizontalWith(Arrangement.Center, LayoutDirection.Ltr)
- assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
- }
-
- @Test
- fun horizontal_endArrangement() {
- composeHorizontalWith(Arrangement.End, LayoutDirection.Ltr)
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
- }
-
- @Test
- fun horizontal_spacedArrangementNotFillingViewport() {
- val arrangement = Arrangement.spacedBy(10.dp)
- composeHorizontalWith(arrangement, LayoutDirection.Ltr)
- assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
- }
-
- @Test
- fun horizontal_rtl_startArrangement() {
- composeHorizontalWith(Arrangement.Center, LayoutDirection.Rtl)
- assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
- }
-
- @Test
- fun horizontal_rtl_endArrangement() {
- composeHorizontalWith(Arrangement.End, LayoutDirection.Rtl)
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
- }
-
- @Test
- fun horizontal_rtl_spacedArrangementNotFillingViewport() {
- val arrangement = Arrangement.spacedBy(10.dp)
- composeHorizontalWith(arrangement, LayoutDirection.Rtl)
- assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
- }
-
- // wrap content and spacing
-
- @Test
- fun vertical_spacing_affects_wrap_content() {
- rule.setContent {
- TvLazyVerticalGrid(
- verticalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.width(itemSize).testTag(ContainerTag),
- columns = TvGridCells.Fixed(1)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize)) }
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertWidthIsEqualTo(itemSize)
- .assertHeightIsEqualTo(itemSize * 3)
- }
-
- @Test
- fun horizontal_spacing_affects_wrap_content() {
- rule.setContent {
- TvLazyHorizontalGrid(
- horizontalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.height(itemSize).testTag(ContainerTag),
- rows = TvGridCells.Fixed(1)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize)) }
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertWidthIsEqualTo(itemSize * 3)
- .assertHeightIsEqualTo(itemSize)
- }
-
- // spacing added when we have enough items to fill the viewport
-
- @Test
- fun vertical_spacing_scrolledToTheTop() {
- rule.setContent {
- TvLazyVerticalGrid(
- verticalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f),
- columns = TvGridCells.Fixed(1)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString())) }
- }
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize * 2)
- }
-
- @Test
- fun vertical_spacing_scrolledToTheBottom() {
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- verticalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
- columns = TvGridCells.Fixed(1),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
- }
-
- @Test
- fun horizontal_spacing_scrolledToTheStart() {
- rule.setContent {
- TvLazyHorizontalGrid(
- horizontalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f),
- rows = TvGridCells.Fixed(1)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString())) }
- }
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize * 2)
- }
-
- @Test
- fun horizontal_spacing_scrolledToTheEnd() {
- rule.setContentWithTestViewConfiguration {
- TvLazyHorizontalGrid(
- horizontalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
- rows = TvGridCells.Fixed(1),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
-
- rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
- }
-
- @Test
- fun vertical_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- modifier = Modifier.size(itemSize * 3),
- state = rememberTvLazyGridState().also { state = it },
- verticalArrangement = Arrangement.spacedBy(spacingSize),
- columns = TvGridCells.Fixed(1)
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollBy((itemSizePx + spacingSizePx).toFloat()) } }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun vertical_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- modifier = Modifier.size(itemSize * 3),
- state = rememberTvLazyGridState().also { state = it },
- verticalArrangement = Arrangement.spacedBy(spacingSize),
- columns = TvGridCells.Fixed(1)
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat()) }
- }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(itemSizePx + spacingSizePx / 2)
- }
- }
-
- @Test
- fun horizontal_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyHorizontalGrid(
- TvGridCells.Fixed(1),
- Modifier.size(itemSize * 3),
- state = rememberTvLazyGridState().also { state = it },
- horizontalArrangement = Arrangement.spacedBy(spacingSize)
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollBy((itemSizePx + spacingSizePx).toFloat()) } }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun horizontal_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyHorizontalGrid(
- TvGridCells.Fixed(1),
- Modifier.size(itemSize * 3),
- state = rememberTvLazyGridState().also { state = it },
- horizontalArrangement = Arrangement.spacedBy(spacingSize)
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat()) }
- }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(itemSizePx + spacingSizePx / 2)
- }
- }
-
- // with reverseLayout == true
-
- @Test
- fun vertical_defaultArrangementIsBottomWithReverseLayout() {
- rule.setContent {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- reverseLayout = true,
- modifier = Modifier.size(containerSize)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
- }
-
- @Test
- fun horizontal_defaultArrangementIsEndWithReverseLayout() {
- rule.setContent {
- TvLazyHorizontalGrid(
- TvGridCells.Fixed(1),
- reverseLayout = true,
- modifier = Modifier.requiredSize(containerSize)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr, reverseLayout = true)
- }
-
- @Test
- fun vertical_whenArrangementChanges() {
- var arrangement by mutableStateOf(Arrangement.Top)
- rule.setContent {
- TvLazyVerticalGrid(
- modifier = Modifier.requiredSize(containerSize),
- verticalArrangement = arrangement,
- columns = TvGridCells.Fixed(1)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Top)
-
- rule.runOnIdle { arrangement = Arrangement.Bottom }
-
- assertArrangementForTwoItems(Arrangement.Bottom)
- }
-
- @Test
- fun horizontal_whenArrangementChanges() {
- var arrangement by mutableStateOf(Arrangement.Start)
- rule.setContent {
- TvLazyHorizontalGrid(
- TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(containerSize),
- horizontalArrangement = arrangement
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
-
- rule.runOnIdle { arrangement = Arrangement.End }
-
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
- }
-
- @Test
- fun vetical_negativeSpacing_itemsVisible() {
- val state = TvLazyGridState()
- val halfItemSize = itemSize / 2
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(itemSize),
- verticalArrangement = Arrangement.spacedBy(-halfItemSize),
- state = state
- ) {
- items(100) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(halfItemSize)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-
- runBlocking { state.scrollBy(with(rule.density) { halfItemSize.toPx() }) }
-
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(-halfItemSize)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun horizontal_negativeSpacing_itemsVisible() {
- val state = TvLazyGridState()
- val halfItemSize = itemSize / 2
- rule.setContent {
- TvLazyHorizontalGrid(
- rows = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(itemSize),
- horizontalArrangement = Arrangement.spacedBy(-halfItemSize),
- state = state
- ) {
- items(100) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(halfItemSize)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-
- runBlocking { state.scrollBy(with(rule.density) { halfItemSize.toPx() }) }
-
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(-halfItemSize)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun vertical_negativeSpacingLargerThanItem_itemsVisible() {
- val state = TvLazyGridState(firstVisibleItemIndex = 2)
- val largerThanItemSize = itemSize * 1.5f
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(2),
- modifier = Modifier.requiredSize(width = itemSize * 2, height = itemSize),
- verticalArrangement = Arrangement.spacedBy(-largerThanItemSize),
- state = state
- ) {
- items(8) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- repeat(8) { rule.onNodeWithTag("$it").assertTopPositionInRootIsEqualTo(0.dp) }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun horizontal_negativeSpacingLargerThanItem_itemsVisible() {
- val state = TvLazyGridState(firstVisibleItemIndex = 2)
- val largerThanItemSize = itemSize * 1.5f
- rule.setContent {
- TvLazyHorizontalGrid(
- rows = TvGridCells.Fixed(2),
- modifier = Modifier.requiredSize(width = itemSize, height = itemSize * 2),
- horizontalArrangement = Arrangement.spacedBy(-largerThanItemSize),
- state = state
- ) {
- items(8) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- repeat(8) { rule.onNodeWithTag("$it").assertLeftPositionInRootIsEqualTo(0.dp) }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- fun composeVerticalGridWith(arrangement: Arrangement.Vertical) {
- rule.setContent {
- TvLazyVerticalGrid(
- verticalArrangement = arrangement,
- modifier = Modifier.requiredSize(containerSize),
- columns = TvGridCells.Fixed(1)
- ) {
- items(2) { Item(it) }
- }
- }
- }
-
- fun composeHorizontalWith(
- arrangement: Arrangement.Horizontal,
- layoutDirection: LayoutDirection
- ) {
- rule.setContent {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- TvLazyHorizontalGrid(
- horizontalArrangement = arrangement,
- modifier = Modifier.requiredSize(containerSize),
- rows = TvGridCells.Fixed(1)
- ) {
- items(2) { Item(it) }
- }
- }
- }
- }
-
- @Composable
- fun Item(index: Int) {
- require(index < 2)
- val size = if (index == 0) itemSize else smallerItemSize
- Box(Modifier.requiredSize(size).testTag(index.toString()))
- }
-
- fun assertArrangementForTwoItems(
- arrangement: Arrangement.Vertical,
- reverseLayout: Boolean = false
- ) {
- with(rule.density) {
- val sizes =
- IntArray(2) {
- val index = if (reverseLayout) if (it == 0) 1 else 0 else it
- if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
- }
- val outPositions = IntArray(2) { 0 }
- with(arrangement) { arrange(containerSize.roundToPx(), sizes, outPositions) }
-
- outPositions.forEachIndexed { index, position ->
- val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
- rule.onNodeWithTag("$realIndex").assertTopPositionInRootIsEqualTo(position.toDp())
- }
- }
- }
-
- fun assertArrangementForTwoItems(
- arrangement: Arrangement.Horizontal,
- layoutDirection: LayoutDirection,
- reverseLayout: Boolean = false
- ) {
- with(rule.density) {
- val sizes =
- IntArray(2) {
- val index = if (reverseLayout) if (it == 0) 1 else 0 else it
- if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
- }
- val outPositions = IntArray(2) { 0 }
- with(arrangement) {
- arrange(containerSize.roundToPx(), sizes, layoutDirection, outPositions)
- }
-
- outPositions.forEachIndexed { index, position ->
- val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
- val expectedPosition = position.toDp()
- rule.onNodeWithTag("$realIndex").assertLeftPositionInRootIsEqualTo(expectedPosition)
- }
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyCustomKeysTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyCustomKeysTest.kt
deleted file mode 100644
index 2c6861b..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyCustomKeysTest.kt
+++ /dev/null
@@ -1,379 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyCustomKeysTest {
-
- @get:Rule val rule = createComposeRule()
-
- val itemSize = with(rule.density) { 100.toDp() }
- val columns = 2
-
- @Test
- fun itemsWithKeysAreLaidOutCorrectly() {
- val list = listOf(MyClass(0), MyClass(1), MyClass(2))
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
- items(list, key = { it.id }) { Item("${it.id}") }
- }
- }
-
- assertItems("0", "1", "2")
- }
-
- @Test
- fun removing_statesAreMoved() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
- items(list, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- rule.runOnIdle { list = listOf(list[0], list[2]) }
-
- assertItems("0", "2")
- }
-
- @Test
- fun reordering_statesAreMoved_list() {
- testReordering { grid -> items(grid, key = { it.id }) { Item(remember { "${it.id}" }) } }
- }
-
- @Test
- fun reordering_statesAreMoved_list_indexed() {
- testReordering { grid ->
- itemsIndexed(grid, key = { _, item -> item.id }) { _, item ->
- Item(remember { "${item.id}" })
- }
- }
- }
-
- @Test
- fun reordering_statesAreMoved_array() {
- testReordering { grid ->
- val array = grid.toTypedArray()
- items(array, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- @Test
- fun reordering_statesAreMoved_array_indexed() {
- testReordering { grid ->
- val array = grid.toTypedArray()
- itemsIndexed(array, key = { _, item -> item.id }) { _, item ->
- Item(remember { "${item.id}" })
- }
- }
- }
-
- @Test
- fun reordering_statesAreMoved_itemsWithCount() {
- testReordering { grid ->
- items(grid.size, key = { grid[it].id }) { Item(remember { "${grid[it].id}" }) }
- }
- }
-
- @Test
- fun fullyReplacingTheList() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- var counter = 0
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
- items(list, key = { it.id }) { Item(remember { counter++ }.toString()) }
- }
- }
-
- rule.runOnIdle { list = listOf(MyClass(3), MyClass(4), MyClass(5), MyClass(6)) }
-
- assertItems("3", "4", "5", "6")
- }
-
- @Test
- fun keepingOneItem() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- var counter = 0
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
- items(list, key = { it.id }) { Item(remember { counter++ }.toString()) }
- }
- }
-
- rule.runOnIdle { list = listOf(MyClass(1)) }
-
- assertItems("1")
- }
-
- @Test
- fun keepingOneItemAndAddingMore() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- var counter = 0
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
- items(list, key = { it.id }) { Item(remember { counter++ }.toString()) }
- }
- }
-
- rule.runOnIdle { list = listOf(MyClass(1), MyClass(3)) }
-
- assertItems("1", "3")
- }
-
- @Test
- fun mixingKeyedItemsAndNot() {
- testReordering { list ->
- item { Item("${list.first().id}") }
- items(list.subList(fromIndex = 1, toIndex = list.size), key = { it.id }) {
- Item(remember { "${it.id}" })
- }
- }
- }
-
- @Test
- fun updatingTheDataSetIsCorrectlyApplied() {
- val state = mutableStateOf(emptyList<Int>())
-
- rule.setContent {
- LaunchedEffect(Unit) { state.value = listOf(4, 1, 3) }
-
- val list = state.value
-
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.fillMaxSize()) {
- items(list, key = { it }) { Item(it.toString()) }
- }
- }
-
- assertItems("4", "1", "3")
-
- rule.runOnIdle { state.value = listOf(2, 4, 6, 1, 3, 5) }
-
- assertItems("2", "4", "6", "1", "3", "5")
- }
-
- @Test
- fun reordering_usingMutableStateListOf() {
- val list = mutableStateListOf(MyClass(0), MyClass(1), MyClass(2))
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
- items(list, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- rule.runOnIdle { list.add(list.removeAt(1)) }
-
- assertItems("0", "2", "1")
- }
-
- @Test
- fun keysInLazyListItemInfoAreCorrect() {
- val list = listOf(MyClass(0), MyClass(1), MyClass(2))
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), state = state) {
- items(list, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- rule.runOnIdle { assertThat(state.visibleKeys).isEqualTo(listOf(0, 1, 2)) }
- }
-
- @Test
- fun keysInLazyListItemInfoAreCorrectAfterReordering() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(columns = TvGridCells.Fixed(columns), state = state) {
- items(list, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- rule.runOnIdle { list = listOf(list[0], list[2], list[1]) }
-
- rule.runOnIdle { assertThat(state.visibleKeys).isEqualTo(listOf(0, 2, 1)) }
- }
-
- @Test
- fun addingItemsBeforeWithoutKeysIsMaintainingTheIndex() {
- var list by mutableStateOf((10..15).toList())
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
- items(list) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..15).toList() }
-
- rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(0) }
- }
-
- @Test
- fun addingItemsBeforeKeepingThisItemFirst() {
- var list by mutableStateOf((10..15).toList())
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..15).toList() }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(10)
- assertThat(state.visibleKeys).isEqualTo(listOf(10, 11, 12, 13, 14, 15))
- }
- }
-
- @Test
- fun addingItemsRightAfterKeepingThisItemFirst() {
- var list by mutableStateOf((0..5).toList() + (10..15).toList())
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState(5)
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..15).toList() }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(4)
- assertThat(state.visibleKeys).isEqualTo(listOf(4, 5, 6, 7, 8, 9))
- }
- }
-
- @Test
- fun addingItemsBeforeWhileCurrentItemIsNotInTheBeginning() {
- var list by mutableStateOf((10..30).toList())
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState(10) // key 20 is the first item
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..30).toList() }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(20)
- assertThat(state.visibleKeys).isEqualTo(listOf(20, 21, 22, 23, 24, 25))
- }
- }
-
- @Test
- fun removingTheCurrentItemMaintainsTheIndex() {
- var list by mutableStateOf((0..20).toList())
- lateinit var state: TvLazyGridState
-
- rule.setContent {
- state = rememberTvLazyGridState(8)
- TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..20) - 8 }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(8)
- assertThat(state.visibleKeys).isEqualTo(listOf(9, 10, 11, 12, 13, 14))
- }
- }
-
- private fun testReordering(content: TvLazyGridScope.(List<MyClass>) -> Unit) {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
-
- rule.setContent { TvLazyVerticalGrid(TvGridCells.Fixed(columns)) { content(list) } }
-
- rule.runOnIdle { list = listOf(list[0], list[2], list[1]) }
-
- assertItems("0", "2", "1")
- }
-
- private fun assertItems(vararg tags: String) {
- var currentTop = 0.dp
- var column = 0
- tags.forEach {
- rule
- .onNodeWithTag(it)
- .assertTopPositionInRootIsEqualTo(currentTop)
- .assertHeightIsEqualTo(itemSize)
- ++column
- if (column == columns) {
- currentTop += itemSize
- column = 0
- }
- }
- }
-
- @Composable
- private fun Item(tag: String) {
- Spacer(Modifier.testTag(tag).size(itemSize))
- }
-
- private class MyClass(val id: Int)
-}
-
-val TvLazyGridState.visibleKeys: List<Any>
- get() = layoutInfo.visibleItemsInfo.map { it.key }
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
deleted file mode 100644
index 05d220b..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ /dev/null
@@ -1,2258 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.VisibilityThreshold
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.requiredWidthIn
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntRect
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import java.util.concurrent.TimeUnit
-import kotlin.math.roundToInt
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@OptIn(ExperimentalFoundationApi::class)
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyGridAnimateItemPlacementTest(private val config: Config) {
-
- private val isVertical: Boolean
- get() = config.isVertical
-
- private val reverseLayout: Boolean
- get() = config.reverseLayout
-
- @get:Rule val rule = createComposeRule()
-
- // the numbers should be divisible by 8 to avoid the rounding issues as we run 4 or 8 frames
- // of the animation.
- private val itemSize: Float = 40f
- private var itemSizeDp: Dp = Dp.Infinity
- private val itemSize2: Float = 24f
- private var itemSize2Dp: Dp = Dp.Infinity
- private val itemSize3: Float = 16f
- private var itemSize3Dp: Dp = Dp.Infinity
- private val containerSize: Float = itemSize * 5
- private var containerSizeDp: Dp = Dp.Infinity
- private val spacing: Float = 8f
- private var spacingDp: Dp = Dp.Infinity
- private val itemSizePlusSpacing = itemSize + spacing
- private var itemSizePlusSpacingDp = Dp.Infinity
- private lateinit var state: TvLazyGridState
-
- @Before
- fun before() {
- rule.mainClock.autoAdvance = false
- with(rule.density) {
- itemSizeDp = itemSize.toDp()
- itemSize2Dp = itemSize2.toDp()
- itemSize3Dp = itemSize3.toDp()
- containerSizeDp = containerSize.toDp()
- spacingDp = spacing.toDp()
- itemSizePlusSpacingDp = itemSizePlusSpacing.toDp()
- }
- }
-
- @Test
- fun reorderTwoItems() {
- var list by mutableStateOf(listOf(0, 1))
- rule.setContent { LazyGrid(1) { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(0 to AxisOffset(0f, 0f), 1 to AxisOffset(0f, itemSize))
-
- rule.runOnUiThread { list = listOf(1, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0f + itemSize * fraction),
- 1 to AxisOffset(0f, itemSize - itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun reorderTwoByTwoItems() {
- var list by mutableStateOf(listOf(0, 1, 2, 3))
- rule.setContent { LazyGrid(2) { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(3, 2, 1, 0) }
-
- onAnimationFrame { fraction ->
- val increasing = 0 + itemSize * fraction
- val decreasing = itemSize - itemSize * fraction
- assertPositions(
- 0 to AxisOffset(increasing, increasing),
- 1 to AxisOffset(decreasing, increasing),
- 2 to AxisOffset(increasing, decreasing),
- 3 to AxisOffset(decreasing, decreasing),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun reorderTwoItems_layoutInfoHasFinalPositions() {
- var list by mutableStateOf(listOf(0, 1, 2, 3))
- rule.setContent { LazyGrid(2) { items(list, key = { it }) { Item(it) } } }
-
- assertLayoutInfoPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(3, 2, 1, 0) }
-
- onAnimationFrame {
- // fraction doesn't affect the offsets in layout info
- assertLayoutInfoPositions(
- 3 to AxisOffset(0f, 0f),
- 2 to AxisOffset(itemSize, 0f),
- 1 to AxisOffset(0f, itemSize),
- 0 to AxisOffset(itemSize, itemSize)
- )
- }
- }
-
- @Test
- fun reorderFirstAndLastItems() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent { LazyGrid(1) { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2),
- 3 to AxisOffset(0f, itemSize * 3),
- 4 to AxisOffset(0f, itemSize * 4)
- )
-
- rule.runOnUiThread { list = listOf(4, 1, 2, 3, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0f + itemSize * 4 * fraction),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2),
- 3 to AxisOffset(0f, itemSize * 3),
- 4 to AxisOffset(0f, itemSize * 4 - itemSize * 4 * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveFirstItemToEndCausingAllItemsToAnimate() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent { LazyGrid(2) { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize),
- 4 to AxisOffset(0f, itemSize * 2),
- 5 to AxisOffset(itemSize, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 5, 0) }
-
- onAnimationFrame { fraction ->
- val increasingX = 0 + itemSize * fraction
- val decreasingX = itemSize - itemSize * fraction
- assertPositions(
- 0 to AxisOffset(increasingX, 0f + itemSize * 2 * fraction),
- 1 to AxisOffset(decreasingX, 0f),
- 2 to AxisOffset(increasingX, itemSize - itemSize * fraction),
- 3 to AxisOffset(decreasingX, itemSize),
- 4 to AxisOffset(increasingX, itemSize * 2 - itemSize * fraction),
- 5 to AxisOffset(decreasingX, itemSize * 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun itemSizeChangeAnimatesNextItems() {
- var size by mutableStateOf(itemSizeDp)
- rule.setContent {
- LazyGrid(1, minSize = itemSizeDp * 5, maxSize = itemSizeDp * 5) {
- items(listOf(0, 1, 2, 3), key = { it }) {
- Item(it, size = if (it == 1) size else itemSizeDp)
- }
- }
- }
-
- rule.runOnUiThread { size = itemSizeDp * 2 }
- rule.mainClock.advanceTimeByFrame()
-
- rule.onNodeWithTag("1").assertMainAxisSizeIsEqualTo(size)
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2 + itemSize * fraction),
- 3 to AxisOffset(0f, itemSize * 3 + itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun onlyItemsWithModifierAnimates() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent {
- LazyGrid(1) {
- items(list, key = { it }) {
- Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
- }
- }
- }
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, itemSize * 4),
- 1 to AxisOffset(0f, itemSize - itemSize * fraction),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(0f, itemSize * 3 - itemSize * fraction),
- 4 to AxisOffset(0f, itemSize * 3),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animationsWithDifferentDurations() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent {
- LazyGrid(1) {
- items(list, key = { it }) {
- val duration = if (it == 1 || it == 3) Duration * 2 else Duration
- Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
- }
- }
- }
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 0) }
-
- onAnimationFrame(duration = Duration * 2) { fraction ->
- val shorterAnimFraction = (fraction * 2).coerceAtMost(1f)
- assertPositions(
- 0 to AxisOffset(0f, 0 + itemSize * 4 * shorterAnimFraction),
- 1 to AxisOffset(0f, itemSize - itemSize * fraction),
- 2 to AxisOffset(0f, itemSize * 2 - itemSize * shorterAnimFraction),
- 3 to AxisOffset(0f, itemSize * 3 - itemSize * fraction),
- 4 to AxisOffset(0f, itemSize * 4 - itemSize * shorterAnimFraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun multipleChildrenPerItem() {
- var list by mutableStateOf(listOf(0, 2))
- rule.setContent {
- LazyGrid(1) {
- items(list, key = { it }) {
- Item(it)
- Item(it + 1)
- }
- }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(0f, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(2, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0 + itemSize * fraction),
- 1 to AxisOffset(0f, 0 + itemSize * fraction),
- 2 to AxisOffset(0f, itemSize - itemSize * fraction),
- 3 to AxisOffset(0f, itemSize - itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun multipleChildrenPerItemSomeDoNotAnimate() {
- var list by mutableStateOf(listOf(0, 2))
- rule.setContent {
- LazyGrid(1) {
- items(list, key = { it }) {
- Item(it)
- Item(it + 1, animSpec = null)
- }
- }
- }
-
- rule.runOnUiThread { list = listOf(2, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0 + itemSize * fraction),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize - itemSize * fraction),
- 3 to AxisOffset(0f, 0f),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animateArrangementChange() {
- var arrangement by mutableStateOf(Arrangement.Center)
- rule.setContent {
- LazyGrid(
- 1,
- arrangement = arrangement,
- minSize = itemSizeDp * 5,
- maxSize = itemSizeDp * 5
- ) {
- items(listOf(1, 2, 3), key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2),
- 3 to AxisOffset(0f, itemSize * 3),
- )
-
- rule.runOnUiThread { arrangement = Arrangement.SpaceBetween }
- rule.mainClock.advanceTimeByFrame()
-
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to AxisOffset(0f, itemSize - itemSize * fraction),
- 2 to AxisOffset(0f, itemSize * 2),
- 3 to AxisOffset(0f, itemSize * 3 + itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveItemToTheBottomOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
- val gridSize = itemSize * 3
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(2, maxSize = gridSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize),
- 4 to AxisOffset(0f, itemSize * 2),
- 5 to AxisOffset(itemSize, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to and item 8 moves from `gridSize`, right after the end edge
- val item1Offset = AxisOffset(itemSize, 0 + gridSize * fraction)
- val item8Offset = AxisOffset(itemSize, gridSize - gridSize * fraction)
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- if (item1Offset.mainAxis < itemSize * 3) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(2 to AxisOffset(0f, itemSize))
- add(3 to AxisOffset(itemSize, itemSize))
- add(4 to AxisOffset(0f, itemSize * 2))
- add(5 to AxisOffset(itemSize, itemSize * 2))
- if (item8Offset.mainAxis < itemSize * 3) {
- add(8 to item8Offset)
- } else {
- rule.onNodeWithTag("8").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
- rule.setContent {
- LazyGrid(2, maxSize = itemSizeDp * 3, startIndex = 6) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 6 to AxisOffset(0f, 0f),
- 7 to AxisOffset(itemSize, 0f),
- 8 to AxisOffset(0f, itemSize),
- 9 to AxisOffset(itemSize, itemSize),
- 10 to AxisOffset(0f, itemSize * 2),
- 11 to AxisOffset(itemSize, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11) }
-
- onAnimationFrame { fraction ->
- // item 1 moves from and item 8 moves to `0 - itemSize`, right before the start edge
- val item8Offset = AxisOffset(0f, itemSize - itemSize * 2 * fraction)
- val item1Offset = AxisOffset(0f, -itemSize + itemSize * 2 * fraction)
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- if (item1Offset.mainAxis > -itemSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(6 to AxisOffset(0f, 0f))
- add(7 to AxisOffset(itemSize, 0f))
- if (item8Offset.mainAxis > -itemSize) {
- add(8 to item8Offset)
- } else {
- rule.onNodeWithTag("8").assertIsNotDisplayed()
- }
- add(9 to AxisOffset(itemSize, itemSize))
- add(10 to AxisOffset(0f, itemSize * 2))
- add(11 to AxisOffset(itemSize, itemSize * 2))
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveFirstItemToEndCausingAllItemsToAnimate_withSpacing() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
- rule.setContent {
- LazyGrid(2, arrangement = Arrangement.spacedBy(spacingDp)) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 5, 6, 7, 0) }
-
- onAnimationFrame { fraction ->
- val increasingX = fraction * itemSize
- val decreasingX = itemSize - itemSize * fraction
- assertPositions(
- 0 to AxisOffset(increasingX, itemSizePlusSpacing * 3 * fraction),
- 1 to AxisOffset(decreasingX, 0f),
- 2 to AxisOffset(increasingX, itemSizePlusSpacing - itemSizePlusSpacing * fraction),
- 3 to AxisOffset(decreasingX, itemSizePlusSpacing),
- 4 to
- AxisOffset(
- increasingX,
- itemSizePlusSpacing * 2 - itemSizePlusSpacing * fraction
- ),
- 5 to AxisOffset(decreasingX, itemSizePlusSpacing * 2),
- 6 to
- AxisOffset(
- increasingX,
- itemSizePlusSpacing * 3 - itemSizePlusSpacing * fraction
- ),
- 7 to AxisOffset(decreasingX, itemSizePlusSpacing * 3),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveItemToTheBottomOutsideOfBounds_withSpacing() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
- val gridSize = itemSize * 3 + spacing * 2
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(2, maxSize = gridSizeDp, arrangement = Arrangement.spacedBy(spacingDp)) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSizePlusSpacing),
- 3 to AxisOffset(itemSize, itemSizePlusSpacing),
- 4 to AxisOffset(0f, itemSizePlusSpacing * 2),
- 5 to AxisOffset(itemSize, itemSizePlusSpacing * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to and item 8 moves from `gridSize`, right after the end edge
- val item1Offset = AxisOffset(itemSize, gridSize * fraction)
- val item8Offset = AxisOffset(itemSize, gridSize - gridSize * fraction)
- val screenSize = itemSize * 3 + spacing * 2
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- if (item1Offset.mainAxis < screenSize) {
- add(1 to item1Offset)
- }
- add(2 to AxisOffset(0f, itemSizePlusSpacing))
- add(3 to AxisOffset(itemSize, itemSizePlusSpacing))
- add(4 to AxisOffset(0f, itemSizePlusSpacing * 2))
- add(5 to AxisOffset(itemSize, itemSizePlusSpacing * 2))
- if (item8Offset.mainAxis < screenSize) {
- add(8 to item8Offset)
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds_withSpacing() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
- rule.setContent {
- LazyGrid(
- 2,
- maxSize = itemSizeDp * 3 + spacingDp * 2,
- arrangement = Arrangement.spacedBy(spacingDp),
- startIndex = 4
- ) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 4 to AxisOffset(0f, 0f),
- 5 to AxisOffset(itemSize, 0f),
- 6 to AxisOffset(0f, itemSizePlusSpacing),
- 7 to AxisOffset(itemSize, itemSizePlusSpacing),
- 8 to AxisOffset(0f, itemSizePlusSpacing * 2),
- 9 to AxisOffset(itemSize, itemSizePlusSpacing * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11) }
-
- onAnimationFrame { fraction ->
- // item 8 moves to and item 1 moves from `-itemSize`, right before the start edge
- val item1Offset =
- AxisOffset(0f, -itemSize + (itemSize + itemSizePlusSpacing * 2) * fraction)
- val item8Offset =
- AxisOffset(
- 0f,
- itemSizePlusSpacing * 2 - (itemSize + itemSizePlusSpacing * 2) * fraction
- )
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- if (item1Offset.mainAxis > -itemSize) {
- add(1 to item1Offset)
- }
- add(4 to AxisOffset(0f, 0f))
- add(5 to AxisOffset(itemSize, 0f))
- add(6 to AxisOffset(0f, itemSizePlusSpacing))
- add(7 to AxisOffset(itemSize, itemSizePlusSpacing))
- if (item8Offset.mainAxis > -itemSize) {
- add(8 to item8Offset)
- }
- add(9 to AxisOffset(itemSize, itemSizePlusSpacing * 2))
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds_differentSizes() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
- rule.setContent {
- LazyGrid(2, maxSize = itemSize2Dp + itemSize3Dp + itemSizeDp, startIndex = 6) {
- items(list, key = { it }) {
- val height =
- when (it) {
- 2 -> itemSize3Dp
- 3 -> itemSize3Dp / 2
- 6 -> itemSize2Dp
- 7 -> itemSize2Dp / 2
- else -> {
- if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
- }
- }
- Item(it, size = height)
- }
- }
- }
-
- val line3Size = itemSize2
- val line4Size = itemSize
- assertPositions(
- 6 to AxisOffset(0f, 0f),
- 7 to AxisOffset(itemSize, 0f),
- 8 to AxisOffset(0f, line3Size),
- 9 to AxisOffset(itemSize, line3Size),
- 10 to AxisOffset(0f, line3Size + line4Size),
- 11 to AxisOffset(itemSize, line3Size + line4Size)
- )
-
- rule.runOnUiThread {
- // swap 8 and 2
- list = listOf(0, 1, 8, 3, 4, 5, 6, 7, 2, 9, 10, 11)
- }
-
- onAnimationFrame { fraction ->
- // items 4 and 5 were lines 1 and 3 but we don't compose it
- rule.onNodeWithTag("4").assertDoesNotExist()
- rule.onNodeWithTag("5").assertDoesNotExist()
- val item2Size = itemSize3 /* the real size of the item 2 */
- // item 2 moves from and item 4 moves to `0 - item size`, right before the start edge
- val startItem2Offset = -item2Size
- val item2Offset = startItem2Offset + (itemSize2 - startItem2Offset) * fraction
- val item8Size = itemSize /* the real size of the item 8 */
- val endItem8Offset = -item8Size
- val item8Offset = line3Size - (line3Size - endItem8Offset) * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- if (item8Offset > -line4Size) {
- add(8 to AxisOffset(0f, item8Offset))
- } else {
- rule.onNodeWithTag("8").assertIsNotDisplayed()
- }
- add(6 to AxisOffset(0f, 0f))
- add(7 to AxisOffset(itemSize, 0f))
- if (item2Offset > -item2Size) {
- add(2 to AxisOffset(0f, item2Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- add(9 to AxisOffset(itemSize, line3Size))
- add(
- 10 to
- AxisOffset(
- 0f,
- line3Size + line4Size - (itemSize - itemSize3) * fraction
- )
- )
- add(
- 11 to
- AxisOffset(
- itemSize,
- line3Size + line4Size - (itemSize - itemSize3) * fraction
- )
- )
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheBottomOutsideOfBounds_differentSizes() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
- val gridSize = itemSize2 + itemSize3 + itemSize
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(2, maxSize = gridSizeDp) {
- items(list, key = { it }) {
- val height =
- when (it) {
- 0 -> itemSize2Dp
- 8 -> itemSize3Dp
- else -> {
- if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
- }
- }
- Item(it, size = height)
- }
- }
- }
-
- val line0Size = itemSize2
- val line1Size = itemSize
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, line0Size),
- 3 to AxisOffset(itemSize, line0Size),
- 4 to AxisOffset(0f, line0Size + line1Size),
- 5 to AxisOffset(itemSize, line0Size + line1Size),
- )
-
- rule.runOnUiThread { list = listOf(0, 1, 8, 3, 4, 5, 6, 7, 2, 9, 10, 11) }
-
- onAnimationFrame { fraction ->
- // item 1 moves from and item 8 moves to `gridSize`, right after the end edge
- val startItem8Offset = gridSize
- val endItem2Offset = gridSize
- val line4Size = itemSize3
- val item2Offset = line0Size + (endItem2Offset - line0Size) * fraction
- val item8Offset = startItem8Offset - (startItem8Offset - line0Size) * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- add(1 to AxisOffset(itemSize, 0f))
- if (item8Offset < gridSize) {
- add(8 to AxisOffset(0f, item8Offset))
- } else {
- rule.onNodeWithTag("8").assertIsNotDisplayed()
- }
- add(3 to AxisOffset(itemSize, line0Size))
- add(
- 4 to
- AxisOffset(
- 0f,
- line0Size + line1Size - (line1Size - line4Size) * fraction
- )
- )
- add(
- 5 to
- AxisOffset(
- itemSize,
- line0Size + line1Size - (line1Size - line4Size) * fraction
- )
- )
- if (item2Offset < gridSize) {
- add(2 to AxisOffset(0f, item2Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- // @Test
- // fun animateAlignmentChange() {
- // var alignment by mutableStateOf(CrossAxisAlignment.End)
- // rule.setContent{
- // LazyGrid(1,
- // crossAxisAlignment = alignment,
- // crossAxisSize = itemSizeDp
- // ) {
- // items(listOf(1, 2, 3), key = { it }) {
- // val crossAxisSize =
- // if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- // Item(it, crossAxisSize = crossAxisSize)
- // }
- // }
- // }
-
- // val item2Start = itemSize - itemSize2
- // val item3Start = itemSize - itemSize3
- // assertPositions(
- // 1 to 0,
- // 2 to itemSize,
- // 3 to itemSize * 2,
- // crossAxis = listOf(
- // 1 to 0,
- // 2 to item2Start,
- // 3 to item3Start,
- // )
- // )
-
- // rule.runOnUiThread {
- // alignment = CrossAxisAlignment.Center
- // }
- // rule.mainClock.advanceTimeByFrame()
-
- // val item2End = itemSize / 2 - itemSize2 / 2
- // val item3End = itemSize / 2 - itemSize3 / 2
- // onAnimationFrame { fraction ->
- // assertPositions(
- // 1 to 0,
- // 2 to itemSize,
- // 3 to itemSize * 2,
- // crossAxis = listOf(
- // 1 to 0,
- // 2 to item2Start + ((item2End - item2Start) * fraction,
- // 3 to item3Start + ((item3End - item3Start) * fraction,
- // ),
- // fraction = fraction
- // )
- // }
- // }
-
- // @Test
- // fun animateAlignmentChange_multipleChildrenPerItem() {
- // var alignment by mutableStateOf(CrossAxisAlignment.Start)
- // rule.setContent{
- // LazyGrid(1,
- // crossAxisAlignment = alignment,
- // crossAxisSize = itemSizeDp * 2
- // ) {
- // items(1) {
- // listOf(1, 2, 3).forEach {
- // val crossAxisSize =
- // if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else
- // itemSize3Dp
- // Item(it, crossAxisSize = crossAxisSize)
- // }
- // }
- // }
- // }
-
- // rule.runOnUiThread {
- // alignment = CrossAxisAlignment.End
- // }
- // rule.mainClock.advanceTimeByFrame()
-
- // val containerSize = itemSize * 2
- // onAnimationFrame { fraction ->
- // assertPositions(
- // 1 to 0,
- // 2 to itemSize,
- // 3 to itemSize * 2,
- // crossAxis = listOf(
- // 1 to ((containerSize - itemSize) * fraction,
- // 2 to ((containerSize - itemSize2) * fraction,
- // 3 to ((containerSize - itemSize3) * fraction
- // ),
- // fraction = fraction
- // )
- // }
- // }
-
- // @Test
- // fun animateAlignmentChange_rtl() {
- // // this test is not applicable to LazyRow
- // assumeTrue(isVertical)
-
- // var alignment by mutableStateOf(CrossAxisAlignment.End)
- // rule.setContent{
- // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- // LazyGrid(1,
- // crossAxisAlignment = alignment,
- // crossAxisSize = itemSizeDp
- // ) {
- // items(listOf(1, 2, 3), key = { it }) {
- // val crossAxisSize =
- // if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else
- // itemSize3Dp
- // Item(it, crossAxisSize = crossAxisSize)
- // }
- // }
- // }
- // }
-
- // assertPositions(
- // 1 to 0,
- // 2 to itemSize,
- // 3 to itemSize * 2,
- // crossAxis = listOf(
- // 1 to 0,
- // 2 to 0,
- // 3 to 0,
- // )
- // )
-
- // rule.runOnUiThread {
- // alignment = CrossAxisAlignment.Center
- // }
- // rule.mainClock.advanceTimeByFrame()
-
- // onAnimationFrame { fraction ->
- // assertPositions(
- // 1 to 0,
- // 2 to itemSize,
- // 3 to itemSize * 2,
- // crossAxis = listOf(
- // 1 to 0,
- // 2 to ((itemSize / 2 - itemSize2 / 2) * fraction,
- // 3 to ((itemSize / 2 - itemSize3 / 2) * fraction,
- // ),
- // fraction = fraction
- // )
- // }
- // }
-
- @Test
- fun moveItemToEndCausingNextItemsToAnimate_withContentPadding() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val rawStartPadding = 8f
- val rawEndPadding = 12f
- val (startPaddingDp, endPaddingDp) =
- with(rule.density) { rawStartPadding.toDp() to rawEndPadding.toDp() }
- rule.setContent {
- LazyGrid(1, startPadding = startPaddingDp, endPadding = endPaddingDp) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
- assertPositions(
- 0 to AxisOffset(0f, startPadding),
- 1 to AxisOffset(0f, startPadding + itemSize),
- 2 to AxisOffset(0f, startPadding + itemSize * 2),
- 3 to AxisOffset(0f, startPadding + itemSize * 3),
- 4 to AxisOffset(0f, startPadding + itemSize * 4),
- )
-
- rule.runOnUiThread { list = listOf(0, 2, 3, 4, 1) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, startPadding),
- 1 to AxisOffset(0f, startPadding + itemSize + itemSize * 3 * fraction),
- 2 to AxisOffset(0f, startPadding + itemSize * 2 - itemSize * fraction),
- 3 to AxisOffset(0f, startPadding + itemSize * 3 - itemSize * fraction),
- 4 to AxisOffset(0f, startPadding + itemSize * 4 - itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
-
- var measurePasses = 0
- rule.setContent {
- LazyGrid(1) { items(list, key = { it }) { Item(it) } }
- LaunchedEffect(Unit) { snapshotFlow { state.layoutInfo }.collect { measurePasses++ } }
- }
-
- rule.runOnUiThread { list = listOf(4, 1, 2, 3, 0) }
-
- var startMeasurePasses = Int.MIN_VALUE
- onAnimationFrame { fraction ->
- if (fraction == 0f) {
- startMeasurePasses = measurePasses
- }
- }
- rule.mainClock.advanceTimeByFrame()
- // new layoutInfo is produced on every remeasure of Lazy lists.
- // but we want to avoid remeasuring and only do relayout on each animation frame.
- // two extra measures are possible as we switch inProgress flag.
- assertThat(measurePasses).isAtMost(startMeasurePasses + 2)
- }
-
- @Test
- fun noAnimationWhenScrolledToOtherPosition() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollToItem(0, (itemSize / 2).roundToInt()) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, -itemSize / 2),
- 1 to AxisOffset(0f, itemSize / 2),
- 2 to AxisOffset(0f, itemSize * 3 / 2),
- 3 to AxisOffset(0f, itemSize * 5 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardBySmallOffset() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize / 2f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, -itemSize / 2),
- 1 to AxisOffset(0f, itemSize / 2),
- 2 to AxisOffset(0f, itemSize * 3 / 2),
- 3 to AxisOffset(0f, itemSize * 5 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardBySmallOffset() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3, startIndex = 2) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(-itemSize / 2f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to AxisOffset(0f, -itemSize / 2),
- 2 to AxisOffset(0f, itemSize / 2),
- 3 to AxisOffset(0f, itemSize * 3 / 2),
- 4 to AxisOffset(0f, itemSize * 5 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 2 to AxisOffset(0f, -itemSize / 2),
- 3 to AxisOffset(0f, itemSize / 2),
- 4 to AxisOffset(0f, itemSize * 3 / 2),
- 5 to AxisOffset(0f, itemSize * 5 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3, startIndex = 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(-itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, -itemSize / 2),
- 1 to AxisOffset(0f, itemSize / 2),
- 2 to AxisOffset(0f, itemSize * 3 / 2),
- 3 to AxisOffset(0f, itemSize * 5 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset_differentSizes() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
- Item(it, size = if (it % 2 == 0) itemSizeDp else itemSize2Dp)
- }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize + itemSize2 + itemSize / 2f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 2 to AxisOffset(0f, -itemSize / 2),
- 3 to AxisOffset(0f, itemSize / 2),
- 4 to AxisOffset(0f, itemSize2 + itemSize / 2),
- 5 to AxisOffset(0f, itemSize2 + itemSize * 3 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset_differentSizes() {
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3, startIndex = 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
- Item(it, size = if (it % 2 == 0) itemSizeDp else itemSize2Dp)
- }
- }
- }
-
- rule.runOnUiThread {
- runBlocking { state.scrollBy(-(itemSize + itemSize2 + itemSize / 2f)) }
- }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, -itemSize / 2),
- 1 to AxisOffset(0f, itemSize / 2),
- 2 to AxisOffset(0f, itemSize2 + itemSize / 2),
- 3 to AxisOffset(0f, itemSize2 + itemSize * 3 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset_multipleCells() {
- rule.setContent {
- LazyGrid(3, maxSize = itemSizeDp * 2) {
- items(List(20) { it }, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(itemSize * 2, 0f),
- 3 to AxisOffset(0f, itemSize),
- 4 to AxisOffset(itemSize, itemSize),
- 5 to AxisOffset(itemSize * 2, itemSize)
- )
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 6 to AxisOffset(0f, -itemSize / 2),
- 7 to AxisOffset(itemSize, -itemSize / 2),
- 8 to AxisOffset(itemSize * 2, -itemSize / 2),
- 9 to AxisOffset(0f, itemSize / 2),
- 10 to AxisOffset(itemSize, itemSize / 2),
- 11 to AxisOffset(itemSize * 2, itemSize / 2),
- 12 to AxisOffset(0f, itemSize * 3 / 2),
- 13 to AxisOffset(itemSize, itemSize * 3 / 2),
- 14 to AxisOffset(itemSize * 2, itemSize * 3 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset_multipleCells() {
- rule.setContent {
- LazyGrid(3, maxSize = itemSizeDp * 2, startIndex = 9) {
- items(List(20) { it }, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 9 to AxisOffset(0f, 0f),
- 10 to AxisOffset(itemSize, 0f),
- 11 to AxisOffset(itemSize * 2, 0f),
- 12 to AxisOffset(0f, itemSize),
- 13 to AxisOffset(itemSize, itemSize),
- 14 to AxisOffset(itemSize * 2, itemSize)
- )
-
- rule.runOnUiThread { runBlocking { state.scrollBy(-itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, -itemSize / 2),
- 1 to AxisOffset(itemSize, -itemSize / 2),
- 2 to AxisOffset(itemSize * 2, -itemSize / 2),
- 3 to AxisOffset(0f, itemSize / 2),
- 4 to AxisOffset(itemSize, itemSize / 2),
- 5 to AxisOffset(itemSize * 2, itemSize / 2),
- 6 to AxisOffset(0f, itemSize * 3 / 2),
- 7 to AxisOffset(itemSize, itemSize * 3 / 2),
- 8 to AxisOffset(itemSize * 2, itemSize * 3 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset_differentSpans() {
- rule.setContent {
- LazyGrid(3, maxSize = itemSizeDp * 2) {
- items(
- List(20) { it },
- key = { it },
- span = { TvGridItemSpan(if (it == 9) 3 else if (it == 10) 2 else 1) }
- ) {
- Item(it)
- }
- }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(itemSize * 2, 0f),
- 3 to AxisOffset(0f, itemSize),
- 4 to AxisOffset(itemSize, itemSize),
- 5 to AxisOffset(itemSize * 2, itemSize)
- )
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 6 to AxisOffset(0f, -itemSize / 2),
- 7 to AxisOffset(itemSize, -itemSize / 2),
- 8 to AxisOffset(itemSize * 2, -itemSize / 2),
- 9 to AxisOffset(0f, itemSize / 2), // 3 spans
- 10 to AxisOffset(0f, itemSize * 3 / 2), // 2 spans
- 11 to AxisOffset(itemSize * 2, itemSize * 3 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset_differentSpans() {
- rule.setContent {
- LazyGrid(3, maxSize = itemSizeDp * 2, startIndex = 6) {
- items(
- List(20) { it },
- key = { it },
- span = { TvGridItemSpan(if (it == 3) 3 else if (it == 4) 2 else 1) }
- ) {
- Item(it)
- }
- }
- }
-
- assertPositions(
- 6 to AxisOffset(0f, 0f),
- 7 to AxisOffset(itemSize, 0f),
- 8 to AxisOffset(itemSize * 2, 0f),
- 9 to AxisOffset(0f, itemSize),
- 10 to AxisOffset(itemSize, itemSize),
- 11 to AxisOffset(itemSize * 2, itemSize)
- )
-
- rule.runOnUiThread { runBlocking { state.scrollBy(-itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, -itemSize / 2),
- 1 to AxisOffset(itemSize, -itemSize / 2),
- 2 to AxisOffset(itemSize * 2, -itemSize / 2),
- 3 to AxisOffset(0f, itemSize / 2), // 3 spans
- 4 to AxisOffset(0f, itemSize * 3 / 2), // 2 spans
- 5 to AxisOffset(itemSize * 2, itemSize * 3 / 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset_differentSpansAndDifferentSizes() {
- rule.setContent {
- LazyGrid(3, maxSize = itemSizeDp * 2) {
- items(
- List(20) { it },
- key = { it },
- span = { TvGridItemSpan(if (it == 9) 3 else if (it == 10) 2 else 1) }
- ) {
- Item(
- it,
- size =
- when (it) {
- in 6..8 -> itemSize2Dp
- 9 -> itemSize3Dp
- else -> itemSizeDp
- }
- )
- }
- }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(itemSize * 2, 0f),
- 3 to AxisOffset(0f, itemSize),
- 4 to AxisOffset(itemSize, itemSize),
- 5 to AxisOffset(itemSize * 2, itemSize)
- )
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- val startOffset = -itemSize / 2
- assertPositions(
- 6 to AxisOffset(0f, startOffset),
- 7 to AxisOffset(itemSize, startOffset),
- 8 to AxisOffset(itemSize * 2, startOffset),
- 9 to AxisOffset(0f, startOffset + itemSize2), // 3 spans
- 10 to AxisOffset(0f, startOffset + itemSize2 + itemSize3), // 2 spans
- 11 to AxisOffset(itemSize * 2, startOffset + itemSize2 + itemSize3),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset_differentSpansAndDifferentSizes() {
- rule.setContent {
- LazyGrid(3, maxSize = itemSizeDp * 2, startIndex = 6) {
- items(
- List(20) { it },
- key = { it },
- span = { TvGridItemSpan(if (it == 3) 3 else if (it == 4) 2 else 1) }
- ) {
- Item(
- it,
- size =
- when (it) {
- in 0..2 -> itemSize2Dp
- 3 -> itemSize3Dp
- else -> itemSizeDp
- }
- )
- }
- }
- }
-
- assertPositions(
- 6 to AxisOffset(0f, 0f),
- 7 to AxisOffset(itemSize, 0f),
- 8 to AxisOffset(itemSize * 2, 0f),
- 9 to AxisOffset(0f, itemSize),
- 10 to AxisOffset(itemSize, itemSize),
- 11 to AxisOffset(itemSize * 2, itemSize)
- )
-
- rule.runOnUiThread {
- runBlocking { state.scrollBy(-itemSize - itemSize3 - itemSize2 / 2f) }
- }
-
- onAnimationFrame { fraction ->
- val startOffset = -itemSize2 / 2
- assertPositions(
- 0 to AxisOffset(0f, startOffset),
- 1 to AxisOffset(itemSize, startOffset),
- 2 to AxisOffset(itemSize * 2, startOffset),
- 3 to AxisOffset(0f, startOffset + itemSize2), // 3 spans
- 4 to AxisOffset(0f, startOffset + itemSize2 + itemSize3), // 2 spans
- 5 to AxisOffset(itemSize * 2, startOffset + itemSize2 + itemSize3),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animatingItemsWithPreviousIndexLargerThanTheNewItemCount() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
- val gridSize = itemSize * 2
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(2, maxSize = gridSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertLayoutInfoPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(0, 2, 4, 6) }
-
- onAnimationFrame { fraction ->
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- add(2 to AxisOffset(itemSize * fraction, itemSize * (1f - fraction)))
- val item4and6MainAxis = gridSize - (gridSize - itemSize) * fraction
- if (item4and6MainAxis < gridSize) {
- add(4 to AxisOffset(0f, item4and6MainAxis))
- add(6 to AxisOffset(itemSize, item4and6MainAxis))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- rule.onNodeWithTag("6").assertIsNotDisplayed()
- }
- }
-
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun animatingItemsWithPreviousIndexLargerThanTheNewItemCount_differentSpans() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6))
- val gridSize = itemSize * 2
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(2, maxSize = gridSizeDp) {
- items(
- list,
- key = { it },
- span = { TvGridItemSpan(if (it == 6) maxLineSpan else 1) }
- ) {
- Item(it)
- }
- }
- }
-
- assertLayoutInfoPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(0, 4, 6) }
-
- onAnimationFrame { fraction ->
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- val item4MainAxis = gridSize - gridSize * fraction
- if (item4MainAxis < gridSize) {
- add(4 to AxisOffset(itemSize, item4MainAxis))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- val item6MainAxis = gridSize + itemSize - gridSize * fraction
- if (item6MainAxis < gridSize) {
- add(6 to AxisOffset(0f, item6MainAxis))
- } else {
- rule.onNodeWithTag("6").assertIsNotDisplayed()
- }
- }
-
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun itemWithSpecsIsMovingOut() {
- var list by mutableStateOf(listOf(0, 1, 2, 3))
- val gridSize = itemSize * 2
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(1, maxSize = gridSizeDp) {
- items(list, key = { it }) { Item(it, animSpec = if (it == 1) AnimSpec else null) }
- }
- }
-
- rule.runOnUiThread { list = listOf(0, 2, 3, 1) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to `gridSize`
- val item1Offset = itemSize + (gridSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- if (item1Offset < gridSize) {
- add(1 to AxisOffset(0f, item1Offset))
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheTopOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3f, startIndex = 3) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 3 to AxisOffset(0f, 0f),
- 4 to AxisOffset(0f, itemSize),
- 5 to AxisOffset(0f, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 4, 5, 3, 1, 2) }
-
- onAnimationFrame { fraction ->
- // item 2 moves from and item 5 moves to `-itemSize`, right before the start edge
- val item2Offset = -itemSize + itemSize * 3 * fraction
- val item5Offset = itemSize * 2 - itemSize * 3 * fraction
- // item 1 moves from and item 4 moves to `-itemSize * 2`, right before item 2
- val item1Offset = -itemSize * 2 + itemSize * 3 * fraction
- val item4Offset = itemSize - itemSize * 3 * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- if (item1Offset > -itemSize) {
- add(1 to AxisOffset(0f, item1Offset))
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset > -itemSize) {
- add(2 to AxisOffset(0f, item2Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- add(3 to AxisOffset(0f, 0f))
- if (item4Offset > -itemSize) {
- add(4 to AxisOffset(0f, item4Offset))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- if (item5Offset > -itemSize) {
- add(5 to AxisOffset(0f, item5Offset))
- } else {
- rule.onNodeWithTag("5").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheTopOutsideOfBounds_withReordering() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 3f, startIndex = 3) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 3 to AxisOffset(0f, 0f),
- 4 to AxisOffset(0f, itemSize),
- 5 to AxisOffset(0f, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 5, 4, 3, 2, 1) }
-
- onAnimationFrame { fraction ->
- // item 2 moves from and item 4 moves to `-itemSize`, right before the start edge
- val item2Offset = -itemSize + itemSize * 2 * fraction
- val item4Offset = itemSize - itemSize * 2 * fraction
- // item 1 moves from and item 5 moves to `-itemSize * 2`, right before item 2
- val item1Offset = -itemSize * 2 + itemSize * 4 * fraction
- val item5Offset = itemSize * 2 - itemSize * 4 * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- if (item1Offset > -itemSize) {
- add(1 to AxisOffset(0f, item1Offset))
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset > -itemSize) {
- add(2 to AxisOffset(0f, item2Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- add(3 to AxisOffset(0f, 0f))
- if (item4Offset > -itemSize) {
- add(4 to AxisOffset(0f, item4Offset))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- if (item5Offset > -itemSize) {
- add(5 to AxisOffset(0f, item5Offset))
- } else {
- rule.onNodeWithTag("5").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheTopOutsideOfBounds_cellsOfTheSameLine() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyGrid(2, maxSize = itemSizeDp * 2f, startIndex = 3) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 2 to AxisOffset(0f, 0f),
- 3 to AxisOffset(itemSize, 0f),
- 4 to AxisOffset(0f, itemSize),
- 5 to AxisOffset(itemSize, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(4, 5, 2, 3, 0, 1) }
-
- onAnimationFrame { fraction ->
- // items 0 and 2 moves from and items 4 and 5 moves to `-itemSize`,
- // right before the start edge
- val items0and1Offset = -itemSize + itemSize * 2 * fraction
- val items4and5Offset = itemSize - itemSize * 2 * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- if (items0and1Offset > -itemSize) {
- add(0 to AxisOffset(0f, items0and1Offset))
- add(1 to AxisOffset(itemSize, items0and1Offset))
- } else {
- rule.onNodeWithTag("0").assertIsNotDisplayed()
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(2 to AxisOffset(0f, 0f))
- add(3 to AxisOffset(itemSize, 0f))
- if (items4and5Offset > -itemSize) {
- add(4 to AxisOffset(0f, items4and5Offset))
- add(5 to AxisOffset(itemSize, items4and5Offset))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- rule.onNodeWithTag("5").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheBottomOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val gridSize = itemSize * 3
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(1, maxSize = gridSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 3, 4, 1, 2) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to and item 3 moves from `gridSize`, right after the end edge
- val item1Offset = itemSize + (gridSize - itemSize) * fraction
- val item3Offset = gridSize - (gridSize - itemSize) * fraction
- // item 2 moves to and item 4 moves from `gridSize + itemSize`, right after item 4
- val item2Offset = itemSize * 2 + (gridSize - itemSize) * fraction
- val item4Offset = gridSize + itemSize - (gridSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- if (item1Offset < gridSize) {
- add(1 to AxisOffset(0f, item1Offset))
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset < gridSize) {
- add(2 to AxisOffset(0f, item2Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- if (item3Offset < gridSize) {
- add(3 to AxisOffset(0f, item3Offset))
- } else {
- rule.onNodeWithTag("3").assertIsNotDisplayed()
- }
- if (item4Offset < gridSize) {
- add(4 to AxisOffset(0f, item4Offset))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheBottomOutsideOfBounds_withReordering() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val gridSize = itemSize * 3
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(1, maxSize = gridSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2)
- )
-
- rule.runOnUiThread { list = listOf(0, 4, 3, 2, 1) }
-
- onAnimationFrame { fraction ->
- // item 2 moves to and item 3 moves from `gridSize`, right after the end edge
- val item2Offset = itemSize * 2 + (gridSize - itemSize * 2) * fraction
- val item3Offset = gridSize - (gridSize - itemSize * 2) * fraction
- // item 1 moves to and item 4 moves from `gridSize + itemSize`, right after item 4
- val item1Offset = itemSize + (gridSize + itemSize - itemSize) * fraction
- val item4Offset = gridSize + itemSize - (gridSize + itemSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- if (item1Offset < gridSize) {
- add(1 to AxisOffset(0f, item1Offset))
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset < gridSize) {
- add(2 to AxisOffset(0f, item2Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- if (item3Offset < gridSize) {
- add(3 to AxisOffset(0f, item3Offset))
- } else {
- rule.onNodeWithTag("3").assertIsNotDisplayed()
- }
- if (item4Offset < gridSize) {
- add(4 to AxisOffset(0f, item4Offset))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheBottomOutsideOfBounds_cellsOfTheSameLine() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- val gridSize = itemSize * 2
- val gridSizeDp = with(rule.density) { gridSize.toDp() }
- rule.setContent {
- LazyGrid(2, maxSize = gridSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(itemSize, 0f),
- 2 to AxisOffset(0f, itemSize),
- 3 to AxisOffset(itemSize, itemSize)
- )
-
- rule.runOnUiThread { list = listOf(0, 1, 4, 5, 2, 3) }
-
- onAnimationFrame { fraction ->
- // items 4 and 5 moves from and items 2 and 3 moves to `gridSize`,
- // right before the start edge
- val items4and5Offset = gridSize - (gridSize - itemSize) * fraction
- val items2and3Offset = itemSize + (gridSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Offset>>().apply {
- add(0 to AxisOffset(0f, 0f))
- add(1 to AxisOffset(itemSize, 0f))
- if (items2and3Offset < gridSize) {
- add(2 to AxisOffset(0f, items2and3Offset))
- add(3 to AxisOffset(itemSize, items2and3Offset))
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- rule.onNodeWithTag("3").assertIsNotDisplayed()
- }
- if (items4and5Offset < gridSize) {
- add(4 to AxisOffset(0f, items4and5Offset))
- add(5 to AxisOffset(itemSize, items4and5Offset))
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- rule.onNodeWithTag("5").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun noAnimationWhenParentSizeShrinks() {
- var size by mutableStateOf(itemSizeDp * 3)
- rule.setContent {
- LazyGrid(1, maxSize = size) { items(listOf(0, 1, 2), key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { size = itemSizeDp * 2 }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- fraction = fraction
- )
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- }
-
- @Test
- fun noAnimationWhenParentSizeExpands() {
- var size by mutableStateOf(itemSizeDp * 2)
- rule.setContent {
- LazyGrid(1, maxSize = size) { items(listOf(0, 1, 2), key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { size = itemSizeDp * 3 }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun scrollIsAffectingItemsMovingWithinViewport() {
- var list by mutableStateOf(listOf(0, 1, 2, 3))
- val scrollDelta = spacing
- rule.setContent {
- LazyGrid(1, maxSize = itemSizeDp * 2) { items(list, key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { list = listOf(0, 2, 1, 3) }
-
- onAnimationFrame { fraction ->
- if (fraction == 0f) {
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- 2 to AxisOffset(0f, itemSize * 2),
- fraction = fraction
- )
- rule.runOnUiThread { runBlocking { state.scrollBy(scrollDelta) } }
- }
- assertPositions(
- 0 to AxisOffset(0f, -scrollDelta),
- 1 to AxisOffset(0f, itemSize - scrollDelta + itemSize * fraction),
- 2 to AxisOffset(0f, itemSize * 2 - scrollDelta - itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun scrollIsNotAffectingItemMovingToTheBottomOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val scrollDelta = spacing
- val containerSizeDp = itemSizeDp * 2
- val containerSize = itemSize * 2
- rule.setContent {
- LazyGrid(1, maxSize = containerSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { list = listOf(0, 4, 2, 3, 1) }
-
- onAnimationFrame { fraction ->
- if (fraction == 0f) {
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- fraction = fraction
- )
- rule.runOnUiThread { runBlocking { state.scrollBy(scrollDelta) } }
- }
- assertPositions(
- 0 to AxisOffset(0f, -scrollDelta),
- 1 to AxisOffset(0f, itemSize + (containerSize - itemSize) * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun scrollIsNotAffectingItemMovingToTheTopOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val scrollDelta = -spacing
- val containerSizeDp = itemSizeDp * 2
- rule.setContent {
- LazyGrid(1, maxSize = containerSizeDp, startIndex = 2) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { list = listOf(3, 0, 1, 2, 4) }
-
- onAnimationFrame { fraction ->
- if (fraction == 0f) {
- assertPositions(
- 2 to AxisOffset(0f, 0f),
- 3 to AxisOffset(0f, itemSize),
- fraction = fraction
- )
- rule.runOnUiThread { runBlocking { state.scrollBy(scrollDelta) } }
- }
- assertPositions(
- 2 to AxisOffset(0f, -scrollDelta),
- 3 to AxisOffset(0f, itemSize - (itemSize * 2 * fraction)),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun afterScrollingEnoughToReachNewPositionScrollDeltasStartAffectingPosition() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val containerSizeDp = itemSizeDp * 2
- val scrollDelta = spacing
- rule.setContent {
- LazyGrid(1, maxSize = containerSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { list = listOf(0, 4, 2, 3, 1) }
-
- onAnimationFrame { fraction ->
- if (fraction == 0f) {
- assertPositions(
- 0 to AxisOffset(0f, 0f),
- 1 to AxisOffset(0f, itemSize),
- fraction = fraction
- )
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize * 2) } }
- assertPositions(
- 2 to AxisOffset(0f, 0f),
- 3 to AxisOffset(0f, itemSize),
- // after the first scroll the new position of item 1 is still not reached
- // so the target didn't change, we still aim to end right after the bounds
- 1 to AxisOffset(0f, itemSize),
- fraction = fraction
- )
- rule.runOnUiThread { runBlocking { state.scrollBy(scrollDelta) } }
- assertPositions(
- 2 to AxisOffset(0f, 0f - scrollDelta),
- 3 to AxisOffset(0f, itemSize - scrollDelta),
- // after the second scroll the item 1 is visible, so we know its new target
- // position. the animation is now targeting the real end position and now
- // we are reacting on the scroll deltas
- 1 to AxisOffset(0f, itemSize - scrollDelta),
- fraction = fraction
- )
- }
- assertPositions(
- 2 to AxisOffset(0f, -scrollDelta),
- 3 to AxisOffset(0f, itemSize - scrollDelta),
- 1 to AxisOffset(0f, itemSize - scrollDelta + itemSize * fraction),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun interruptedSizeChange() {
- var item0Size by mutableStateOf(itemSizeDp)
- val animSpec = spring(visibilityThreshold = IntOffset.VisibilityThreshold)
- rule.setContent {
- LazyGrid(cells = 1) {
- items(2, key = { it }) {
- Item(it, if (it == 0) item0Size else itemSizeDp, animSpec = animSpec)
- }
- }
- }
-
- rule.runOnUiThread { item0Size = itemSize2Dp }
-
- rule.waitForIdle()
- rule.mainClock.advanceTimeByFrame()
- onAnimationFrame(duration = FrameDuration) { fraction ->
- if (fraction == 0f) {
- assertPositions(0 to AxisOffset(0f, 0f), 1 to AxisOffset(0f, itemSize))
- } else {
- assertThat(fraction).isEqualTo(1f)
- val valueAfterOneFrame =
- animSpec.getValueAtFrame(1, from = itemSize, to = itemSize2)
- assertPositions(0 to AxisOffset(0f, 0f), 1 to AxisOffset(0f, valueAfterOneFrame))
- }
- }
-
- rule.runOnUiThread { item0Size = 0.dp }
-
- rule.waitForIdle()
- val startValue = animSpec.getValueAtFrame(2, from = itemSize, to = itemSize2)
- val startVelocity = animSpec.getVelocityAtFrame(2, from = itemSize, to = itemSize2)
- onAnimationFrame(duration = FrameDuration) { fraction ->
- if (fraction == 0f) {
- assertPositions(0 to AxisOffset(0f, 0f), 1 to AxisOffset(0f, startValue))
- } else {
- assertThat(fraction).isEqualTo(1f)
- val valueAfterThreeFrames =
- animSpec.getValueAtFrame(
- 1,
- from = startValue,
- to = 0f,
- initialVelocity = startVelocity
- )
- assertPositions(0 to AxisOffset(0f, 0f), 1 to AxisOffset(0f, valueAfterThreeFrames))
- }
- }
- }
-
- internal fun SpringSpec<IntOffset>.getVelocityAtFrame(
- frameCount: Int,
- from: Float,
- to: Float,
- initialVelocity: IntOffset = IntOffset.Zero
- ): IntOffset {
- val frameInNanos = TimeUnit.MILLISECONDS.toNanos(FrameDuration)
- val vectorized = vectorize(converter = IntOffset.VectorConverter)
- return IntOffset.VectorConverter.convertFromVector(
- vectorized.getVelocityFromNanos(
- playTimeNanos = frameInNanos * frameCount,
- initialValue =
- IntOffset.VectorConverter.convertToVector(IntOffset(0, from.toInt())),
- targetValue = IntOffset.VectorConverter.convertToVector(IntOffset(0, to.toInt())),
- initialVelocity = IntOffset.VectorConverter.convertToVector(initialVelocity)
- )
- )
- }
-
- internal fun SpringSpec<IntOffset>.getValueAtFrame(
- frameCount: Int,
- from: Float,
- to: Float,
- initialVelocity: IntOffset = IntOffset.Zero
- ): Float {
- val frameInNanos = TimeUnit.MILLISECONDS.toNanos(FrameDuration)
- val vectorized = vectorize(converter = IntOffset.VectorConverter)
- return IntOffset.VectorConverter.convertFromVector(
- vectorized.getValueFromNanos(
- playTimeNanos = frameInNanos * frameCount,
- initialValue =
- IntOffset.VectorConverter.convertToVector(IntOffset(0, from.toInt())),
- targetValue =
- IntOffset.VectorConverter.convertToVector(IntOffset(0, to.toInt())),
- initialVelocity = IntOffset.VectorConverter.convertToVector(initialVelocity)
- )
- )
- .y
- .toFloat()
- }
-
- private fun AxisOffset(crossAxis: Float, mainAxis: Float) =
- if (isVertical) Offset(crossAxis, mainAxis) else Offset(mainAxis, crossAxis)
-
- private val Offset.mainAxis: Float
- get() = if (isVertical) y else x
-
- private fun assertPositions(
- vararg expected: Pair<Any, Offset>,
- crossAxis: List<Pair<Any, Float>>? = null,
- fraction: Float? = null,
- autoReverse: Boolean = reverseLayout
- ) {
- val roundedExpected = expected.map { it.first to it.second.round() }
- val actualBounds =
- rule
- .onAllNodes(NodesWithTagMatcher)
- .fetchSemanticsNodes()
- .associateBy(
- keySelector = { it.config[SemanticsProperties.TestTag] },
- valueTransform = { IntRect(it.positionInRoot.round(), it.size) }
- )
- val actualPositions =
- expected.map { it.first to actualBounds.getValue(it.first.toString()).topLeft }
- val subject =
- if (fraction == null) {
- assertThat(actualPositions)
- } else {
- assertWithMessage("Fraction=$fraction").that(actualPositions)
- }
- subject.isEqualTo(
- roundedExpected.let { list ->
- if (!autoReverse) {
- list
- } else {
- val containerSize = actualBounds.getValue(ContainerTag).size
- list.map {
- val itemSize = actualBounds.getValue(it.first.toString()).size
- it.first to
- IntOffset(
- if (isVertical) {
- it.second.x
- } else {
- containerSize.width - itemSize.width - it.second.x
- },
- if (!isVertical) {
- it.second.y
- } else {
- containerSize.height - itemSize.height - it.second.y
- }
- )
- }
- }
- }
- )
- if (crossAxis != null) {
- val actualCross =
- expected.map {
- it.first to
- actualBounds.getValue(it.first.toString()).topLeft.let { offset ->
- if (isVertical) offset.x else offset.y
- }
- }
- assertWithMessage("CrossAxis" + if (fraction != null) "for fraction=$fraction" else "")
- .that(actualCross)
- .isEqualTo(crossAxis.map { it.first to it.second.roundToInt() })
- }
- }
-
- private fun assertLayoutInfoPositions(vararg offsets: Pair<Any, Offset>) {
- rule.runOnIdle {
- assertThat(visibleItemsOffsets).isEqualTo(offsets.map { it.first to it.second.round() })
- }
- }
-
- private val visibleItemsOffsets: List<Pair<Any, IntOffset>>
- get() = state.layoutInfo.visibleItemsInfo.map { it.key to it.offset }
-
- private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
- require(duration.mod(FrameDuration) == 0L)
- rule.waitForIdle()
- rule.mainClock.advanceTimeByFrame()
- var expectedTime = rule.mainClock.currentTime
- for (i in 0..duration step FrameDuration) {
- val fraction = i / duration.toFloat()
- onFrame(fraction)
- if (i < duration) {
- rule.mainClock.advanceTimeBy(FrameDuration)
- expectedTime += FrameDuration
- assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
- }
- }
- }
-
- @Composable
- private fun LazyGrid(
- cells: Int,
- arrangement: Arrangement.HorizontalOrVertical? = null,
- minSize: Dp = 0.dp,
- maxSize: Dp = containerSizeDp,
- startIndex: Int = 0,
- startPadding: Dp = 0.dp,
- endPadding: Dp = 0.dp,
- content: TvLazyGridScope.() -> Unit
- ) {
- state = rememberTvLazyGridState(startIndex)
- if (isVertical) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(cells),
- Modifier.requiredHeightIn(minSize, maxSize)
- .requiredWidth(itemSizeDp * cells)
- .testTag(ContainerTag),
- state = state,
- verticalArrangement =
- arrangement as? Arrangement.Vertical
- ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
- reverseLayout = reverseLayout,
- contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
- content = content
- )
- } else {
- TvLazyHorizontalGrid(
- TvGridCells.Fixed(cells),
- Modifier.requiredWidthIn(minSize, maxSize)
- .requiredHeight(itemSizeDp * cells)
- .testTag(ContainerTag),
- state = state,
- horizontalArrangement =
- arrangement as? Arrangement.Horizontal
- ?: if (!reverseLayout) Arrangement.Start else Arrangement.End,
- reverseLayout = reverseLayout,
- contentPadding = PaddingValues(start = startPadding, end = endPadding),
- content = content
- )
- }
- }
-
- @Composable
- private fun TvLazyGridItemScope.Item(
- tag: Int,
- size: Dp = itemSizeDp,
- animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
- ) {
- Box(
- if (animSpec != null) {
- Modifier.animateItemPlacement(animSpec)
- } else {
- Modifier
- }
- .then(
- if (isVertical) {
- Modifier.requiredHeight(size)
- } else {
- Modifier.requiredWidth(size)
- }
- )
- .testTag(tag.toString())
- )
- }
-
- private fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(
- expected: Dp
- ): SemanticsNodeInteraction {
- return if (isVertical) assertHeightIsEqualTo(expected) else assertWidthIsEqualTo(expected)
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params() =
- arrayOf(
- Config(isVertical = true, reverseLayout = false),
- Config(isVertical = false, reverseLayout = false),
- Config(isVertical = true, reverseLayout = true),
- Config(isVertical = false, reverseLayout = true),
- )
-
- class Config(val isVertical: Boolean, val reverseLayout: Boolean) {
- override fun toString() =
- (if (isVertical) "LazyVerticalGrid" else "LazyHorizontalGrid") +
- (if (reverseLayout) "(reverse)" else "")
- }
- }
-}
-
-private val FrameDuration = 16L
-private val Duration = 64L // 4 frames, so we get 0f, 0.25f, 0.5f, 0.75f and 1f fractions
-private val AnimSpec = tween<IntOffset>(Duration.toInt(), easing = LinearEasing)
-private val ContainerTag = "container"
-private val NodesWithTagMatcher =
- SemanticsMatcher("NodesWithTag") { it.config.contains(SemanticsProperties.TestTag) }
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
deleted file mode 100644
index 0d14bdb..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
+++ /dev/null
@@ -1,543 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.lazy.list.PlacementComparator
-import androidx.tv.foundation.lazy.list.TrackPlacedElement
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyGridBeyondBoundsTest(param: Param) {
-
- @get:Rule val rule = createComposeRule()
-
- // We need to wrap the inline class parameter in another class because Java can't instantiate
- // the inline class.
- class Param(
- val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
- val reverseLayout: Boolean,
- val layoutDirection: LayoutDirection,
- ) {
- override fun toString() =
- "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
- "reverseLayout=$reverseLayout " +
- "layoutDirection=$layoutDirection"
- }
-
- private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
- private val reverseLayout = param.reverseLayout
- private val layoutDirection = param.layoutDirection
- private val placedItems = sortedMapOf<Int, Rect>()
- private var beyondBoundsLayout: BeyondBoundsLayout? = null
- private lateinit var lazyGridState: TvLazyGridState
- private val placementComparator =
- PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters() = buildList {
- for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
- for (reverseLayout in listOf(false, true)) {
- for (layoutDirection in listOf(Ltr, Rtl)) {
- add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
- }
- }
- }
- }
- }
-
- @Test
- fun onlyOneVisibleItemIsPlaced() {
- // Arrange.
- rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
- items(100) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(0)
- assertThat(visibleItems).containsExactly(0)
- }
- }
-
- @Test
- fun onlyTwoVisibleItemsArePlaced() {
- // Arrange.
- rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
- items(100) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(0, 1)
- assertThat(visibleItems).containsExactly(0, 1)
- }
- }
-
- @Test
- fun onlyThreeVisibleItemsArePlaced() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
- items(100) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(0, 1, 2)
- assertThat(visibleItems).containsExactly(0, 1, 2)
- }
- }
-
- @Test
- fun emptyLazyList_doesNotCrash() {
- // Arrange.
- var addItems by mutableStateOf(true)
- lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
- if (addItems) {
- item {
- Box(
- Modifier.modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- }
- }
- rule.runOnIdle {
- beyondBoundsLayoutRef = beyondBoundsLayout!!
- addItems = false
- }
-
- // Act.
- val hasMoreContent =
- rule.runOnIdle {
- beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) { hasMoreContent }
- }
-
- // Assert.
- rule.runOnIdle { assertThat(hasMoreContent).isFalse() }
- }
-
- @Test
- fun oneExtraItemBeyondVisibleBounds() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp()).trackPlaced(5).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
- }
- assertThat(visibleItems).containsExactly(5, 6, 7)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Just return true so that we stop as soon as we run this once.
- // This should result in one extra item being added.
- true
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
-
- @Test
- fun oneExtraItemBeyondVisibleBounds_multipleCells() {
- val itemSize = 50
- val itemSizeDp = itemSize.toDp()
- // Arrange.
- rule.setLazyContent(cells = 2, size = itemSizeDp * 3, firstVisibleItem = 10) {
- // item | item | x5
- // item | local | x1
- // item | item | x5
- items(11) { index -> Box(Modifier.size(itemSizeDp).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(itemSizeDp).trackPlaced(11).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(10) { index -> Box(Modifier.size(itemSizeDp).trackPlaced(index + 12)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(9, 10, 11, 12, 13, 14, 15)
- } else {
- assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15, 16)
- }
- assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Just return true so that we stop as soon as we run this once.
- // This should result in one extra item being added.
- true
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(10, 11, 12, 13, 14, 15)
- assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
- }
- }
-
- @Test
- fun twoExtraItemsBeyondVisibleBounds() {
- // Arrange.
- var extraItemCount = 2
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp()).trackPlaced(5).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- if (--extraItemCount > 0) {
- // Return null to continue the search.
- null
- } else {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
- }
- assertThat(visibleItems).containsExactly(5, 6, 7)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Return true to stop the search.
- true
- }
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
-
- @Test
- fun allBeyondBoundsItemsInSpecifiedDirection() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp())
- .modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- .trackPlaced(5)
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- if (hasMoreContent) {
- // Just return null so that we keep adding more items till we reach the end.
- null
- } else {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
- }
- assertThat(visibleItems).containsExactly(5, 6, 7)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Return true to end the search.
- true
- }
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle { assertThat(placedItems.keys).containsExactly(5, 6, 7) }
- }
-
- @Test
- fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
- // Arrange.
- var beyondBoundsLayoutCount = 0
- rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp()).trackPlaced(5).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- beyondBoundsLayoutCount++
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Above,
- Below -> {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- Before,
- After -> {
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
- }
- // Just return true so that we stop as soon as we run this once.
- // This should result in one extra item being added.
- true
- }
- }
-
- rule.runOnIdle {
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Above,
- Below -> {
- assertThat(beyondBoundsLayoutCount).isEqualTo(0)
- }
- Before,
- After -> {
- assertThat(beyondBoundsLayoutCount).isEqualTo(1)
-
- // Assert that the beyond bounds items are removed.
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- else -> error("Unsupported BeyondBoundsLayoutDirection")
- }
- }
- }
-
- @Test
- fun returningNullDoesNotCauseInfiniteLoop() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp())
- .modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- .trackPlaced(5)
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- var count = 0
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- // Assert that we don't keep iterating when there is no ending condition.
- assertThat(count++).isLessThan(lazyGridState.layoutInfo.totalItemsCount)
- // Always return null to continue the search.
- null
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
-
- private fun ComposeContentTestRule.setLazyContent(
- size: Dp,
- firstVisibleItem: Int,
- cells: Int = 1,
- content: TvLazyGridScope.() -> Unit
- ) {
- setContent {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- lazyGridState = rememberTvLazyGridState(firstVisibleItem)
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Before,
- After ->
- TvLazyHorizontalGrid(
- rows = TvGridCells.Fixed(cells),
- modifier = Modifier.size(size),
- state = lazyGridState,
- reverseLayout = reverseLayout,
- content = content
- )
- Above,
- Below ->
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(cells),
- modifier = Modifier.size(size),
- state = lazyGridState,
- reverseLayout = reverseLayout,
- content = content
- )
- else -> unsupportedDirection()
- }
- }
- }
- }
-
- private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
- size: Dp,
- firstVisibleItem: Int,
- content: TvLazyGridScope.() -> Unit
- ) {
- setContent {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- lazyGridState = rememberTvLazyGridState(firstVisibleItem)
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Before,
- After ->
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(size),
- state = lazyGridState,
- reverseLayout = reverseLayout,
- content = content
- )
- Above,
- Below ->
- TvLazyHorizontalGrid(
- rows = TvGridCells.Fixed(1),
- modifier = Modifier.size(size),
- state = lazyGridState,
- reverseLayout = reverseLayout,
- content = content
- )
- else -> unsupportedDirection()
- }
- }
- }
- }
-
- private fun Int.toDp(): Dp = with(rule.density) { toDp() }
-
- private val visibleItems: List<Int>
- get() = lazyGridState.layoutInfo.visibleItemsInfo.map { it.index }
-
- private fun expectedExtraItemsBeforeVisibleBounds() =
- when (beyondBoundsLayoutDirection) {
- Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
- Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
- Above -> !reverseLayout
- Below -> reverseLayout
- After -> false
- Before -> true
- else -> error("Unsupported BeyondBoundsDirection")
- }
-
- private fun unsupportedDirection(): Nothing =
- error("Lazy list does not support beyond bounds layout for the specified direction")
-
- private fun Modifier.trackPlaced(index: Int): Modifier =
- this then TrackPlacedElement(index, placedItems)
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
deleted file mode 100644
index 783f649..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
+++ /dev/null
@@ -1,592 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.LocalPinnableContainer
-import androidx.compose.ui.layout.PinnableContainer
-import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.lazy.list.assertIsNotPlaced
-import androidx.tv.foundation.lazy.list.assertIsPlaced
-import com.google.common.truth.Truth.assertThat
-import kotlin.collections.removeFirst as removeFirstKt
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-@MediumTest
-class LazyGridPinnableContainerTest {
-
- @get:Rule val rule = createComposeRule()
-
- private var pinnableContainer: PinnableContainer? = null
-
- private val itemSizePx = 10
- private var itemSize = Dp.Unspecified
-
- private val composed = mutableSetOf<Int>()
-
- @Before
- fun setup() {
- itemSize = with(rule.density) { itemSizePx.toDp() }
- }
-
- @Composable
- fun Item(index: Int) {
- Box(Modifier.size(itemSize).testTag("$index"))
- DisposableEffect(index) {
- composed.add(index)
- onDispose { composed.remove(index) }
- }
- }
-
- @Test
- fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
- val state = TvLazyGridState()
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(1)
- runBlocking { state.scrollToItem(3) }
- }
-
- rule.waitUntil {
- // not visible items were disposed
- !composed.contains(0)
- }
-
- rule.runOnIdle {
- // item 1 is still pinned
- assertThat(composed).contains(1)
- }
-
- rule.onNodeWithTag("1").assertExists().assertIsNotDisplayed().assertIsPlaced()
- }
-
- @Test
- fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
- val state = TvLazyGridState()
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(4) } }
-
- rule.waitUntil {
- // not visible items were disposed
- !composed.contains(0)
- }
-
- rule.runOnIdle {
- assertThat(composed).doesNotContain(0)
- assertThat(composed).contains(1)
- assertThat(composed).doesNotContain(2)
- assertThat(composed).doesNotContain(3)
- assertThat(composed).contains(4)
- }
- }
-
- @Test
- fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
- val state = TvLazyGridState()
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 4) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(4) } }
-
- rule.waitUntil {
- // wait for not visible items to be disposed
- !composed.contains(1)
- }
-
- rule.runOnIdle {
- requireNotNull(pinnableContainer).pin()
- assertThat(composed).contains(5)
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(0) } }
-
- rule.waitUntil {
- // wait for not visible items to be disposed
- !composed.contains(5)
- }
-
- rule.runOnIdle {
- assertThat(composed).contains(0)
- assertThat(composed).contains(1)
- assertThat(composed).doesNotContain(2)
- assertThat(composed).doesNotContain(3)
- assertThat(composed).contains(4)
- assertThat(composed).doesNotContain(5)
- }
- }
-
- @Test
- fun pinnedItemCanBeUnpinned() {
- val state = TvLazyGridState()
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- val handle = rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(3) } }
-
- rule.waitUntil {
- // wait for not visible items to be disposed
- !composed.contains(0)
- }
-
- rule.runOnIdle { handle.release() }
-
- rule.waitUntil {
- // wait for unpinned item to be disposed
- !composed.contains(1)
- }
-
- rule.onNodeWithTag("1").assertIsNotPlaced()
- }
-
- @Test
- fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
- val state = TvLazyGridState()
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 3),
- state = state
- ) {
- items(list, key = { it }) { index ->
- if (index == 2) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle {
- assertThat(composed).containsExactly(0, 1, 2)
- requireNotNull(pinnableContainer).pin()
- }
-
- rule.runOnIdle { list = listOf(0, 3, 4, 1, 2) }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(1)
- }
-
- rule.runOnIdle {
- assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
- }
-
- rule.onNodeWithTag("2").assertIsPlaced()
- }
-
- @Test
- fun unpinnedWhenTvLazyGridStateChanges() {
- var state by mutableStateOf(TvLazyGridState(firstVisibleItemIndex = 2))
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 2) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(3)
- runBlocking { state.scrollToItem(0) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(3)
- }
-
- rule.runOnIdle {
- assertThat(composed).contains(2)
- state = TvLazyGridState()
- }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(2)
- }
-
- rule.onNodeWithTag("2").assertIsNotPlaced()
- }
-
- @Test
- fun pinAfterTvLazyGridStateChange() {
- var state by mutableStateOf(TvLazyGridState())
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 0) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { state = TvLazyGridState() }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(1)
- runBlocking { state.scrollToItem(2) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(1)
- }
-
- rule.runOnIdle { assertThat(composed).contains(0) }
- }
-
- @Test
- fun itemsArePinnedBasedOnGlobalIndexes() {
- val state = TvLazyGridState(firstVisibleItemIndex = 3)
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- repeat(100) { index ->
- item {
- if (index == 3) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(4)
- runBlocking { state.scrollToItem(6) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(4)
- }
-
- rule.runOnIdle { assertThat(composed).contains(3) }
-
- rule.onNodeWithTag("3").assertExists().assertIsNotDisplayed().assertIsPlaced()
- }
-
- @Test
- fun pinnedItemIsRemovedWhenNotVisible() {
- val state = TvLazyGridState(3)
- var itemCount by mutableStateOf(10)
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(itemCount) { index ->
- if (index == 3) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle {
- requireNotNull(pinnableContainer).pin()
- assertThat(composed).contains(4)
- runBlocking { state.scrollToItem(0) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(4)
- }
-
- rule.runOnIdle { itemCount = 3 }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(3)
- }
-
- rule.onNodeWithTag("3").assertIsNotPlaced()
- }
-
- @Test
- fun pinnedItemIsRemovedWhenVisible() {
- val state = TvLazyGridState(0)
- var items by mutableStateOf(listOf(0, 1, 2))
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(items) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle { items = listOf(0, 2) }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(1)
- }
-
- rule.onNodeWithTag("1").assertIsNotPlaced()
- }
-
- @Test
- fun pinnedMultipleTimes() {
- val state = TvLazyGridState(0)
- // Arrange.
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.size(itemSize * 2),
- state = state
- ) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- val handles = mutableListOf<PinnedHandle>()
- rule.runOnIdle {
- handles.add(requireNotNull(pinnableContainer).pin())
- handles.add(requireNotNull(pinnableContainer).pin())
- }
-
- rule.runOnIdle {
- // pinned 3 times in total
- handles.add(requireNotNull(pinnableContainer).pin())
- assertThat(composed).contains(0)
- runBlocking { state.scrollToItem(3) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(0)
- }
-
- while (handles.isNotEmpty()) {
- rule.runOnIdle {
- assertThat(composed).contains(1)
- handles.removeFirstKt().release()
- }
- }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(1)
- }
- }
-
- @Test
- fun pinningIsPropagatedToParentContainer() {
- var parentPinned = false
- val parentContainer =
- object : PinnableContainer {
- override fun pin(): PinnedHandle {
- parentPinned = true
- return PinnedHandle { parentPinned = false }
- }
- }
- // Arrange.
- rule.setContent {
- CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
- TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
- item {
- pinnableContainer = LocalPinnableContainer.current
- Box(Modifier.size(itemSize))
- }
- }
- }
- }
-
- val handle = rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(parentPinned).isTrue()
- handle.release()
- }
-
- rule.runOnIdle { assertThat(parentPinned).isFalse() }
- }
-
- @Test
- fun parentContainerChange_pinningIsMaintained() {
- var parent1Pinned = false
- val parent1Container =
- object : PinnableContainer {
- override fun pin(): PinnedHandle {
- parent1Pinned = true
- return PinnedHandle { parent1Pinned = false }
- }
- }
- var parent2Pinned = false
- val parent2Container =
- object : PinnableContainer {
- override fun pin(): PinnedHandle {
- parent2Pinned = true
- return PinnedHandle { parent2Pinned = false }
- }
- }
- var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
- // Arrange.
- rule.setContent {
- CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
- TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
- item {
- pinnableContainer = LocalPinnableContainer.current
- Box(Modifier.size(itemSize))
- }
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(parent1Pinned).isTrue()
- assertThat(parent2Pinned).isFalse()
- parentContainer = parent2Container
- }
-
- rule.runOnIdle {
- assertThat(parent1Pinned).isFalse()
- assertThat(parent2Pinned).isTrue()
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridPrefetcherTest.kt
deleted file mode 100644
index 593fd74..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridPrefetcherTest.kt
+++ /dev/null
@@ -1,361 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.Remeasurement
-import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyGridPrefetcherTest(orientation: Orientation) :
- BaseLazyGridTestWithOrientation(orientation) {
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters(): Array<Any> =
- arrayOf(
- Orientation.Vertical,
- Orientation.Horizontal,
- )
- }
-
- val itemsSizePx = 30
- val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
-
- lateinit var state: TvLazyGridState
-
- @Test
- fun notPrefetchingForwardInitially() {
- composeGrid()
-
- rule.onNodeWithTag("4").assertDoesNotExist()
- }
-
- @Test
- fun notPrefetchingBackwardInitially() {
- composeGrid(firstItem = 4)
-
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAfterSmallScroll() {
- composeGrid()
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(4)
- waitForPrefetch(5)
-
- rule.onNodeWithTag("4").assertExists()
- rule.onNodeWithTag("5").assertExists()
- rule.onNodeWithTag("6").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingBackwardAfterSmallScroll() {
- composeGrid(firstItem = 4, itemOffset = 10)
-
- rule.runOnIdle { runBlocking { state.scrollBy(-5f) } }
-
- waitForPrefetch(2)
- waitForPrefetch(3)
-
- rule.onNodeWithTag("2").assertExists()
- rule.onNodeWithTag("3").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAndBackward() {
- composeGrid(firstItem = 2)
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(6)
- waitForPrefetch(7)
-
- rule.onNodeWithTag("6").assertExists()
- rule.onNodeWithTag("7").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(-2f)
- state.scrollBy(-1f)
- }
- }
-
- waitForPrefetch(0)
- waitForPrefetch(1)
-
- rule.onNodeWithTag("0").assertExists()
- rule.onNodeWithTag("1").assertExists()
- rule.onNodeWithTag("6").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardTwice() {
- composeGrid()
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(4)
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(itemsSizePx / 2f)
- state.scrollBy(itemsSizePx / 2f)
- }
- }
-
- waitForPrefetch(6)
-
- rule.onNodeWithTag("4").assertIsDisplayed()
- rule.onNodeWithTag("6").assertExists()
- rule.onNodeWithTag("8").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingBackwardTwice() {
- composeGrid(firstItem = 8)
-
- rule.runOnIdle { runBlocking { state.scrollBy(-5f) } }
-
- waitForPrefetch(4)
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(-itemsSizePx / 2f)
- state.scrollBy(-itemsSizePx / 2f)
- }
- }
-
- waitForPrefetch(2)
-
- rule.onNodeWithTag("4").assertIsDisplayed()
- rule.onNodeWithTag("6").assertIsDisplayed()
- rule.onNodeWithTag("2").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAndBackwardReverseLayout() {
- composeGrid(firstItem = 2, reverseLayout = true)
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(6)
- waitForPrefetch(7)
-
- rule.onNodeWithTag("6").assertExists()
- rule.onNodeWithTag("7").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(-2f)
- state.scrollBy(-1f)
- }
- }
-
- waitForPrefetch(0)
- waitForPrefetch(1)
-
- rule.onNodeWithTag("0").assertExists()
- rule.onNodeWithTag("1").assertExists()
- rule.onNodeWithTag("6").assertDoesNotExist()
- rule.onNodeWithTag("7").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAndBackwardWithContentPadding() {
- val halfItemSize = itemsSizeDp / 2f
- composeGrid(
- firstItem = 4,
- itemOffset = 5,
- contentPadding = PaddingValues(mainAxis = halfItemSize)
- )
-
- rule.onNodeWithTag("2").assertIsDisplayed()
- rule.onNodeWithTag("4").assertIsDisplayed()
- rule.onNodeWithTag("6").assertIsDisplayed()
- rule.onNodeWithTag("0").assertDoesNotExist()
- rule.onNodeWithTag("8").assertDoesNotExist()
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(6)
-
- rule.onNodeWithTag("8").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
-
- rule.runOnIdle { runBlocking { state.scrollBy(-2f) } }
-
- waitForPrefetch(0)
-
- rule.onNodeWithTag("0").assertExists()
- }
-
- @Test
- fun disposingWhilePrefetchingScheduled() {
- var emit = true
- lateinit var remeasure: Remeasurement
- rule.setContent {
- SubcomposeLayout(
- modifier =
- object : RemeasurementModifier {
- override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
- remeasure = remeasurement
- }
- }
- ) { constraints ->
- val placeable =
- if (emit) {
- subcompose(Unit) {
- state = rememberTvLazyGridState()
- LazyGrid(
- 2,
- Modifier.mainAxisSize(itemsSizeDp * 1.5f),
- state,
- ) {
- items(1000) { Spacer(Modifier.mainAxisSize(itemsSizeDp)) }
- }
- }
- .first()
- .measure(constraints)
- } else {
- null
- }
- layout(constraints.maxWidth, constraints.maxHeight) { placeable?.place(0, 0) }
- }
- }
-
- rule.runOnIdle {
- // this will schedule the prefetching
- runBlocking(AutoTestFrameClock()) { state.scrollBy(itemsSizePx.toFloat()) }
- // then we synchronously dispose LazyColumn
- emit = false
- remeasure.forceRemeasure()
- }
-
- rule.runOnIdle {}
- }
-
- @Test
- fun scrollingByListSizeCancelsPreviousPrefetch() {
- composeGrid()
-
- // now we have items 0-3 visible
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) {
- // this will move the viewport so items 2-5 are visible
- // and schedule a prefetching for 6-7
- state.scrollBy(itemsSizePx.toFloat())
-
- // move viewport by screen size to items 8-11, so item 6 is just behind
- // the first visible item
- state.scrollBy(itemsSizePx * 3f)
-
- // move scroll further to items 10-13, so item 6 is reused
- state.scrollBy(itemsSizePx.toFloat())
- }
- }
-
- waitForPrefetch(13)
-
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) {
- // scroll again to ensure item 6 was dropped
- state.scrollBy(itemsSizePx * 100f)
- }
- }
-
- rule.runOnIdle { assertThat(activeNodes).doesNotContain(6) }
- }
-
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil { activeNodes.contains(index) && activeMeasuredNodes.contains(index) }
- }
-
- private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
-
- private fun composeGrid(
- firstItem: Int = 0,
- itemOffset: Int = 0,
- reverseLayout: Boolean = false,
- contentPadding: PaddingValues = PaddingValues(0.dp)
- ) {
- rule.setContent {
- state =
- rememberTvLazyGridState(
- initialFirstVisibleItemIndex = firstItem,
- initialFirstVisibleItemScrollOffset = itemOffset
- )
- LazyGrid(
- 2,
- Modifier.mainAxisSize(itemsSizeDp * 1.5f),
- state,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding
- ) {
- items(100) {
- DisposableEffect(it) {
- activeNodes.add(it)
- onDispose {
- activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
- }
- }
- Spacer(
- Modifier.mainAxisSize(itemsSizeDp).testTag("$it").layout {
- measurable,
- constraints ->
- val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- )
- }
- }
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
deleted file mode 100644
index 78b3e02..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
+++ /dev/null
@@ -1,424 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyGridSlotsReuseTest {
-
- @get:Rule val rule = createComposeRule()
-
- val itemsSizePx = 30f
- val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
-
- @Test
- fun scroll1ItemScrolledOffItemIsKeptForReuse() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
-
- val id0 = rule.onNodeWithTag("0").semanticsId()
- rule.onNodeWithTag("0").assertIsDisplayed()
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onNodeWithTag("1").assertIsDisplayed()
- }
-
- @Test
- fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
- // Semantics IDs must be fetched before scrolling.
- val id0 = rule.onNodeWithTag("0").semanticsId()
- val id1 = rule.onNodeWithTag("1").semanticsId()
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.runOnIdle { runBlocking { state.scrollToItem(2) } }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id1)
- rule.onNodeWithTag("2").assertIsDisplayed()
- }
-
- @Test
- fun checkMaxItemsKeptForReuse() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
- state
- ) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
- // Semantics IDs must be fetched before scrolling.
- val deactivatedIds = mutableListOf<Int>()
- repeat(DefaultMaxItemsToRetain) {
- deactivatedIds.add(rule.onNodeWithTag("$it").semanticsId())
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(DefaultMaxItemsToRetain + 1) } }
-
- deactivatedIds.fastForEach {
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(it)
- }
- rule.onNodeWithTag("$DefaultMaxItemsToRetain").assertDoesNotExist()
- rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}").assertIsDisplayed()
- }
-
- @Test
- fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
-
- val id0 = rule.onNodeWithTag("0").semanticsId()
- val id1 = rule.onNodeWithTag("1").semanticsId()
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.runOnIdle {
- runBlocking {
- // after this step 0 and 1 are in reusable buffer
- state.scrollToItem(2)
-
- // this step requires one item and will take the last item from the buffer - item
- // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
- state.scrollToItem(3)
- }
- }
-
- // recycled
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id1)
-
- // visible
- rule.onNodeWithTag("3").assertIsDisplayed()
- rule.onNodeWithTag("4").assertIsDisplayed()
- }
-
- @Test
- fun doMultipleScrollsOneByOne() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(1) // buffer is [0]
- state.scrollToItem(2) // 0 used, buffer is [1]
- }
- }
-
- // 3 should be visible at this point, so save its ID to check later
- val id3 = rule.onNodeWithTag("3").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(3) // 1 used, buffer is [2]
- state.scrollToItem(4) // 2 used, buffer is [3]
- }
- }
-
- // recycled
- rule.onNodeWithTag("0").assertDoesNotExist()
- rule.onNodeWithTag("1").assertDoesNotExist()
- rule.onNodeWithTag("2").assertDoesNotExist()
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id3)
-
- // visible
- rule.onNodeWithTag("4").assertIsDisplayed()
- rule.onNodeWithTag("5").assertIsDisplayed()
- }
-
- @Test
- fun scrollBackwardOnce() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState(10)
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
-
- val id10 = rule.onNodeWithTag("10").semanticsId()
- val id11 = rule.onNodeWithTag("11").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(8) // buffer is [10, 11]
- }
- }
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id10)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id11)
-
- // visible
- rule.onNodeWithTag("8").assertIsDisplayed()
- rule.onNodeWithTag("9").assertIsDisplayed()
- }
-
- @Test
- fun scrollBackwardOneByOne() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState(10)
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) { Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it")) }
- }
- }
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(9) // buffer is [11]
- state.scrollToItem(7) // 11 reused, buffer is [9]
- }
- }
- // 8 should be visible at this point, so save its ID to check later
- val id8 = rule.onNodeWithTag("8").semanticsId()
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(6) // 9 reused, buffer is [8]
- }
- }
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id8)
-
- // visible
- rule.onNodeWithTag("6").assertIsDisplayed()
- rule.onNodeWithTag("7").assertIsDisplayed()
- }
-
- @Test
- fun scrollingBackReusesTheSameSlot() {
- lateinit var state: TvLazyGridState
- var counter0 = 0
- var counter1 = 0
-
- val measureCountModifier0 =
- Modifier.layout { measurable, constraints ->
- counter0++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(IntOffset.Zero) }
- }
-
- val measureCountModifier1 =
- Modifier.layout { measurable, constraints ->
- counter1++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(IntOffset.Zero) }
- }
-
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 1.5f), state) {
- items(100) {
- val modifier =
- when (it) {
- 0 -> measureCountModifier0
- 1 -> measureCountModifier1
- else -> Modifier
- }
- Spacer(Modifier.height(itemsSizeDp).testTag("$it").then(modifier))
- }
- }
- }
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(2) // buffer is [0, 1]
- }
- }
-
- // 2 and 3 should be visible at this point, so save its ID to check later
- val id2 = rule.onNodeWithTag("2").semanticsId()
- val id3 = rule.onNodeWithTag("3").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- counter0 = 0
- counter1 = 0
- state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
- }
- }
-
- rule.runOnIdle {
- Truth.assertWithMessage("Item 0 measured $counter0 times, expected 0.")
- .that(counter0)
- .isEqualTo(0)
- Truth.assertWithMessage("Item 1 measured $counter1 times, expected 0.")
- .that(counter1)
- .isEqualTo(0)
- }
-
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id2)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id3)
- }
-
- @Test
- fun differentContentTypes() {
- lateinit var state: TvLazyGridState
- val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
- val startOfType1 = DefaultMaxItemsToRetain + 1
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
- state
- ) {
- items(100, contentType = { if (it >= startOfType1) 1 else 0 }) {
- Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
- }
- }
- }
-
- val deactivatedIds = mutableListOf<Int>()
- for (i in 0 until visibleItemsCount) {
- deactivatedIds.add(rule.onNodeWithTag("$i").semanticsId())
- rule.onNodeWithTag("$i").assertIsDisplayed()
- }
- for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
- deactivatedIds.add(rule.onNodeWithTag("$i").fetchSemanticsNode().id)
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(visibleItemsCount) } }
-
- rule.onNodeWithTag("$visibleItemsCount").assertIsDisplayed()
-
- // [DefaultMaxItemsToRetain] items of type 0 are left for reuse and 7 items of type 1
- deactivatedIds.fastForEach {
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(it)
- }
-
- rule.onNodeWithTag("$DefaultMaxItemsToRetain").assertDoesNotExist()
- rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}").assertDoesNotExist()
- }
-
- @Test
- fun differentTypesFromDifferentItemCalls() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(itemsSizeDp * 2.5f), state) {
- val content =
- @Composable { tag: String ->
- Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
- }
- item(contentType = "not-to-reuse-0") { content("0") }
- item(contentType = "reuse") { content("1") }
- items(
- List(100) { it + 2 },
- contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }
- ) {
- content("$it")
- }
- }
- }
-
- val id0 = rule.onNodeWithTag("0").semanticsId()
- val id1 = rule.onNodeWithTag("1").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(2)
- // now items 0 and 1 are put into reusables
- }
- }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id1)
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(9)
- // item 10 should reuse slot 1
- }
- }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onNodeWithTag("1").assertDoesNotExist()
- rule.onNodeWithTag("9").assertIsDisplayed()
- rule.onNodeWithTag("10").assertIsDisplayed()
- rule.onNodeWithTag("11").assertIsDisplayed()
- }
-
- private fun SemanticsNode.assertLayoutDeactivatedById(id: Int) {
- children.fastForEach {
- if (it.id == id) {
- assert(it.layoutInfo.isDeactivated)
- }
- }
- }
-}
-
-private val DefaultMaxItemsToRetain = 7
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSpanTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSpanTest.kt
deleted file mode 100644
index 785f450..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSpanTest.kt
+++ /dev/null
@@ -1,320 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyGridSpanTest {
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun spans() {
- val columns = 4
- val columnWidth = with(rule.density) { 5.toDp() }
- val itemHeight = with(rule.density) { 10.toDp() }
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(columns),
- modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3)
- ) {
- items(
- count = 6,
- span = { index ->
- when (index) {
- 0 -> {
- Truth.assertThat(maxLineSpan).isEqualTo(4)
- Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
- TvGridItemSpan(3)
- }
- 1 -> {
- Truth.assertThat(maxLineSpan).isEqualTo(4)
- Truth.assertThat(maxCurrentLineSpan).isEqualTo(1)
- TvGridItemSpan(1)
- }
- 2 -> {
- Truth.assertThat(maxLineSpan).isEqualTo(4)
- Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
- TvGridItemSpan(1)
- }
- 3 -> {
- Truth.assertThat(maxLineSpan).isEqualTo(4)
- Truth.assertThat(maxCurrentLineSpan).isEqualTo(3)
- TvGridItemSpan(3)
- }
- 4 -> {
- Truth.assertThat(maxLineSpan).isEqualTo(4)
- Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
- TvGridItemSpan(1)
- }
- 5 -> {
- Truth.assertThat(maxLineSpan).isEqualTo(4)
- Truth.assertThat(maxCurrentLineSpan).isEqualTo(3)
- TvGridItemSpan(1)
- }
- else -> error("Out of index span queried")
- }
- },
- ) {
- Box(Modifier.height(itemHeight).testTag("$it"))
- }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(columnWidth * 3)
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(itemHeight)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("3")
- .assertTopPositionInRootIsEqualTo(itemHeight)
- .assertLeftPositionInRootIsEqualTo(columnWidth)
- rule
- .onNodeWithTag("4")
- .assertTopPositionInRootIsEqualTo(itemHeight * 2)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("5")
- .assertTopPositionInRootIsEqualTo(itemHeight * 2)
- .assertLeftPositionInRootIsEqualTo(columnWidth)
- }
-
- @Test
- fun spansWithHorizontalSpacing() {
- val columns = 4
- val columnWidth = with(rule.density) { 5.toDp() }
- val itemHeight = with(rule.density) { 10.toDp() }
- val spacing = with(rule.density) { 4.toDp() }
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(columns),
- modifier =
- Modifier.requiredSize(
- columnWidth * columns + spacing * (columns - 1),
- itemHeight
- ),
- horizontalArrangement = Arrangement.spacedBy(spacing)
- ) {
- items(
- count = 2,
- span = { index ->
- when (index) {
- 0 -> TvGridItemSpan(1)
- 1 -> TvGridItemSpan(3)
- else -> error("Out of index span queried")
- }
- }
- ) {
- Box(Modifier.height(itemHeight).testTag("$it"))
- }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(columnWidth)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(columnWidth + spacing)
- .assertWidthIsEqualTo(columnWidth * 3 + spacing * 2)
- }
-
- @Test
- fun spansMultipleBlocks() {
- val columns = 4
- val columnWidth = with(rule.density) { 5.toDp() }
- val itemHeight = with(rule.density) { 10.toDp() }
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(columns),
- modifier = Modifier.requiredSize(columnWidth * columns, itemHeight)
- ) {
- items(
- count = 1,
- span = { index ->
- when (index) {
- 0 -> TvGridItemSpan(1)
- else -> error("Out of index span queried")
- }
- }
- ) {
- Box(Modifier.height(itemHeight).testTag("0"))
- }
- item(
- span = {
- if (maxCurrentLineSpan != 3) error("Wrong maxSpan")
- TvGridItemSpan(2)
- }
- ) {
- Box(Modifier.height(itemHeight).testTag("1"))
- }
- items(
- count = 1,
- span = { index ->
- if (maxCurrentLineSpan != 1 || index != 0) {
- error("Wrong span calculation parameters")
- }
- TvGridItemSpan(1)
- }
- ) {
- if (it != 0) error("Wrong index")
- Box(Modifier.height(itemHeight).testTag("2"))
- }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(columnWidth)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(columnWidth)
- .assertWidthIsEqualTo(columnWidth * 2)
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(columnWidth * 3)
- .assertWidthIsEqualTo(columnWidth)
- }
-
- @Test
- fun spansLineBreak() {
- val columns = 4
- val columnWidth = with(rule.density) { 5.toDp() }
- val itemHeight = with(rule.density) { 10.toDp() }
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(columns),
- modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3)
- ) {
- item(
- span = {
- if (maxCurrentLineSpan != 4) error("Wrong maxSpan")
- TvGridItemSpan(3)
- }
- ) {
- Box(Modifier.height(itemHeight).testTag("0"))
- }
- items(
- count = 4,
- span = { index ->
- if (
- maxCurrentLineSpan !=
- when (index) {
- 0 -> 1
- 1 -> 2
- 2 -> 1
- 3 -> 2
- else -> error("Wrong index")
- }
- )
- error("Wrong maxSpan")
- TvGridItemSpan(listOf(2, 1, 2, 2)[index])
- }
- ) {
- Box(Modifier.height(itemHeight).testTag((it + 1).toString()))
- }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(columnWidth * 3)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(itemHeight)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(columnWidth * 2)
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(itemHeight)
- .assertLeftPositionInRootIsEqualTo(columnWidth * 2)
- .assertWidthIsEqualTo(columnWidth)
- rule
- .onNodeWithTag("3")
- .assertTopPositionInRootIsEqualTo(itemHeight * 2)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(columnWidth * 2)
- rule
- .onNodeWithTag("4")
- .assertTopPositionInRootIsEqualTo(itemHeight * 2)
- .assertLeftPositionInRootIsEqualTo(columnWidth * 2)
- .assertWidthIsEqualTo(columnWidth * 2)
- }
-
- @Test
- fun spansCalculationDoesntCrash() {
- // regression from b/222530458
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(2),
- state = state,
- modifier = Modifier.size(100.dp)
- ) {
- repeat(100) {
- item(span = { TvGridItemSpan(maxLineSpan) }) {
- Box(Modifier.fillMaxWidth().height(1.dp))
- }
- items(10) { Box(Modifier.fillMaxWidth().height(1.dp)) }
- }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(state.layoutInfo.totalItemsCount) } }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
deleted file mode 100644
index 04b4ad2..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
+++ /dev/null
@@ -1,1000 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import android.os.Build
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.assertPixels
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
-import com.google.common.collect.Range
-import com.google.common.truth.IntegerSubject
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyGridTest(private val orientation: Orientation) :
- BaseLazyGridTestWithOrientation(orientation) {
-
- @Suppress("PrivatePropertyName") private val LazyGridTag = "LazyGridTag"
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters(): Array<Any> =
- arrayOf(
- Orientation.Vertical,
- Orientation.Horizontal,
- )
- }
-
- @Test
- fun lazyGridShowsOneItem() {
- val itemTestTag = "itemTestTag"
-
- rule.setContent {
- LazyGrid(cells = 3) { item { Spacer(Modifier.size(10.dp).testTag(itemTestTag)) } }
- }
-
- rule.onNodeWithTag(itemTestTag).assertIsDisplayed()
- }
-
- @Test
- fun lazyGridShowsOneLine() {
- val items = (1..5).map { it.toString() }
-
- rule.setContent {
- LazyGrid(cells = 3, modifier = Modifier.axisSize(300.dp, 100.dp)) {
- items(items) { Spacer(Modifier.mainAxisSize(101.dp).testTag(it)) }
- }
- }
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertIsDisplayed()
-
- rule.onNodeWithTag("4").assertDoesNotExist()
-
- rule.onNodeWithTag("5").assertDoesNotExist()
- }
-
- @Test
- fun lazyGridShowsSecondLineOnScroll() {
- val items = (1..12).map { it.toString() }
-
- rule.setContentWithTestViewConfiguration {
- LazyGrid(cells = 3, modifier = Modifier.mainAxisSize(200.dp).testTag(LazyGridTag)) {
- items(items) { Box(Modifier.mainAxisSize(101.dp).testTag(it).focusable()) }
- }
- }
-
- rule.keyPress(2)
-
- rule.onNodeWithTag("4").assertIsDisplayed()
-
- rule.onNodeWithTag("5").assertIsDisplayed()
-
- rule.onNodeWithTag("6").assertIsDisplayed()
-
- rule.onNodeWithTag("10").assertIsNotDisplayed()
-
- rule.onNodeWithTag("11").assertIsNotDisplayed()
-
- rule.onNodeWithTag("12").assertIsNotDisplayed()
- }
-
- @Test
- fun lazyGridScrollHidesFirstLine() {
- val items = (1..9).map { it.toString() }
-
- rule.setContentWithTestViewConfiguration {
- LazyGrid(
- cells = 3,
- modifier = Modifier.mainAxisSize(200.dp).testTag(LazyGridTag),
- ) {
- items(items) { Spacer(Modifier.mainAxisSize(101.dp).testTag(it).focusable()) }
- }
- }
-
- rule.keyPress(3)
-
- rule.onNodeWithTag("1").assertIsNotDisplayed()
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.onNodeWithTag("4").assertIsDisplayed()
-
- rule.onNodeWithTag("5").assertIsDisplayed()
-
- rule.onNodeWithTag("6").assertIsDisplayed()
-
- rule.onNodeWithTag("7").assertIsDisplayed()
-
- rule.onNodeWithTag("8").assertIsDisplayed()
-
- rule.onNodeWithTag("9").assertIsDisplayed()
- }
-
- @Test
- fun adaptiveLazyGridFillsAllCrossAxisSize() {
- val items = (1..5).map { it.toString() }
-
- rule.setContent {
- LazyGrid(
- cells = TvGridCells.Adaptive(130.dp),
- modifier = Modifier.axisSize(300.dp, 100.dp)
- ) {
- items(items) { Spacer(Modifier.mainAxisSize(101.dp).testTag(it)) }
- }
- }
-
- rule.onNodeWithTag("1").assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("2").assertCrossAxisStartPositionInRootIsEqualTo(150.dp)
-
- rule.onNodeWithTag("3").assertDoesNotExist()
-
- rule.onNodeWithTag("4").assertDoesNotExist()
-
- rule.onNodeWithTag("5").assertDoesNotExist()
- }
-
- @Test
- fun adaptiveLazyGridAtLeastOneSlot() {
- val items = (1..3).map { it.toString() }
-
- rule.setContent {
- LazyGrid(
- cells = TvGridCells.Adaptive(301.dp),
- modifier = Modifier.axisSize(300.dp, 100.dp)
- ) {
- items(items) { Spacer(Modifier.mainAxisSize(101.dp).testTag(it)) }
- }
- }
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertDoesNotExist()
-
- rule.onNodeWithTag("3").assertDoesNotExist()
- }
-
- @Test
- fun adaptiveLazyGridAppliesHorizontalSpacings() {
- val items = (1..3).map { it.toString() }
-
- val spacing = with(rule.density) { 10.toDp() }
- val itemSize = with(rule.density) { 100.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = TvGridCells.Adaptive(itemSize),
- modifier = Modifier.axisSize(itemSize * 3 + spacing * 2, itemSize),
- crossAxisSpacedBy = spacing
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize + spacing)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 2)
- .assertCrossAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun adaptiveLazyGridAppliesHorizontalSpacingsWithContentPaddings() {
- val items = (1..3).map { it.toString() }
-
- val spacing = with(rule.density) { 8.toDp() }
- val itemSize = with(rule.density) { 40.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = TvGridCells.Adaptive(itemSize),
- modifier = Modifier.axisSize(itemSize * 3 + spacing * 4, itemSize),
- crossAxisSpacedBy = spacing,
- contentPadding = PaddingValues(crossAxis = spacing)
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize + spacing * 2)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 3)
- .assertCrossAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun adaptiveLazyGridAppliesVerticalSpacings() {
- val items = (1..3).map { it.toString() }
-
- val spacing = with(rule.density) { 4.toDp() }
- val itemSize = with(rule.density) { 32.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = TvGridCells.Adaptive(itemSize),
- modifier = Modifier.axisSize(itemSize, itemSize * 3 + spacing * 2),
- mainAxisSpacedBy = spacing
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize + spacing)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 2)
- .assertMainAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun adaptiveLazyGridAppliesVerticalSpacingsWithContentPadding() {
- val items = (1..3).map { it.toString() }
-
- val spacing = with(rule.density) { 16.toDp() }
- val itemSize = with(rule.density) { 72.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = TvGridCells.Adaptive(itemSize),
- modifier = Modifier.axisSize(itemSize, itemSize * 3 + spacing * 2),
- mainAxisSpacedBy = spacing,
- contentPadding = PaddingValues(mainAxis = spacing)
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing * 3 + itemSize * 2)
- .assertMainAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun fixedLazyGridAppliesVerticalSpacings() {
- val items = (1..4).map { it.toString() }
-
- val spacing = with(rule.density) { 24.toDp() }
- val itemSize = with(rule.density) { 80.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.axisSize(itemSize, itemSize * 2 + spacing),
- mainAxisSpacedBy = spacing,
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing + itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("4")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing + itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun fixedLazyGridAppliesHorizontalSpacings() {
- val items = (1..4).map { it.toString() }
-
- val spacing = with(rule.density) { 15.toDp() }
- val itemSize = with(rule.density) { 30.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.axisSize(itemSize * 2 + spacing, itemSize * 2),
- crossAxisSpacedBy = spacing
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing + itemSize)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("4")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing + itemSize)
- .assertCrossAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun fixedLazyGridAppliesVerticalSpacingsWithContentPadding() {
- val items = (1..4).map { it.toString() }
-
- val spacing = with(rule.density) { 30.toDp() }
- val itemSize = with(rule.density) { 77.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.axisSize(itemSize, itemSize * 2 + spacing),
- mainAxisSpacedBy = spacing,
- contentPadding = PaddingValues(mainAxis = spacing)
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("4")
- .assertIsDisplayed()
- .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun fixedLazyGridAppliesHorizontalSpacingsWithContentPadding() {
- val items = (1..4).map { it.toString() }
-
- val spacing = with(rule.density) { 22.toDp() }
- val itemSize = with(rule.density) { 44.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.axisSize(itemSize * 2 + spacing * 3, itemSize * 2),
- crossAxisSpacedBy = spacing,
- contentPadding = PaddingValues(crossAxis = spacing)
- ) {
- items(items) { Spacer(Modifier.size(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
- .assertCrossAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("4")
- .assertIsDisplayed()
- .assertCrossAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
- .assertCrossAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun usedWithArray() {
- val items = arrayOf("1", "2", "3", "4")
-
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContent {
- LazyGrid(cells = 2, modifier = Modifier.crossAxisSize(itemSize * 2)) {
- items(items) { Spacer(Modifier.mainAxisSize(itemSize).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule
- .onNodeWithTag("2")
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("3")
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule
- .onNodeWithTag("4")
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun usedWithArrayIndexed() {
- val items = arrayOf("1", "2", "3", "4")
-
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContent {
- LazyGrid(cells = 2, Modifier.crossAxisSize(itemSize * 2)) {
- itemsIndexed(items) { index, item ->
- Spacer(Modifier.mainAxisSize(itemSize).testTag("$index*$item"))
- }
- }
- }
-
- rule
- .onNodeWithTag("0*1")
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule
- .onNodeWithTag("1*2")
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("2*3")
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule
- .onNodeWithTag("3*4")
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun changeItemsCountAndScrollImmediately() {
- lateinit var state: TvLazyGridState
- var count by mutableStateOf(100)
- val composedIndexes = mutableListOf<Int>()
- rule.setContent {
- state = rememberTvLazyGridState()
- LazyGrid(cells = 1, modifier = Modifier.mainAxisSize(10.dp), state = state) {
- items(count) { index ->
- composedIndexes.add(index)
- Box(Modifier.size(20.dp))
- }
- }
- }
-
- rule.runOnIdle {
- composedIndexes.clear()
- count = 10
- runBlocking(AutoTestFrameClock()) {
- // we try to scroll to the index after 10, but we expect that the component will
- // already be aware there is a new count and not compose items with index > 10
- state.scrollToItem(50)
- }
- composedIndexes.forEach { assertThat(it).isLessThan(count) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(9)
- }
- }
-
- @Test
- fun maxIntElements() {
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.size(itemSize * 2).testTag(LazyGridTag),
- state = TvLazyGridState(firstVisibleItemIndex = Int.MAX_VALUE - 3)
- ) {
- items(Int.MAX_VALUE) { Box(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule
- .onNodeWithTag("${Int.MAX_VALUE - 3}")
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule
- .onNodeWithTag("${Int.MAX_VALUE - 2}")
- .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("${Int.MAX_VALUE - 1}")
- .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist()
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyGrid(
- cells = 1,
- modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
- userScrollEnabled = true
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.keyPress(2)
-
- rule.onNodeWithTag("1").assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyGrid(
- cells = 1,
- modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
- userScrollEnabled = false
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.keyPress(2)
-
- rule.onNodeWithTag("1").assertMainAxisStartPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
- val itemSizePx = 30f
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- LazyGrid(
- cells = 1,
- modifier = Modifier.size(itemSize * 3),
- state = rememberTvLazyGridState().also { state = it },
- userScrollEnabled = false,
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollBy(itemSizePx) } }
-
- rule.onNodeWithTag("1").assertMainAxisStartPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyGrid(
- cells = 1,
- modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
- userScrollEnabled = false,
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule
- .onNodeWithTag(LazyGridTag)
- .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy))
- .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollToIndex))
- // but we still have a read only scroll range property
- .assert(
- keyIsDefined(
- if (orientation == Orientation.Vertical) {
- SemanticsProperties.VerticalScrollAxisRange
- } else {
- SemanticsProperties.HorizontalScrollAxisRange
- }
- )
- )
- }
-
- @Test
- fun rtl() {
- val gridCrossAxisSize = 30
- val gridCrossAxisSizeDp = with(rule.density) { gridCrossAxisSize.toDp() }
- rule.setContent {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- LazyGrid(cells = 3, modifier = Modifier.crossAxisSize(gridCrossAxisSizeDp)) {
- items(3) { Box(Modifier.mainAxisSize(1.dp).testTag("$it")) }
- }
- }
- }
-
- val tags =
- if (orientation == Orientation.Vertical) {
- listOf("0", "1", "2")
- } else {
- listOf("2", "1", "0")
- }
- rule
- .onNodeWithTag(tags[0])
- .assertCrossAxisStartPositionInRootIsEqualTo(gridCrossAxisSizeDp * 2 / 3)
- rule
- .onNodeWithTag(tags[1])
- .assertCrossAxisStartPositionInRootIsEqualTo(gridCrossAxisSizeDp / 3)
- rule.onNodeWithTag(tags[2]).assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun withMissingItems() {
- val itemMainAxisSize = with(rule.density) { 30.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- LazyGrid(
- cells = 2,
- modifier = Modifier.mainAxisSize(itemMainAxisSize + 1.dp),
- state = state
- ) {
- items((0..8).map { it.toString() }) {
- if (it != "3") {
- Box(Modifier.mainAxisSize(itemMainAxisSize).testTag(it))
- }
- }
- }
- }
-
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.runOnIdle { runBlocking { state.scrollToItem(3) } }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- rule.onNodeWithTag("2").assertIsDisplayed()
- rule.onNodeWithTag("4").assertIsDisplayed()
- rule.onNodeWithTag("5").assertIsDisplayed()
- rule.onNodeWithTag("6").assertDoesNotExist()
- rule.onNodeWithTag("7").assertDoesNotExist()
- }
-
- @Test
- fun passingNegativeItemsCountIsNotAllowed() {
- var exception: Exception? = null
- rule.setContentWithTestViewConfiguration {
- LazyGrid(cells = 1) {
- try {
- items(-1) { Box(Modifier) }
- } catch (e: Exception) {
- exception = e
- }
- }
- }
-
- rule.runOnIdle { assertThat(exception).isInstanceOf(IllegalArgumentException::class.java) }
- }
-
- @Test
- fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
- var remeasureCount = 0
- val layoutModifier =
- Modifier.layout { measurable, constraints ->
- remeasureCount++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- val counter = mutableStateOf(0)
-
- rule.setContentWithTestViewConfiguration {
- counter.value // just to trigger recomposition
- LazyGrid(
- cells = 1,
- // this will return a new object everytime causing LazyGrid recomposition
- // without causing remeasure
- modifier = Modifier.composed { layoutModifier }
- ) {
- items(1) { Spacer(Modifier.size(10.dp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(remeasureCount).isEqualTo(1)
- counter.value++
- }
-
- rule.runOnIdle { assertThat(remeasureCount).isEqualTo(1) }
- }
-
- @Test
- fun scrollingALotDoesNotCauseLazyLayoutRecomposition() {
- var recomposeCount = 0
- lateinit var state: TvLazyGridState
-
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyGridState()
- LazyGrid(
- cells = 1,
- modifier =
- Modifier.composed {
- recomposeCount++
- Modifier
- }
- .size(100.dp),
- state
- ) {
- items(1000) { Spacer(Modifier.size(100.dp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(recomposeCount).isEqualTo(1)
-
- runBlocking { state.scrollToItem(100) }
- }
-
- rule.runOnIdle { assertThat(recomposeCount).isEqualTo(1) }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun zIndexOnItemAffectsDrawingOrder() {
- rule.setContentWithTestViewConfiguration {
- LazyGrid(cells = 1, modifier = Modifier.size(6.dp).testTag(LazyGridTag)) {
- items(listOf(Color.Blue, Color.Green, Color.Red)) { color ->
- Box(
- Modifier.axisSize(6.dp, 2.dp)
- .zIndex(if (color == Color.Green) 1f else 0f)
- .drawBehind {
- drawRect(
- color,
- topLeft = Offset(-10.dp.toPx(), -10.dp.toPx()),
- size = Size(20.dp.toPx(), 20.dp.toPx())
- )
- }
- )
- }
- }
- }
-
- rule.onNodeWithTag(LazyGridTag).captureToImage().assertPixels { Color.Green }
- }
-
- @Suppress("OVERRIDE_DEPRECATION")
- @Test
- fun customGridCells() {
- val items = (1..5).map { it.toString() }
-
- rule.setContent {
- LazyGrid(
- // Two columns in ratio 1:2
- cells =
- object : TvGridCells {
- override fun Density.calculateCrossAxisCellSizes(
- availableSize: Int,
- spacing: Int
- ): List<Int> {
- val availableCrossAxis = availableSize - spacing
- val columnSize = availableCrossAxis / 3
- return listOf(columnSize, columnSize * 2)
- }
- },
- modifier = Modifier.axisSize(300.dp, 100.dp)
- ) {
- items(items) { Spacer(Modifier.mainAxisSize(101.dp).testTag(it)) }
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(100.dp)
-
- rule
- .onNodeWithTag("2")
- .assertCrossAxisStartPositionInRootIsEqualTo(100.dp)
- .assertCrossAxisSizeIsEqualTo(200.dp)
-
- rule.onNodeWithTag("3").assertDoesNotExist()
-
- rule.onNodeWithTag("4").assertDoesNotExist()
-
- rule.onNodeWithTag("5").assertDoesNotExist()
- }
-
- @Test
- fun onlyOneInitialMeasurePass() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- LazyGrid(1, Modifier.requiredSize(100.dp).testTag(LazyGridTag), state = state) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- rule.runOnIdle { assertThat(state.numMeasurePasses).isEqualTo(1) }
- }
-
- @Test
- fun fillingFullSize_nextItemIsNotComposed() {
- val state = TvLazyGridState()
- state.prefetchingEnabled = false
- val itemSizePx = 5f
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyGrid(1, Modifier.testTag(LazyGridTag).mainAxisSize(itemSize), state) {
- items(3) { index -> Box(Modifier.size(itemSize).testTag("$index")) }
- }
- }
-
- repeat(3) { index ->
- rule.onNodeWithTag("$index").assertIsDisplayed()
- rule.onNodeWithTag("${index + 1}").assertDoesNotExist()
- rule.runOnIdle { runBlocking { state.scrollBy(itemSizePx) } }
- }
- }
-}
-
-internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
- isIn(Range.closed(expected - tolerance, expected + tolerance))
-}
-
-internal fun ComposeContentTestRule.keyPress(keyCode: Int, numberOfPresses: Int = 1) {
- repeat(numberOfPresses) {
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
- waitForIdle()
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
deleted file mode 100644
index 58c3655..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
+++ /dev/null
@@ -1,1109 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.snap
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyGridsContentPaddingTest {
- private val LazyListTag = "LazyList"
- private val ItemTag = "item"
- private val ContainerTag = "container"
-
- @get:Rule val rule = createComposeRule()
-
- private var itemSize: Dp = Dp.Infinity
- private var smallPaddingSize: Dp = Dp.Infinity
- private var itemSizePx = 50f
- private var smallPaddingSizePx = 12f
-
- @Before
- fun before() {
- with(rule.density) {
- itemSize = itemSizePx.toDp()
- smallPaddingSize = smallPaddingSizePx.toDp()
- }
- }
-
- @Test
- fun verticalGrid_contentPaddingIsApplied() {
- lateinit var state: TvLazyGridState
- val containerSize = itemSize * 2
- val largePaddingSize = itemSize
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(containerSize).testTag(LazyListTag),
- state = rememberTvLazyGridState().also { state = it },
- contentPadding =
- PaddingValues(
- start = smallPaddingSize,
- top = largePaddingSize,
- end = smallPaddingSize,
- bottom = largePaddingSize
- )
- ) {
- items(listOf(1)) { Spacer(Modifier.height(itemSize).testTag(ItemTag)) }
- }
- }
-
- rule
- .onNodeWithTag(ItemTag)
- .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
- .assertTopPositionInRootIsEqualTo(largePaddingSize)
- .assertWidthIsEqualTo(containerSize - smallPaddingSize * 2)
- .assertHeightIsEqualTo(itemSize)
-
- state.scrollBy(largePaddingSize)
-
- rule
- .onNodeWithTag(ItemTag)
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertHeightIsEqualTo(itemSize)
- }
-
- @Test
- fun verticalGrid_contentPaddingIsNotAffectingScrollPosition() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(itemSize * 2).testTag(LazyListTag),
- state = rememberTvLazyGridState().also { state = it },
- contentPadding = PaddingValues(top = itemSize, bottom = itemSize)
- ) {
- items(listOf(1)) { Spacer(Modifier.height(itemSize).testTag(ItemTag)) }
- }
- }
-
- state.assertScrollPosition(0, 0.dp)
-
- state.scrollBy(itemSize)
-
- state.assertScrollPosition(0, itemSize)
- }
-
- @Test
- fun verticalGrid_scrollForwardItemWithinStartPaddingDisplayed() {
- lateinit var state: TvLazyGridState
- val padding = itemSize * 1.5f
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(padding * 2 + itemSize).testTag(LazyListTag),
- state = rememberTvLazyGridState().also { state = it },
- contentPadding = PaddingValues(top = padding, bottom = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(padding)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize + padding)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2 + padding)
-
- state.scrollBy(padding)
-
- state.assertScrollPosition(1, padding - itemSize)
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2)
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize * 3)
- }
-
- @Test
- fun verticalGrid_scrollBackwardItemWithinStartPaddingDisplayed() {
- lateinit var state: TvLazyGridState
- val padding = itemSize * 1.5f
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(itemSize + padding * 2).testTag(LazyListTag),
- state = rememberTvLazyGridState().also { state = it },
- contentPadding = PaddingValues(top = padding, bottom = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
- state.scrollBy(-itemSize * 1.5f)
-
- state.assertScrollPosition(1, itemSize * 0.5f)
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize * 4.5f - padding)
- }
-
- @Test
- fun verticalGrid_scrollForwardTillTheEnd() {
- lateinit var state: TvLazyGridState
- val padding = itemSize * 1.5f
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(padding * 2 + itemSize).testTag(LazyListTag),
- state = rememberTvLazyGridState().also { state = it },
- contentPadding = PaddingValues(top = padding, bottom = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
-
- state.assertScrollPosition(3, 0.dp)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize - padding)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
-
- // there are no space to scroll anymore, so it should change nothing
- state.scrollBy(10.dp)
-
- state.assertScrollPosition(3, 0.dp)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize - padding)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
- }
-
- @Test
- fun verticalGrid_scrollForwardTillTheEndAndABitBack() {
- lateinit var state: TvLazyGridState
- val padding = itemSize * 1.5f
- rule.setContent {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- modifier = Modifier.requiredSize(padding * 2 + itemSize).testTag(LazyListTag),
- state = rememberTvLazyGridState().also { state = it },
- contentPadding = PaddingValues(top = padding, bottom = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
- state.scrollBy(-itemSize / 2)
-
- state.assertScrollPosition(2, itemSize / 2)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
- }
-
- @Test
- fun verticalGrid_contentPaddingFixedWidthContainer() {
- rule.setContent {
- Box(modifier = Modifier.testTag(ContainerTag).width(itemSize + 8.dp)) {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- contentPadding =
- PaddingValues(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 8.dp)
- ) {
- items(listOf(1)) { Spacer(Modifier.size(itemSize).testTag(ItemTag)) }
- }
- }
- }
-
- rule
- .onNodeWithTag(ItemTag)
- .assertLeftPositionInRootIsEqualTo(2.dp)
- .assertTopPositionInRootIsEqualTo(4.dp)
- .assertWidthIsEqualTo(itemSize)
- .assertHeightIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
- .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
- }
-
- @Test
- fun verticalGrid_contentPaddingAndNoContent() {
- rule.setContent {
- Box(modifier = Modifier.testTag(ContainerTag)) {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- contentPadding =
- PaddingValues(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 8.dp)
- ) {}
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(8.dp)
- .assertHeightIsEqualTo(12.dp)
- }
-
- @Test
- fun verticalGrid_contentPaddingAndZeroSizedItem() {
- rule.setContent {
- Box(modifier = Modifier.testTag(ContainerTag)) {
- TvLazyVerticalGrid(
- columns = TvGridCells.Fixed(1),
- contentPadding =
- PaddingValues(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 8.dp)
- ) {
- items(0) {}
- }
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertWidthIsEqualTo(8.dp)
- .assertHeightIsEqualTo(12.dp)
- }
-
- @Test
- fun verticalGrid_contentPaddingAndReverseLayout() {
- val topPadding = itemSize * 2
- val bottomPadding = itemSize / 2
- val listSize = itemSize * 3
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- reverseLayout = true,
- state = rememberTvLazyGridState().also { state = it },
- modifier = Modifier.size(listSize),
- contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
- ) {
- items(3) { index -> Box(Modifier.size(itemSize).testTag("$index")) }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
- // Partially visible.
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(-itemSize / 2)
-
- // Scroll to the top.
- state.scrollBy(itemSize * 2.5f)
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
- // Shouldn't be visible
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- rule.onNodeWithTag("0").assertIsNotDisplayed()
- }
-
- @Test
- fun column_overscrollWithContentPadding() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = smallPaddingSize)
- ) {
- items(2) { Box(Modifier.testTag("$it").height(itemSize)) }
- }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(smallPaddingSize)
- .assertHeightIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(smallPaddingSize + itemSize)
- .assertHeightIsEqualTo(itemSize)
-
- rule.runOnIdle {
- runBlocking {
- // itemSizePx is the maximum offset, plus if we overscroll the content padding
- // the layout mechanism will decide the item 0 is not needed until we start
- // filling the over scrolled gap.
- state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(smallPaddingSize)
- .assertHeightIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(smallPaddingSize - itemSize)
- .assertHeightIsEqualTo(itemSize)
- }
-
- @Test
- fun totalPaddingLargerParentSize_initialState() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- rule.runOnIdle {
- state.assertScrollPosition(0, 0.dp)
- state.assertVisibleItems(0 to 0.dp)
- state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollByPadding() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(1, 0.dp)
- state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollToLastItem() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollTo(3)
-
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
-
- rule.onNodeWithTag("1").assertIsNotDisplayed()
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollTillTheEnd() {
- // the whole end content padding is displayed
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 4.5f)
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(-itemSize * 0.5f)
-
- rule.runOnIdle {
- state.assertScrollPosition(3, itemSize * 1.5f)
- state.assertVisibleItems(3 to -itemSize * 1.5f)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_initialState() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(0, 0.dp)
- state.assertVisibleItems(0 to 0.dp)
- state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollByPadding() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 2)
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(2, 0.dp)
- state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollToLastItem() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollTo(3)
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollTillTheEnd() {
- // only the end content padding is displayed
- lateinit var state: TvLazyGridState
- rule.setContent {
- state = rememberTvLazyGridState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- state = state,
- contentPadding = PaddingValues(vertical = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(
- itemSize * 1.5f + // container size
- itemSize * 2 + // start padding
- itemSize * 3 // all items
- )
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(3, itemSize * 3.5f)
- state.assertVisibleItems(3 to -itemSize * 3.5f)
- }
- }
-
- // @Test
- // fun row_contentPaddingIsApplied() {
- // lateinit var state: LazyGridState
- // val containerSize = itemSize * 2
- // val largePaddingSize = itemSize
- // rule.setContent {
- // LazyRow(
- // modifier = Modifier.requiredSize(containerSize)
- // .testTag(LazyListTag),
- // state = rememberTvLazyGridState().also { state = it },
- // contentPadding = PaddingValues(
- // top = smallPaddingSize,
- // start = largePaddingSize,
- // bottom = smallPaddingSize,
- // end = largePaddingSize
- // )
- // ) {
- // items(listOf(1)) {
- // Spacer(Modifier.fillParentMaxHeight().width(itemSize).testTag(ItemTag))
- // }
- // }
- // }
-
- // rule.onNodeWithTag(ItemTag)
- // .assertTopPositionInRootIsEqualTo(smallPaddingSize)
- // .assertLeftPositionInRootIsEqualTo(largePaddingSize)
- // .assertHeightIsEqualTo(containerSize - smallPaddingSize * 2)
- // .assertWidthIsEqualTo(itemSize)
-
- // state.scrollBy(largePaddingSize)
-
- // rule.onNodeWithTag(ItemTag)
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // .assertWidthIsEqualTo(itemSize)
- // }
-
- // @Test
- // fun row_contentPaddingIsNotAffectingScrollPosition() {
- // lateinit var state: LazyGridState
- // val itemSize = with(rule.density) {
- // 50.dp.roundToPx().toDp()
- // }
- // rule.setContent {
- // LazyRow(
- // modifier = Modifier.requiredSize(itemSize * 2)
- // .testTag(LazyListTag),
- // state = rememberTvLazyGridState().also { state = it },
- // contentPadding = PaddingValues(
- // start = itemSize,
- // end = itemSize
- // )
- // ) {
- // items(listOf(1)) {
- // Spacer(Modifier.fillParentMaxHeight().width(itemSize).testTag(ItemTag))
- // }
- // }
- // }
-
- // state.assertScrollPosition(0, 0.dp)
-
- // state.scrollBy(itemSize)
-
- // state.assertScrollPosition(0, itemSize)
- // }
-
- // @Test
- // fun row_scrollForwardItemWithinStartPaddingDisplayed() {
- // lateinit var state: LazyGridState
- // val padding = itemSize * 1.5f
- // rule.setContent {
- // LazyRow(
- // modifier = Modifier.requiredSize(padding * 2 + itemSize)
- // .testTag(LazyListTag),
- // state = rememberTvLazyGridState().also { state = it },
- // contentPadding = PaddingValues(
- // start = padding,
- // end = padding
- // )
- // ) {
- // items((0..3).toList()) {
- // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- // }
- // }
- // }
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(padding)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize + padding)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2 + padding)
-
- // state.scrollBy(padding)
-
- // state.assertScrollPosition(1, padding - itemSize)
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2)
- // rule.onNodeWithTag("3")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 3)
- // }
-
- // @Test
- // fun row_scrollBackwardItemWithinStartPaddingDisplayed() {
- // lateinit var state: LazyGridState
- // val padding = itemSize * 1.5f
- // rule.setContent {
- // LazyRow(
- // modifier = Modifier.requiredSize(itemSize + padding * 2)
- // .testTag(LazyListTag),
- // state = rememberTvLazyGridState().also { state = it },
- // contentPadding = PaddingValues(
- // start = padding,
- // end = padding
- // )
- // ) {
- // items((0..3).toList()) {
- // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- // }
- // }
- // }
-
- // state.scrollBy(itemSize * 3)
- // state.scrollBy(-itemSize * 1.5f)
-
- // state.assertScrollPosition(1, itemSize * 0.5f)
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
- // rule.onNodeWithTag("3")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 4.5f - padding)
- // }
-
- // @Test
- // fun row_scrollForwardTillTheEnd() {
- // lateinit var state: LazyGridState
- // val padding = itemSize * 1.5f
- // rule.setContent {
- // LazyRow(
- // modifier = Modifier.requiredSize(padding * 2 + itemSize)
- // .testTag(LazyListTag),
- // state = rememberTvLazyGridState().also { state = it },
- // contentPadding = PaddingValues(
- // start = padding,
- // end = padding
- // )
- // ) {
- // items((0..3).toList()) {
- // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- // }
- // }
- // }
-
- // state.scrollBy(itemSize * 3)
-
- // state.assertScrollPosition(3, 0.dp)
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize - padding)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
- // rule.onNodeWithTag("3")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
-
- // // there are no space to scroll anymore, so it should change nothing
- // state.scrollBy(10.dp)
-
- // state.assertScrollPosition(3, 0.dp)
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize - padding)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
- // rule.onNodeWithTag("3")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
- // }
-
- // @Test
- // fun row_scrollForwardTillTheEndAndABitBack() {
- // lateinit var state: LazyGridState
- // val padding = itemSize * 1.5f
- // rule.setContent {
- // LazyRow(
- // modifier = Modifier.requiredSize(padding * 2 + itemSize)
- // .testTag(LazyListTag),
- // state = rememberTvLazyGridState().also { state = it },
- // contentPadding = PaddingValues(
- // start = padding,
- // end = padding
- // )
- // ) {
- // items((0..3).toList()) {
- // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- // }
- // }
- // }
-
- // state.scrollBy(itemSize * 3)
- // state.scrollBy(-itemSize / 2)
-
- // state.assertScrollPosition(2, itemSize / 2)
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
- // rule.onNodeWithTag("3")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
- // }
-
- // @Test
- // fun row_contentPaddingAndWrapContent() {
- // rule.setContent {
- // Box(modifier = Modifier.testTag(ContainerTag)) {
- // LazyRow(
- // contentPadding = PaddingValues(
- // start = 2.dp,
- // top = 4.dp,
- // end = 6.dp,
- // bottom = 8.dp
- // )
- // ) {
- // items(listOf(1)) {
- // Spacer(Modifier.requiredSize(itemSize).testTag(ItemTag))
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag(ItemTag)
- // .assertLeftPositionInRootIsEqualTo(2.dp)
- // .assertTopPositionInRootIsEqualTo(4.dp)
- // .assertWidthIsEqualTo(itemSize)
- // .assertHeightIsEqualTo(itemSize)
-
- // rule.onNodeWithTag(ContainerTag)
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // .assertTopPositionInRootIsEqualTo(0.dp)
- // .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
- // .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
- // }
-
- // @Test
- // fun row_contentPaddingAndNoContent() {
- // rule.setContent {
- // Box(modifier = Modifier.testTag(ContainerTag)) {
- // LazyRow(
- // contentPadding = PaddingValues(
- // start = 2.dp,
- // top = 4.dp,
- // end = 6.dp,
- // bottom = 8.dp
- // )
- // ) { }
- // }
- // }
-
- // rule.onNodeWithTag(ContainerTag)
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // .assertTopPositionInRootIsEqualTo(0.dp)
- // .assertWidthIsEqualTo(8.dp)
- // .assertHeightIsEqualTo(12.dp)
- // }
-
- // @Test
- // fun row_contentPaddingAndZeroSizedItem() {
- // rule.setContent {
- // Box(modifier = Modifier.testTag(ContainerTag)) {
- // LazyRow(
- // contentPadding = PaddingValues(
- // start = 2.dp,
- // top = 4.dp,
- // end = 6.dp,
- // bottom = 8.dp
- // )
- // ) {
- // items(0) {}
- // }
- // }
- // }
-
- // rule.onNodeWithTag(ContainerTag)
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // .assertTopPositionInRootIsEqualTo(0.dp)
- // .assertWidthIsEqualTo(8.dp)
- // .assertHeightIsEqualTo(12.dp)
- // }
-
- // @Test
- // fun row_contentPaddingAndReverseLayout() {
- // val startPadding = itemSize * 2
- // val endPadding = itemSize / 2
- // val listSize = itemSize * 3
- // lateinit var state: LazyGridState
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true,
- // state = rememberTvLazyGridState().also { state = it },
- // modifier = Modifier.requiredSize(listSize),
- // contentPadding = PaddingValues(start = startPadding, end = endPadding),
- // ) {
- // items(3) { index ->
- // Box(Modifier.requiredSize(itemSize).testTag("$index"))
- // }
- // }
- // }
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(listSize - endPadding - itemSize)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(listSize - endPadding - itemSize * 2)
- // // Partially visible.
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(-itemSize / 2)
-
- // // Scroll to the top.
- // state.scrollBy(itemSize * 2.5f)
-
- // rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(startPadding)
- // // Shouldn't be visible
- // rule.onNodeWithTag("1").assertIsNotDisplayed()
- // rule.onNodeWithTag("0").assertIsNotDisplayed()
- // }
-
- // @Test
- // fun row_overscrollWithContentPadding() {
- // lateinit var state: LazyListState
- // rule.setContent {
- // state = rememberLazyListState()
- // Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2))
- // {
- // LazyRow(
- // state = state,
- // contentPadding = PaddingValues(
- // horizontal = smallPaddingSize
- // )
- // ) {
- // items(2) {
- // Box(Modifier.testTag("$it").fillParentMaxSize())
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
- // .assertWidthIsEqualTo(itemSize)
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(smallPaddingSize + itemSize)
- // .assertWidthIsEqualTo(itemSize)
-
- // rule.runOnIdle {
- // runBlocking {
- // // itemSizePx is the maximum offset, plus if we overscroll the content padding
- // // the layout mechanism will decide the item 0 is not needed until we start
- // // filling the over scrolled gap.
- // state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
- // }
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
- // .assertWidthIsEqualTo(itemSize)
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(smallPaddingSize - itemSize)
- // .assertWidthIsEqualTo(itemSize)
- // }
-
- private fun TvLazyGridState.scrollBy(offset: Dp) {
- runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
- animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
- }
- }
-
- private fun TvLazyGridState.assertScrollPosition(index: Int, offset: Dp) =
- with(rule.density) {
- assertThat([email protected]).isEqualTo(index)
- assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
- }
-
- private fun TvLazyGridState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) =
- with(rule.density) {
- assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
- .isEqualTo(from.roundToPx() to to.roundToPx())
- }
-
- private fun TvLazyGridState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
- with(rule.density) {
- assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset.y })
- .isEqualTo(expected.map { it.first to it.second.roundToPx() })
- }
-
- fun TvLazyGridState.scrollTo(index: Int) {
- runBlocking(Dispatchers.Main + AutoTestFrameClock()) { scrollToItem(index) }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsIndexedTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsIndexedTest.kt
deleted file mode 100644
index 93467fa..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsIndexedTest.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.dp
-import org.junit.Rule
-import org.junit.Test
-
-class LazyGridsIndexedTest {
-
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun lazyVerticalGridShowsIndexedItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(200.dp)) {
- itemsIndexed(items) { index, item ->
- Spacer(Modifier.height(101.dp).testTag("$index-$item"))
- }
- }
- }
-
- rule.onNodeWithTag("0-1").assertIsDisplayed()
-
- rule.onNodeWithTag("1-2").assertIsDisplayed()
-
- rule.onNodeWithTag("2-3").assertDoesNotExist()
-
- rule.onNodeWithTag("3-4").assertDoesNotExist()
- }
-
- @Test
- fun verticalGridWithIndexesComposedWithCorrectIndexAndItem() {
- val items = (0..1).map { it.toString() }
-
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(200.dp)) {
- itemsIndexed(items) { index, item ->
- BasicText("${index}x$item", Modifier.requiredHeight(100.dp))
- }
- }
- }
-
- rule.onNodeWithText("0x0").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithText("1x1").assertTopPositionInRootIsEqualTo(100.dp)
- }
-
- // @Test
- // fun lazyRowShowsIndexedItems() {
- // val items = (1..4).map { it.toString() }
-
- // rule.setContent {
- // LazyRow(Modifier.width(200.dp)) {
- // itemsIndexed(items) { index, item ->
- // Spacer(
- // Modifier.width(101.dp).fillParentMaxHeight()
- // .testTag("$index-$item")
- // )
- // }
- // }
- // }
-
- // rule.onNodeWithTag("0-1")
- // .assertIsDisplayed()
-
- // rule.onNodeWithTag("1-2")
- // .assertIsDisplayed()
-
- // rule.onNodeWithTag("2-3")
- // .assertDoesNotExist()
-
- // rule.onNodeWithTag("3-4")
- // .assertDoesNotExist()
- // }
-
- // @Test
- // fun rowWithIndexesComposedWithCorrectIndexAndItem() {
- // val items = (0..1).map { it.toString() }
-
- // rule.setContent {
- // LazyRow(Modifier.width(200.dp)) {
- // itemsIndexed(items) { index, item ->
- // BasicText(
- // "${index}x$item", Modifier.fillParentMaxHeight().requiredWidth(100.dp)
- // )
- // }
- // }
- // }
-
- // rule.onNodeWithText("0x0")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
-
- // rule.onNodeWithText("1x1")
- // .assertLeftPositionInRootIsEqualTo(100.dp)
- // }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
deleted file mode 100644
index f8b40de..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
+++ /dev/null
@@ -1,529 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-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.tv.foundation.lazy.list.setContentWithTestViewConfiguration
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-private const val ContainerTag = "ContainerTag"
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyGridsReverseLayoutTest {
- @get:Rule val rule = createComposeRule()
-
- private var itemSize: Dp = Dp.Infinity
-
- @Before
- fun before() {
- with(rule.density) { itemSize = 50.toDp() }
- }
-
- @Test
- fun verticalGrid_reverseLayout() {
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(2),
- Modifier.width(itemSize * 2),
- reverseLayout = true
- ) {
- items(4) { Box(Modifier.height(itemSize).testTag(it.toString())) }
- }
- }
-
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("3")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun column_emitTwoElementsAsOneItem() {
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(2),
- Modifier.width(itemSize * 2),
- reverseLayout = true
- ) {
- items(4) {
- Box(Modifier.height(itemSize).testTag((it * 2).toString()))
- Box(Modifier.height(itemSize).testTag((it * 2 + 1).toString()))
- }
- }
- }
-
- rule
- .onNodeWithTag("4")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("5")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("6")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- rule
- .onNodeWithTag("7")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- rule
- .onNodeWithTag("3")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun verticalGrid_initialScrollPositionIs0() {
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(2),
- reverseLayout = true,
- state = rememberTvLazyGridState().also { state = it },
- modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
- ) {
- items((0..5).toList()) { Box(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
- }
-
- @Test
- fun verticalGrid_scrollInWrongDirectionDoesNothing() {
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- reverseLayout = true,
- state = rememberTvLazyGridState().also { state = it },
- modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
- ) {
- items((0..2).toList()) { Box(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- // we scroll down and as the scrolling is reversed it shouldn't affect anything
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun verticalGrid_scrollForwardHalfWay() {
- lateinit var state: TvLazyGridState
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- reverseLayout = true,
- state = rememberTvLazyGridState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 1)
-
- val scrolled =
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
- }
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(scrolled)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize + scrolled)
- }
-
- // @Test
- // fun row_emitTwoElementsAsOneItem_positionedReversed() {
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true
- // ) {
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("0"))
- // Box(Modifier.requiredSize(itemSize).testTag("1"))
- // }
- // }
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // }
-
- // @Test
- // fun row_emitTwoItems_positionedReversed() {
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true
- // ) {
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("0"))
- // }
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("1"))
- // }
- // }
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // }
-
- // @Test
- // fun row_initialScrollPositionIs0() {
- // lateinit var state: LazyListState
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true,
- // state = rememberLazyListState().also { state = it },
- // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
- // ) {
- // items((0..2).toList()) {
- // Box(Modifier.requiredSize(itemSize).testTag("$it"))
- // }
- // }
- // }
-
- // rule.runOnIdle {
- // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- // }
- // }
-
- // @Test
- // fun row_scrollInWrongDirectionDoesNothing() {
- // lateinit var state: LazyListState
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true,
- // state = rememberLazyListState().also { state = it },
- // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
- // ) {
- // items((0..2).toList()) {
- // Box(Modifier.requiredSize(itemSize).testTag("$it"))
- // }
- // }
- // }
-
- // // we scroll down and as the scrolling is reversed it shouldn't affect anything
- // rule.onNodeWithTag(ContainerTag)
- // .scrollBy(x = itemSize, density = rule.density)
-
- // rule.runOnIdle {
- // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // }
-
- // @Test
- // fun row_scrollForwardHalfWay() {
- // lateinit var state: LazyListState
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true,
- // state = rememberLazyListState().also { state = it },
- // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
- // ) {
- // items((0..2).toList()) {
- // Box(Modifier.requiredSize(itemSize).testTag("$it"))
- // }
- // }
- // }
-
- // rule.onNodeWithTag(ContainerTag)
- // .scrollBy(x = -itemSize * 0.5f, density = rule.density)
-
- // val scrolled = rule.runOnIdle {
- // assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- // with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
- // }
-
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(scrolled)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
- // }
-
- // @Test
- // fun row_scrollForwardTillTheEnd() {
- // lateinit var state: LazyListState
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = true,
- // state = rememberLazyListState().also { state = it },
- // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
- // ) {
- // items((0..3).toList()) {
- // Box(Modifier.requiredSize(itemSize).testTag("$it"))
- // }
- // }
- // }
-
- // // we scroll a bit more than it is possible just to make sure we would stop correctly
- // rule.onNodeWithTag(ContainerTag)
- // .scrollBy(x = -itemSize * 2.2f, density = rule.density)
-
- // rule.runOnIdle {
- // with(rule.density) {
- // val realOffset = state.firstVisibleItemScrollOffset.toDp() +
- // itemSize * state.firstVisibleItemIndex
- // assertThat(realOffset).isEqualTo(itemSize * 2)
- // }
- // }
-
- // rule.onNodeWithTag("3")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // }
-
- // @Test
- // fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
- // rule.setContentWithTestViewConfiguration {
- // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- // LazyRow(
- // reverseLayout = true
- // ) {
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("0"))
- // Box(Modifier.requiredSize(itemSize).testTag("1"))
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // }
-
- // @Test
- // fun row_rtl_emitTwoItems_positionedReversed() {
- // rule.setContentWithTestViewConfiguration {
- // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- // LazyRow(
- // reverseLayout = true
- // ) {
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("0"))
- // }
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("1"))
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // }
-
- // @Test
- // fun row_rtl_scrollForwardHalfWay() {
- // lateinit var state: LazyListState
- // rule.setContentWithTestViewConfiguration {
- // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- // LazyRow(
- // reverseLayout = true,
- // state = rememberLazyListState().also { state = it },
- // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
- // ) {
- // items((0..2).toList()) {
- // Box(Modifier.requiredSize(itemSize).testTag("$it"))
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag(ContainerTag)
- // .scrollBy(x = itemSize * 0.5f, density = rule.density)
-
- // val scrolled = rule.runOnIdle {
- // assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- // with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
- // }
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(-scrolled)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
- // rule.onNodeWithTag("2")
- // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
- // }
-
- @Test
- fun verticalGrid_whenParameterChanges() {
- var reverse by mutableStateOf(true)
- rule.setContentWithTestViewConfiguration {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(2),
- Modifier.width(itemSize * 2),
- reverseLayout = reverse
- ) {
- items(4) { Box(Modifier.size(itemSize).testTag(it.toString())) }
- }
- }
-
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("3")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle { reverse = false }
-
- rule
- .onNodeWithTag("0")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("1")
- .assertTopPositionInRootIsEqualTo(0.dp)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- rule
- .onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(0.dp)
- rule
- .onNodeWithTag("3")
- .assertTopPositionInRootIsEqualTo(itemSize)
- .assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- // @Test
- // fun row_whenParameterChanges() {
- // var reverse by mutableStateOf(true)
- // rule.setContentWithTestViewConfiguration {
- // LazyRow(
- // reverseLayout = reverse
- // ) {
- // item {
- // Box(Modifier.requiredSize(itemSize).testTag("0"))
- // Box(Modifier.requiredSize(itemSize).testTag("1"))
- // }
- // }
- // }
-
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
-
- // rule.runOnIdle {
- // reverse = false
- // }
-
- // rule.onNodeWithTag("0")
- // .assertLeftPositionInRootIsEqualTo(0.dp)
- // rule.onNodeWithTag("1")
- // .assertLeftPositionInRootIsEqualTo(itemSize)
- // }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyNestedScrollingTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyNestedScrollingTest.kt
deleted file mode 100644
index 0cf0ef6..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyNestedScrollingTest.kt
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.lazy.list.TestTouchSlop
-import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
-import com.google.common.truth.Truth
-import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyNestedScrollingTest {
- private val LazyTag = "LazyTag"
-
- @get:Rule val rule = createComposeRule()
-
- private val expectedDragOffset = 20f
- private val dragOffsetWithTouchSlop = expectedDragOffset + TestTouchSlop
-
- @Test
- fun verticalGrid_nestedScrollingBackwardInitially() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- Modifier.requiredSize(100.dp).testTag(LazyTag)
- ) {
- items(items) { Spacer(Modifier.requiredSize(50.dp).testTag("$it")) }
- }
- }
- }
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = 100f + TestTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(100f) }
- }
-
- @Test
- fun verticalGrid_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- Modifier.requiredSize(100.dp).testTag(LazyTag),
- ) {
- items(items) { Box(Modifier.requiredHeight(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- // scroll forward
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-
- // scroll back so we again on 0 position
- // we scroll one extra dp to prevent rounding issues
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- draggedOffset = 0f
- down(Offset(x = 100f, y = 100f))
- moveBy(Offset(x = 0f, y = dragOffsetWithTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset) }
- }
-
- @Test
- fun verticalGrid_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
- val items = (1..2).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- Modifier.requiredSize(100.dp).testTag(LazyTag)
- ) {
- items(items) { Spacer(Modifier.requiredSize(40.dp).testTag("$it")) }
- }
- }
- }
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset) }
- }
-
- @Ignore("b/278219642")
- @Test
- fun verticalGrid_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyVerticalGrid(
- TvGridCells.Fixed(1),
- Modifier.requiredSize(100.dp).testTag(LazyTag)
- ) {
- items(items) { Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- // scroll till the end
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- draggedOffset = 0f
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset) }
- }
-
- // @Test
- // fun row_nestedScrollingBackwardInitially() = runBlocking {
- // val items = (1..3).toList()
- // var draggedOffset = 0f
- // val scrollable = ScrollableState {
- // draggedOffset += it
- // it
- // }
- // rule.setContentWithTestViewConfiguration {
- // Box(
- // Modifier.scrollable(
- // orientation = Orientation.Horizontal,
- // state = scrollable
- // )
- // ) {
- // LazyRow(
- // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
- // ) {
- // items(items) {
- // Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag(LazyTag)
- // .performTouchInput {
- // down(Offset(x = 10f, y = 10f))
- // moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
- // up()
- // }
-
- // rule.runOnIdle {
- // Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
- // }
- // }
-
- // @Test
- // fun row_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
- // val items = (1..3).toList()
- // var draggedOffset = 0f
- // val scrollable = ScrollableState {
- // draggedOffset += it
- // it
- // }
- // rule.setContentWithTestViewConfiguration {
- // Box(
- // Modifier.scrollable(
- // orientation = Orientation.Horizontal,
- // state = scrollable
- // )
- // ) {
- // LazyRow(
- // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
- // ) {
- // items(items) {
- // Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
- // }
- // }
- // }
- // }
-
- // // scroll forward
- // rule.onNodeWithTag(LazyTag)
- // .scrollBy(x = 20.dp, density = rule.density)
-
- // // scroll back so we again on 0 position
- // // we scroll one extra dp to prevent rounding issues
- // rule.onNodeWithTag(LazyTag)
- // .scrollBy(x = -(21.dp), density = rule.density)
-
- // rule.onNodeWithTag(LazyTag)
- // .performTouchInput {
- // draggedOffset = 0f
- // down(Offset(x = 10f, y = 10f))
- // moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
- // up()
- // }
-
- // rule.runOnIdle {
- // Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
- // }
- // }
-
- // @Test
- // fun row_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
- // val items = (1..2).toList()
- // var draggedOffset = 0f
- // val scrollable = ScrollableState {
- // draggedOffset += it
- // it
- // }
- // rule.setContentWithTestViewConfiguration {
- // Box(
- // Modifier.scrollable(
- // orientation = Orientation.Horizontal,
- // state = scrollable
- // )
- // ) {
- // LazyRow(
- // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
- // ) {
- // items(items) {
- // Spacer(Modifier.requiredSize(40.dp).testTag("$it"))
- // }
- // }
- // }
- // }
-
- // rule.onNodeWithTag(LazyTag)
- // .performTouchInput {
- // down(Offset(x = 10f, y = 10f))
- // moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
- // up()
- // }
-
- // rule.runOnIdle {
- // Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
- // }
- // }
-
- // @Test
- // fun row_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
- // val items = (1..3).toList()
- // var draggedOffset = 0f
- // val scrollable = ScrollableState {
- // draggedOffset += it
- // it
- // }
- // rule.setContentWithTestViewConfiguration {
- // Box(
- // Modifier.scrollable(
- // orientation = Orientation.Horizontal,
- // state = scrollable
- // )
- // ) {
- // LazyRow(
- // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
- // ) {
- // items(items) {
- // Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
- // }
- // }
- // }
- // }
-
- // // scroll till the end
- // rule.onNodeWithTag(LazyTag)
- // .scrollBy(x = 55.dp, density = rule.density)
-
- // rule.onNodeWithTag(LazyTag)
- // .performTouchInput {
- // draggedOffset = 0f
- // down(Offset(x = 10f, y = 10f))
- // moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
- // up()
- // }
-
- // rule.runOnIdle {
- // Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
- // }
- // }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
deleted file mode 100644
index 41d10b5..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
+++ /dev/null
@@ -1,344 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import android.R.id.accessibilityActionScrollDown
-import android.R.id.accessibilityActionScrollLeft
-import android.R.id.accessibilityActionScrollRight
-import android.R.id.accessibilityActionScrollUp
-import android.view.View
-import android.view.accessibility.AccessibilityNodeProvider
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.core.view.ViewCompat
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
-import androidx.test.filters.MediumTest
-import com.google.common.truth.IterableSubject
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyScrollAccessibilityTest(private val config: TestConfig) :
- BaseLazyGridTestWithOrientation(config.orientation) {
-
- data class TestConfig(val orientation: Orientation, val rtl: Boolean, val reversed: Boolean) {
- val horizontal = orientation == Orientation.Horizontal
- val vertical = !horizontal
-
- override fun toString(): String {
- return (if (orientation == Orientation.Horizontal) "horizontal" else "vertical") +
- (if (rtl) ",rtl" else ",ltr") +
- (if (reversed) ",reversed" else "")
- }
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params() =
- listOf(Orientation.Horizontal, Orientation.Vertical).flatMap { horizontal ->
- listOf(false, true).flatMap { rtl ->
- listOf(false, true).map { reversed -> TestConfig(horizontal, rtl, reversed) }
- }
- }
- }
-
- private val scrollerTag = "ScrollerTest"
- private var composeView: View? = null
- private val accessibilityNodeProvider: AccessibilityNodeProvider
- get() =
- checkNotNull(composeView) {
- "composeView not initialized. Did `composeView = LocalView.current` not work?"
- }
- .let { composeView ->
- ViewCompat.getAccessibilityDelegate(composeView)!!.getAccessibilityNodeProvider(
- composeView
- )!!
- .provider as AccessibilityNodeProvider
- }
-
- private val itemSize = 21
- private var itemSizeDp: Dp = Dp.Unspecified
- private val containerSize = 200
- private var containerSizeDp: Dp = Dp.Unspecified
- private val contentPadding = 50
- private var contentPaddingDp: Dp = Dp.Unspecified
-
- @Before
- fun before() {
- with(rule.density) {
- itemSizeDp = itemSize.toDp()
- containerSizeDp = containerSize.toDp()
- contentPaddingDp = contentPadding.toDp()
- }
- }
-
- @Test
- fun scrollForward() {
- testRelativeDirection(58, ACTION_SCROLL_FORWARD)
- }
-
- @Test
- fun scrollBackward() {
- testRelativeDirection(41, ACTION_SCROLL_BACKWARD)
- }
-
- @Test
- fun scrollRight() {
- testAbsoluteDirection(58, accessibilityActionScrollRight, config.horizontal)
- }
-
- @Test
- fun scrollLeft() {
- testAbsoluteDirection(41, accessibilityActionScrollLeft, config.horizontal)
- }
-
- @Test
- fun scrollDown() {
- testAbsoluteDirection(58, accessibilityActionScrollDown, config.vertical)
- }
-
- @Test
- fun scrollUp() {
- testAbsoluteDirection(41, accessibilityActionScrollUp, config.vertical)
- }
-
- @Test
- fun verifyScrollActionsAtStart() {
- createScrollableContent_StartAtStart()
- verifyNodeInfoScrollActions(
- expectForward = !config.reversed,
- expectBackward = config.reversed
- )
- }
-
- @Test
- fun verifyScrollActionsInMiddle() {
- createScrollableContent_StartInMiddle()
- verifyNodeInfoScrollActions(expectForward = true, expectBackward = true)
- }
-
- @Test
- fun verifyScrollActionsAtEnd() {
- createScrollableContent_StartAtEnd()
- verifyNodeInfoScrollActions(
- expectForward = config.reversed,
- expectBackward = !config.reversed
- )
- }
-
- /**
- * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget] has
- * been reached. The canonical target is the item that we expect to see when moving forward in a
- * non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR). The actual
- * target is either the canonical target or the target that is as far from the middle of the
- * lazy list as the canonical target, but on the other side of the middle, depending on the
- * [configuration][config].
- */
- private fun testRelativeDirection(canonicalTarget: Int, accessibilityAction: Int) {
- val target = if (!config.reversed) canonicalTarget else 100 - canonicalTarget - 1
- testScrollAction(target, accessibilityAction)
- }
-
- /**
- * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget] has
- * been reached (but only if we [expect][expectActionSuccess] the action to succeed). The
- * canonical target is the item that we expect to see when moving forward in a non-reversed
- * scrollable (e.g. down in LazyColumn or right in LazyRow in LTR). The actual target is either
- * the canonical target or the target that is as far from the middle of the scrollable as the
- * canonical target, but on the other side of the middle, depending on the
- * [configuration][config].
- */
- private fun testAbsoluteDirection(
- canonicalTarget: Int,
- accessibilityAction: Int,
- expectActionSuccess: Boolean
- ) {
- var target = canonicalTarget
- if (config.horizontal && config.rtl) {
- target = 100 - target - 1
- }
- if (config.reversed) {
- target = 100 - target - 1
- }
- testScrollAction(target, accessibilityAction, expectActionSuccess)
- }
-
- /**
- * Setup the test, run the given [accessibilityAction], and check if the [target] has been
- * reached (but only if we [expect][expectActionSuccess] the action to succeed).
- */
- private fun testScrollAction(
- target: Int,
- accessibilityAction: Int,
- expectActionSuccess: Boolean = true
- ) {
- createScrollableContent_StartInMiddle()
- rule.onNodeWithText("$target").assertDoesNotExist()
-
- val returnValue =
- rule.onNodeWithTag(scrollerTag).withSemanticsNode {
- accessibilityNodeProvider.performAction(id, accessibilityAction, null)
- }
-
- assertThat(returnValue).isEqualTo(expectActionSuccess)
- if (expectActionSuccess) {
- rule.onNodeWithText("$target").assertIsDisplayed()
- } else {
- rule.onNodeWithText("$target").assertDoesNotExist()
- }
- }
-
- /**
- * Checks if all of the scroll actions are present or not according to what we expect based on
- * [expectForward] and [expectBackward]. The scroll actions that are checked are forward,
- * backward, left, right, up and down. The expectation parameters must already account for
- * [reversing][TestConfig.reversed].
- */
- private fun verifyNodeInfoScrollActions(expectForward: Boolean, expectBackward: Boolean) {
- val nodeInfo =
- rule.onNodeWithTag(scrollerTag).withSemanticsNode {
- rule.runOnUiThread { accessibilityNodeProvider.createAccessibilityNodeInfo(id) }
- }
-
- val actions = nodeInfo?.actionList?.map { it.id }
-
- assertThat(actions).contains(expectForward, ACTION_SCROLL_FORWARD)
- assertThat(actions).contains(expectBackward, ACTION_SCROLL_BACKWARD)
-
- if (config.horizontal) {
- val expectLeft = if (config.rtl) expectForward else expectBackward
- val expectRight = if (config.rtl) expectBackward else expectForward
- assertThat(actions).contains(expectLeft, accessibilityActionScrollLeft)
- assertThat(actions).contains(expectRight, accessibilityActionScrollRight)
- assertThat(actions).contains(false, accessibilityActionScrollDown)
- assertThat(actions).contains(false, accessibilityActionScrollUp)
- } else {
- assertThat(actions).contains(false, accessibilityActionScrollLeft)
- assertThat(actions).contains(false, accessibilityActionScrollRight)
- assertThat(actions).contains(expectForward, accessibilityActionScrollDown)
- assertThat(actions).contains(expectBackward, accessibilityActionScrollUp)
- }
- }
-
- private fun IterableSubject.contains(expectPresent: Boolean, element: Any) {
- if (expectPresent) {
- contains(element)
- } else {
- doesNotContain(element)
- }
- }
-
- /**
- * Creates a Row/Column that starts at the first item, according to [createScrollableContent]
- */
- private fun createScrollableContent_StartAtStart() {
- createScrollableContent {
- // Start at the start:
- // -> pretty basic
- rememberTvLazyGridState(0, 0)
- }
- }
-
- /** Creates a Row/Column that starts in the middle, according to [createScrollableContent] */
- private fun createScrollableContent_StartInMiddle() {
- createScrollableContent {
- // Start at the middle:
- // Content size: 100 items * 21 per item = 2100
- // Viewport size: 200 rect - 50 padding on both sides = 100
- // Content outside viewport: 2100 - 100 = 2000
- // -> centered when 1000 on either side, which is 47 items + 13
- rememberTvLazyGridState(47, 13)
- }
- }
-
- /** Creates a Row/Column that starts at the last item, according to [createScrollableContent] */
- private fun createScrollableContent_StartAtEnd() {
- createScrollableContent {
- // Start at the end:
- // Content size: 100 items * 21 per item = 2100
- // Viewport size: 200 rect - 50 padding on both sides = 100
- // Content outside viewport: 2100 - 100 = 2000
- // -> at the end when offset at 2000, which is 95 items + 5
- rememberTvLazyGridState(95, 5)
- }
- }
-
- /**
- * Creates a grid with a viewport of 100 px, containing 100 items each 21 px in size. The items
- * have a text with their index (ASC), and where the viewport starts is determined by the given
- * [lambda][rememberTvLazyGridState]. All properties from [config] are applied. The viewport has
- * padding around it to make sure scroll distance doesn't include padding.
- */
- private fun createScrollableContent(
- rememberTvLazyGridState: @Composable () -> TvLazyGridState
- ) {
- rule.setContent {
- composeView = LocalView.current
-
- val state = rememberTvLazyGridState()
-
- Box(Modifier.requiredSize(containerSizeDp).background(Color.White)) {
- val direction = if (config.rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
- CompositionLocalProvider(LocalLayoutDirection provides direction) {
- LazyGrid(
- cells = 1,
- modifier = Modifier.testTag(scrollerTag).matchParentSize(),
- state = state,
- contentPadding = PaddingValues(contentPaddingDp),
- reverseLayout = config.reversed
- ) {
- items(100) {
- Box(Modifier.requiredSize(itemSizeDp).background(Color.Yellow)) {
- BasicText("$it", Modifier.align(Alignment.Center))
- }
- }
- }
- }
- }
- }
- }
-
- private fun <T> SemanticsNodeInteraction.withSemanticsNode(block: SemanticsNode.() -> T): T {
- return block.invoke(fetchSemanticsNode())
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
deleted file mode 100644
index 16d1af9..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ /dev/null
@@ -1,359 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.FloatSpringSpec
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import java.util.concurrent.TimeUnit
-import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-@MediumTest
-// @RunWith(Parameterized::class)
-class LazyScrollTest { // (private val orientation: Orientation)
- @get:Rule val rule = createComposeRule()
-
- private val vertical: Boolean
- get() = true // orientation == Orientation.Vertical
-
- private val itemsCount = 40
- private lateinit var state: TvLazyGridState
-
- private val itemSizePx = 100
- private var itemSizeDp = Dp.Unspecified
- private var containerSizeDp = Dp.Unspecified
-
- lateinit var scope: CoroutineScope
-
- @Before
- fun setup() {
- with(rule.density) {
- itemSizeDp = itemSizePx.toDp()
- containerSizeDp = itemSizeDp * 3
- }
- rule.setContent {
- state = rememberTvLazyGridState()
- scope = rememberCoroutineScope()
- TestContent()
- }
- }
-
- @Test
- fun setupWorks() {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
-
- @Test
- fun scrollToItem() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(2) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(0)
- state.scrollToItem(3)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun scrollToItemWithOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(6, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(6)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
-
- @Test
- fun scrollToItemWithNegativeOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(6, -10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(4)
- val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
- assertThat(item6Offset).isEqualTo(10)
- }
-
- @Test
- fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(itemsCount - 6, 10)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
- }
-
- @Test
- fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(1, -(itemSizePx + 10))
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
- }
-
- @Test
- fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(itemsCount + 4) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
- }
-
- @Test
- fun animateScrollBy() = runBlocking {
- val scrollDistance = 320
-
- val expectedLine = scrollDistance / itemSizePx // resolves to 3
- val expectedItem = expectedLine * 2 // resolves to 6
- val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
-
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollBy(scrollDistance.toFloat())
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(expectedItem)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
- }
-
- @Test
- fun animateScrollToItem() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.animateScrollToItem(10, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(10)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
-
- @Test
- fun animateScrollToItemWithOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.animateScrollToItem(6, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(6)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
-
- @Test
- fun animateScrollToItemWithNegativeOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.animateScrollToItem(6, -10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(4)
- val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
- assertThat(item6Offset).isEqualTo(10)
- }
-
- @Test
- fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(itemsCount - 6, 10)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
- }
-
- @Test
- fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(2, -(itemSizePx + 10))
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
- }
-
- @Test
- fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(itemsCount + 2)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
- }
-
- @Test
- fun animatePerFrameForwardToVisibleItem() {
- assertSpringAnimation(toIndex = 4)
- }
-
- @Test
- fun animatePerFrameForwardToVisibleItemWithOffset() {
- assertSpringAnimation(toIndex = 4, toOffset = 35)
- }
-
- @Test
- fun animatePerFrameForwardToNotVisibleItem() {
- assertSpringAnimation(toIndex = 16)
- }
-
- @Test
- fun animatePerFrameForwardToNotVisibleItemWithOffset() {
- assertSpringAnimation(toIndex = 20, toOffset = 35)
- }
-
- @Test
- fun animatePerFrameBackward() {
- assertSpringAnimation(toIndex = 2, fromIndex = 12)
- }
-
- @Test
- fun animatePerFrameBackwardWithOffset() {
- assertSpringAnimation(toIndex = 2, fromIndex = 10, fromOffset = 58)
- }
-
- @Test
- fun animatePerFrameBackwardWithInitialOffset() {
- assertSpringAnimation(toIndex = 0, toOffset = 40, fromIndex = 8)
- }
-
- @Test
- fun animateScrollToItemWithOffsetLargerThanItemSize_forward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(10, -itemSizePx * 3)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(4)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(10)
- state.animateScrollToItem(0, itemSizePx * 3)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(6)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun canScrollForward() = runBlocking {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.canScrollForward).isTrue()
- assertThat(state.canScrollBackward).isFalse()
- }
-
- @Test
- fun canScrollBackward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(itemsCount) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
- assertThat(state.canScrollForward).isFalse()
- assertThat(state.canScrollBackward).isTrue()
- }
-
- @Test
- fun canScrollForwardAndBackward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(10)
- assertThat(state.canScrollForward).isTrue()
- assertThat(state.canScrollBackward).isTrue()
- }
-
- private fun assertSpringAnimation(
- toIndex: Int,
- toOffset: Int = 0,
- fromIndex: Int = 0,
- fromOffset: Int = 0
- ) {
- if (fromIndex != 0 || fromOffset != 0) {
- rule.runOnIdle { runBlocking { state.scrollToItem(fromIndex, fromOffset) } }
- }
- rule.waitForIdle()
-
- assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
-
- rule.mainClock.autoAdvance = false
-
- scope.launch { state.animateScrollToItem(toIndex, toOffset) }
-
- while (!state.isScrollInProgress) {
- Thread.sleep(5)
- }
-
- val startOffset = (fromIndex / 2 * itemSizePx + fromOffset).toFloat()
- val endOffset = (toIndex / 2 * itemSizePx + toOffset).toFloat()
- val spec = FloatSpringSpec()
-
- val duration =
- TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
- rule.mainClock.advanceTimeByFrame()
- var expectedTime = rule.mainClock.currentTime
- for (i in 0..duration step FrameDuration) {
- val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
- val expectedValue = spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
- val actualValue =
- (state.firstVisibleItemIndex / 2 * itemSizePx + state.firstVisibleItemScrollOffset)
- assertWithMessage(
- "On animation frame at $i index=${state.firstVisibleItemIndex} " +
- "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
- )
- .that(actualValue)
- .isEqualTo(expectedValue.roundToInt(), tolerance = 1)
-
- rule.mainClock.advanceTimeBy(FrameDuration)
- expectedTime += FrameDuration
- assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
- rule.waitForIdle()
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
- }
-
- @Composable
- private fun TestContent() {
- if (vertical) {
- TvLazyVerticalGrid(TvGridCells.Fixed(2), Modifier.height(containerSizeDp), state) {
- items(itemsCount) { ItemContent() }
- }
- } else {
- // LazyRow(Modifier.width(300.dp), state) {
- // items(items) {
- // ItemContent()
- // }
- // }
- }
- }
-
- @Composable
- private fun ItemContent() {
- val modifier =
- if (vertical) {
- Modifier.height(itemSizeDp)
- } else {
- Modifier.width(itemSizeDp)
- }
- Spacer(modifier)
- }
-
- // companion object {
- // @JvmStatic
- // @Parameterized.Parameters(name = "{0}")
- // fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
- // }
-}
-
-private val FrameDuration = 16L
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazySemanticsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazySemanticsTest.kt
deleted file mode 100644
index 886e72a..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazySemanticsTest.kt
+++ /dev/null
@@ -1,168 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
-import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
-import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Tests the semantics properties defined on a LazyGrid:
- * - GetIndexForKey
- * - ScrollToIndex
- *
- * GetIndexForKey: Create a lazy grid, iterate over all indices, verify key of each of them
- *
- * ScrollToIndex: Create a lazy grid, scroll to a line off screen, verify shown items
- *
- * All tests performed in [runTest], scenarios set up in the test methods.
- */
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazySemanticsTest {
- private val N = 20
- private val LazyGridTag = "lazy_grid"
- private val LazyGridModifier = Modifier.testTag(LazyGridTag).requiredSize(100.dp)
-
- private fun tag(index: Int): String = "tag_$index"
-
- private fun key(index: Int): String = "key_$index"
-
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun itemSemantics_verticalGrid() {
- rule.setContent {
- TvLazyVerticalGrid(TvGridCells.Fixed(1), LazyGridModifier) {
- repeat(N) { item(key = key(it)) { SpacerInColumn(it) } }
- }
- }
- runTest()
- }
-
- @Test
- fun itemsSemantics_verticalGrid() {
- rule.setContent {
- val state = rememberTvLazyGridState()
- TvLazyVerticalGrid(TvGridCells.Fixed(1), LazyGridModifier, state) {
- items(items = List(N) { it }, key = { key(it) }) { SpacerInColumn(it) }
- }
- }
- runTest()
- }
-
- // @Test
- // fun itemSemantics_row() {
- // rule.setContent {
- // LazyRow(LazyGridModifier) {
- // repeat(N) {
- // item(key = key(it)) {
- // SpacerInRow(it)
- // }
- // }
- // }
- // }
- // runTest()
- // }
-
- // @Test
- // fun itemsSemantics_row() {
- // rule.setContent {
- // LazyRow(LazyGridModifier) {
- // items(items = List(N) { it }, key = { key(it) }) {
- // SpacerInRow(it)
- // }
- // }
- // }
- // runTest()
- // }
-
- private fun runTest() {
- checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)
-
- // Verify IndexForKey
- rule
- .onNodeWithTag(LazyGridTag)
- .assert(
- SemanticsMatcher.keyIsDefined(IndexForKey)
- .and(
- SemanticsMatcher("keys match") { node ->
- val actualIndex = node.config.getOrNull(IndexForKey)!!
- (0 until N).all { expectedIndex ->
- expectedIndex == actualIndex.invoke(key(expectedIndex))
- }
- }
- )
- )
-
- // Verify ScrollToIndex
- rule.onNodeWithTag(LazyGridTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))
-
- invokeScrollToIndex(targetIndex = 10)
- checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)
-
- invokeScrollToIndex(targetIndex = N - 1)
- checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
- }
-
- private fun invokeScrollToIndex(targetIndex: Int) {
- val node =
- rule.onNodeWithTag(LazyGridTag).fetchSemanticsNode("Failed: invoke ScrollToIndex")
- rule.runOnUiThread { node.config[ScrollToIndex].action!!.invoke(targetIndex) }
- }
-
- private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
- if (firstExpectedItem > 0) {
- rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
- }
- (firstExpectedItem..lastExpectedItem).forEach { rule.onNodeWithTag(tag(it)).assertExists() }
- if (firstExpectedItem < N - 1) {
- rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
- }
- }
-
- @Composable
- private fun SpacerInColumn(index: Int) {
- Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
- }
-
- @Composable
- private fun SpacerInRow(index: Int) {
- Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt
deleted file mode 100644
index 24ce2d1..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt
+++ /dev/null
@@ -1,499 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.lazy.list.LayoutInfoTestParam
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class TvLazyGridLayoutInfoTest(param: LayoutInfoTestParam) :
- BaseLazyGridTestWithOrientation(param.orientation) {
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters(): Array<Any> =
- arrayOf(
- LayoutInfoTestParam(Orientation.Vertical, false),
- LayoutInfoTestParam(Orientation.Vertical, true),
- LayoutInfoTestParam(Orientation.Horizontal, false),
- LayoutInfoTestParam(Orientation.Horizontal, true),
- )
- }
-
- private val isVertical = param.orientation == Orientation.Vertical
- private val reverseLayout = param.reverseLayout
-
- private var itemSizePx: Int = 50
- private var itemSizeDp: Dp = Dp.Infinity
- private var gridWidthPx: Int = itemSizePx * 2
- private var gridWidthDp: Dp = Dp.Infinity
-
- @Before
- fun before() {
- with(rule.density) {
- itemSizeDp = itemSizePx.toDp()
- gridWidthDp = gridWidthPx.toDp()
- }
- }
-
- @Test
- fun visibleItemsAreCorrect() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
- ) {
- items((0..11).toList()) { Box(Modifier.mainAxisSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle { state.layoutInfo.assertVisibleItems(count = 8, cells = 2) }
- }
-
- @Test
- fun visibleItemsAreCorrectAfterScroll() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
- ) {
- items((0..11).toList()) { Box(Modifier.mainAxisSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollToItem(2, 10) }
- state.layoutInfo.assertVisibleItems(
- count = 8,
- startIndex = 2,
- startOffset = -10,
- cells = 2
- )
- }
- }
-
- @Test
- fun visibleItemsAreCorrectWithSpacing() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 1,
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- mainAxisSpacedBy = itemSizeDp,
- modifier = Modifier.axisSize(itemSizeDp, itemSizeDp * 3.5f),
- ) {
- items((0..11).toList()) { Box(Modifier.mainAxisSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle {
- state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx, cells = 1)
- }
- }
-
- @Composable
- fun ObservingFun(state: TvLazyGridState, currentInfo: StableRef<TvLazyGridLayoutInfo?>) {
- currentInfo.value = state.layoutInfo
- }
-
- @Test
- fun visibleItemsAreObservableWhenWeScroll() {
- lateinit var state: TvLazyGridState
- val currentInfo = StableRef<TvLazyGridLayoutInfo?>(null)
- rule.setContent {
- LazyGrid(
- cells = 2,
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.axisSize(itemSizeDp * 2f, itemSizeDp * 3.5f),
- ) {
- items((0..11).toList()) { Box(Modifier.mainAxisSize(itemSizeDp)) }
- }
- ObservingFun(state, currentInfo)
- }
-
- rule.runOnIdle {
- // empty it here and scrolling should invoke observingFun again
- currentInfo.value = null
- runBlocking { state.scrollToItem(2, 0) }
- }
-
- rule.runOnIdle {
- assertThat(currentInfo.value).isNotNull()
- currentInfo.value!!.assertVisibleItems(count = 8, startIndex = 2, cells = 2)
- }
- }
-
- @Test
- fun visibleItemsAreObservableWhenResize() {
- lateinit var state: TvLazyGridState
- var size by mutableStateOf(itemSizeDp * 2)
- var currentInfo: TvLazyGridLayoutInfo? = null
- @Composable
- fun observingFun() {
- currentInfo = state.layoutInfo
- }
- rule.setContent {
- LazyGrid(
- cells = 1,
- modifier = Modifier.crossAxisSize(itemSizeDp),
- reverseLayout = reverseLayout,
- state = rememberTvLazyGridState().also { state = it },
- ) {
- item { Box(Modifier.size(size)) }
- }
- observingFun()
- }
-
- rule.runOnIdle {
- assertThat(currentInfo).isNotNull()
- currentInfo!!.assertVisibleItems(
- count = 1,
- expectedSize =
- if (isVertical) {
- IntSize(itemSizePx, itemSizePx * 2)
- } else {
- IntSize(itemSizePx * 2, itemSizePx)
- },
- cells = 1
- )
- currentInfo = null
- size = itemSizeDp
- }
-
- rule.runOnIdle {
- assertThat(currentInfo).isNotNull()
- currentInfo!!.assertVisibleItems(
- count = 1,
- expectedSize = IntSize(itemSizePx, itemSizePx),
- cells = 1
- )
- }
- }
-
- @Test
- fun totalCountIsCorrect() {
- var count by mutableStateOf(10)
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- reverseLayout = reverseLayout,
- state = rememberTvLazyGridState().also { state = it },
- ) {
- items((0 until count).toList()) { Box(Modifier.mainAxisSize(10.dp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
- count = 20
- }
-
- rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
- }
-
- @Test
- fun viewportOffsetsAndSizeAreCorrect() {
- val sizePx = 45
- val sizeDp = with(rule.density) { sizePx.toDp() }
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.axisSize(sizeDp * 2, sizeDp),
- reverseLayout = reverseLayout,
- state = rememberTvLazyGridState().also { state = it },
- ) {
- items((0..7).toList()) { Box(Modifier.mainAxisSize(sizeDp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (isVertical) {
- IntSize(sizePx * 2, sizePx)
- } else {
- IntSize(sizePx, sizePx * 2)
- }
- )
- }
- }
-
- @Test
- fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
- val sizePx = 45
- val startPaddingPx = 10
- val endPaddingPx = 15
- val sizeDp = with(rule.density) { sizePx.toDp() }
- val beforeContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
- }
- val afterContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
- }
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- modifier = Modifier.axisSize(sizeDp * 2, sizeDp),
- contentPadding =
- PaddingValues(
- beforeContent = beforeContentPaddingDp,
- afterContent = afterContentPaddingDp,
- beforeContentCrossAxis = 2.dp,
- afterContentCrossAxis = 2.dp
- ),
- reverseLayout = reverseLayout,
- state = rememberTvLazyGridState().also { state = it },
- ) {
- items((0..7).toList()) { Box(Modifier.mainAxisSize(sizeDp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (isVertical) {
- IntSize(sizePx * 2, sizePx)
- } else {
- IntSize(sizePx, sizePx * 2)
- }
- )
- }
- }
-
- @Test
- fun emptyItemsInVisibleItemsInfo() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(cells = 2, state = rememberTvLazyGridState().also { state = it }) {
- item { Box(Modifier) }
- item {}
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
- assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
- assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
- }
- }
-
- @Test
- fun emptyContent() {
- lateinit var state: TvLazyGridState
- val sizePx = 45
- val startPaddingPx = 10
- val endPaddingPx = 15
- val sizeDp = with(rule.density) { sizePx.toDp() }
- val beforeContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
- }
- val afterContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
- }
- rule.setContent {
- LazyGrid(
- cells = 1,
- modifier = Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- contentPadding =
- PaddingValues(
- beforeContent = beforeContentPaddingDp,
- afterContent = afterContentPaddingDp
- )
- ) {}
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
- assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
- )
- }
- }
-
- @Test
- fun viewportIsLargerThenTheContent() {
- lateinit var state: TvLazyGridState
- val sizePx = 45
- val startPaddingPx = 10
- val endPaddingPx = 15
- val sizeDp = with(rule.density) { sizePx.toDp() }
- val beforeContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
- }
- val afterContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
- }
- rule.setContent {
- LazyGrid(
- cells = 1,
- modifier = Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- contentPadding =
- PaddingValues(
- beforeContent = beforeContentPaddingDp,
- afterContent = afterContentPaddingDp
- )
- ) {
- item { Box(Modifier.size(sizeDp / 2)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
- assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
- )
- }
- }
-
- @Test
- fun reverseLayoutIsCorrect() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
- ) {
- items((0..11).toList()) { Box(Modifier.size(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle { assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout) }
- }
-
- @Test
- fun orientationIsCorrect() {
- lateinit var state: TvLazyGridState
- rule.setContent {
- LazyGrid(
- cells = 2,
- state = rememberTvLazyGridState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
- ) {
- items((0..11).toList()) { Box(Modifier.mainAxisSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.orientation == Orientation.Vertical).isEqualTo(isVertical)
- }
- }
-
- fun TvLazyGridLayoutInfo.assertVisibleItems(
- count: Int,
- cells: Int,
- startIndex: Int = 0,
- startOffset: Int = 0,
- expectedSize: IntSize = IntSize(itemSizePx, itemSizePx),
- spacing: Int = 0
- ) {
- assertThat(visibleItemsInfo.size).isEqualTo(count)
- if (count == 0) return
-
- assertThat(startIndex % cells).isEqualTo(0)
- assertThat(visibleItemsInfo.size % cells).isEqualTo(0)
-
- var currentIndex = startIndex
- var currentOffset = startOffset
- var currentLine = startIndex / cells
- var currentCell = 0
- visibleItemsInfo.forEach {
- assertThat(it.index).isEqualTo(currentIndex)
- assertWithMessage("Offset of item $currentIndex")
- .that(if (isVertical) it.offset.y else it.offset.x)
- .isEqualTo(currentOffset)
- assertThat(it.size).isEqualTo(expectedSize)
- assertThat(if (isVertical) it.row else it.column).isEqualTo(currentLine)
- assertThat(if (isVertical) it.column else it.row).isEqualTo(currentCell)
- currentIndex++
- currentCell++
- if (currentCell == cells) {
- currentCell = 0
- ++currentLine
- currentOffset += spacing + if (isVertical) it.size.height else it.size.width
- }
- }
- }
-}
-
-class LayoutInfoTestParam(val orientation: Orientation, val reverseLayout: Boolean) {
- override fun toString(): String {
- return "orientation=$orientation;reverseLayout=$reverseLayout"
- }
-}
-
-@Stable class StableRef<T>(var value: T)
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
deleted file mode 100644
index a4437f0..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ /dev/null
@@ -1,231 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.animation.core.snap
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertIsEqualTo
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import androidx.tv.foundation.lazy.grid.keyPress
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-
-open class BaseLazyListTestWithOrientation(private val orientation: Orientation) {
-
- @get:Rule val rule = createComposeRule()
-
- val vertical: Boolean
- get() = orientation == Orientation.Vertical
-
- fun Modifier.mainAxisSize(size: Dp) =
- if (vertical) {
- this.height(size)
- } else {
- this.width(size)
- }
-
- fun Modifier.crossAxisSize(size: Dp) =
- if (vertical) {
- this.width(size)
- } else {
- this.height(size)
- }
-
- fun Modifier.fillMaxCrossAxis() =
- if (vertical) {
- this.fillMaxWidth()
- } else {
- this.fillMaxHeight()
- }
-
- fun TvLazyListItemScope.fillParentMaxMainAxis() =
- if (vertical) {
- Modifier.fillParentMaxHeight()
- } else {
- Modifier.fillParentMaxWidth()
- }
-
- fun TvLazyListItemScope.fillParentMaxCrossAxis() =
- if (vertical) {
- Modifier.fillParentMaxWidth()
- } else {
- Modifier.fillParentMaxHeight()
- }
-
- fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertHeightIsEqualTo(expectedSize)
- } else {
- assertWidthIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
- if (vertical) {
- assertWidthIsEqualTo(expectedSize)
- } else {
- assertHeightIsEqualTo(expectedSize)
- }
-
- fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
- val position =
- if (vertical) {
- getUnclippedBoundsInRoot().top
- } else {
- getUnclippedBoundsInRoot().left
- }
- position.assertIsEqualTo(expected, tolerance = 2.dp)
- }
-
- fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertTopPositionInRootIsEqualTo(expectedStart)
- } else {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- }
-
- fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
- if (vertical) {
- assertLeftPositionInRootIsEqualTo(expectedStart)
- } else {
- assertTopPositionInRootIsEqualTo(expectedStart)
- }
-
- fun PaddingValues(mainAxis: Dp = 0.dp, crossAxis: Dp = 0.dp) =
- PaddingValues(
- beforeContent = mainAxis,
- afterContent = mainAxis,
- beforeContentCrossAxis = crossAxis,
- afterContentCrossAxis = crossAxis
- )
-
- fun PaddingValues(
- beforeContent: Dp = 0.dp,
- afterContent: Dp = 0.dp,
- beforeContentCrossAxis: Dp = 0.dp,
- afterContentCrossAxis: Dp = 0.dp,
- ) =
- if (vertical) {
- PaddingValues(
- start = beforeContentCrossAxis,
- top = beforeContent,
- end = afterContentCrossAxis,
- bottom = afterContent
- )
- } else {
- PaddingValues(
- start = beforeContent,
- top = beforeContentCrossAxis,
- end = afterContent,
- bottom = afterContentCrossAxis
- )
- }
-
- fun TvLazyListState.scrollBy(offset: Dp) {
- runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
- animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
- }
- }
-
- fun TvLazyListState.scrollTo(index: Int) {
- runBlocking(Dispatchers.Main + AutoTestFrameClock()) { scrollToItem(index) }
- }
-
- fun ComposeContentTestRule.keyPress(numberOfKeyPresses: Int, reverseScroll: Boolean = false) {
- val keyCode: Int =
- when {
- vertical && reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_UP
- vertical && !reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_DOWN
- !vertical && reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_LEFT
- !vertical && !reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_RIGHT
- else -> NativeKeyEvent.KEYCODE_DPAD_RIGHT
- }
-
- keyPress(keyCode, numberOfKeyPresses)
- }
-
- @Composable
- fun LazyColumnOrRow(
- modifier: Modifier = Modifier,
- state: TvLazyListState = rememberTvLazyListState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- userScrollEnabled: Boolean = true,
- spacedBy: Dp = 0.dp,
- pivotOffsets: PivotOffsets = PivotOffsets(parentFraction = 0f),
- content: TvLazyListScope.() -> Unit
- ) {
- if (vertical) {
- val verticalArrangement =
- when {
- spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
- !reverseLayout -> Arrangement.Top
- else -> Arrangement.Bottom
- }
- TvLazyColumn(
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- verticalArrangement = verticalArrangement,
- pivotOffsets = pivotOffsets,
- content = content
- )
- } else {
- val horizontalArrangement =
- when {
- spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
- !reverseLayout -> Arrangement.Start
- else -> Arrangement.End
- }
- TvLazyRow(
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- horizontalArrangement = horizontalArrangement,
- pivotOffsets = pivotOffsets,
- content = content
- )
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyArrangementsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyArrangementsTest.kt
deleted file mode 100644
index a4b66ce..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyArrangementsTest.kt
+++ /dev/null
@@ -1,641 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.grid.keyPress
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-
-class LazyArrangementsTest {
-
- private val ContainerTag = "ContainerTag"
-
- @get:Rule val rule = createComposeRule()
-
- private var itemSize: Dp = Dp.Infinity
- private var smallerItemSize: Dp = Dp.Infinity
- private var containerSize: Dp = Dp.Infinity
-
- @Before
- fun before() {
- with(rule.density) { itemSize = 50.toDp() }
- with(rule.density) { smallerItemSize = 40.toDp() }
- containerSize = itemSize * 5
- }
-
- // cases when we have not enough items to fill min constraints:
-
- @Test
- fun column_defaultArrangementIsTop() {
- rule.setContent {
- TvLazyColumn(
- modifier = Modifier.requiredSize(containerSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Top)
- }
-
- @Test
- fun column_centerArrangement() {
- composeColumnWith(Arrangement.Center)
- assertArrangementForTwoItems(Arrangement.Center)
- }
-
- @Test
- fun column_bottomArrangement() {
- composeColumnWith(Arrangement.Bottom)
- assertArrangementForTwoItems(Arrangement.Bottom)
- }
-
- @Test
- fun column_spacedArrangementNotFillingViewport() {
- val arrangement = Arrangement.spacedBy(10.dp)
- composeColumnWith(arrangement)
- assertArrangementForTwoItems(arrangement)
- }
-
- @Test
- fun row_defaultArrangementIsStart() {
- rule.setContent {
- TvLazyRow(
- modifier = Modifier.requiredSize(containerSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
- }
-
- @Test
- fun row_centerArrangement() {
- composeRowWith(Arrangement.Center, LayoutDirection.Ltr)
- assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
- }
-
- @Test
- fun row_endArrangement() {
- composeRowWith(Arrangement.End, LayoutDirection.Ltr)
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
- }
-
- @Test
- fun row_spacedArrangementNotFillingViewport() {
- val arrangement = Arrangement.spacedBy(10.dp)
- composeRowWith(arrangement, LayoutDirection.Ltr)
- assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
- }
-
- @Test
- fun row_rtl_startArrangement() {
- composeRowWith(Arrangement.Center, LayoutDirection.Rtl)
- assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
- }
-
- @Test
- fun row_rtl_endArrangement() {
- composeRowWith(Arrangement.End, LayoutDirection.Rtl)
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
- }
-
- @Test
- fun row_rtl_spacedArrangementNotFillingViewport() {
- val arrangement = Arrangement.spacedBy(10.dp)
- composeRowWith(arrangement, LayoutDirection.Rtl)
- assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
- }
-
- // wrap content and spacing
-
- @Test
- fun column_spacing_affects_wrap_content() {
- rule.setContent {
- TvLazyColumn(
- verticalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize).focusable()) }
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertWidthIsEqualTo(itemSize)
- .assertHeightIsEqualTo(itemSize * 3)
- }
-
- @Test
- fun row_spacing_affects_wrap_content() {
- rule.setContent {
- TvLazyRow(
- horizontalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Box(Modifier.requiredSize(itemSize).focusable()) }
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertWidthIsEqualTo(itemSize * 3)
- .assertHeightIsEqualTo(itemSize)
- }
-
- // spacing added when we have enough items to fill the viewport
-
- @Test
- fun column_spacing_scrolledToTheTop() {
- rule.setContent {
- TvLazyColumn(
- verticalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize * 2)
- }
-
- @Test
- fun column_spacing_scrolledToTheBottom() {
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- verticalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
- }
-
- @Test
- fun row_spacing_scrolledToTheStart() {
- rule.setContent {
- TvLazyRow(
- horizontalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize * 2)
- }
-
- @Test
- fun row_spacing_scrolledToTheEnd() {
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- horizontalArrangement = Arrangement.spacedBy(itemSize),
- modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(3) { Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable()) }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
-
- rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
- }
-
- @Test
- fun column_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- Modifier.size(itemSize * 3),
- state = rememberTvLazyListState().also { state = it },
- verticalArrangement = Arrangement.spacedBy(spacingSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollBy((itemSizePx + spacingSizePx).toFloat()) } }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun column_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- Modifier.size(itemSize * 3),
- state = rememberTvLazyListState().also { state = it },
- verticalArrangement = Arrangement.spacedBy(spacingSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat()) }
- }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(itemSizePx + spacingSizePx / 2)
- }
- }
-
- @Test
- fun row_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- Modifier.size(itemSize * 3),
- state = rememberTvLazyListState().also { state = it },
- horizontalArrangement = Arrangement.spacedBy(spacingSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(5) { Box(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollBy((itemSizePx + spacingSizePx).toFloat()) } }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun row_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
- val itemSizePx = 30
- val spacingSizePx = 4
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- val spacingSize = with(rule.density) { spacingSizePx.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- Modifier.size(itemSize * 3),
- state = rememberTvLazyListState().also { state = it },
- horizontalArrangement = Arrangement.spacedBy(spacingSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(5) { Box(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat()) }
- }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(itemSizePx + spacingSizePx / 2)
- }
- }
-
- // with reverseLayout == true
-
- @Test
- fun column_defaultArrangementIsBottomWithReverseLayout() {
- rule.setContent {
- TvLazyColumn(
- reverseLayout = true,
- modifier = Modifier.requiredSize(containerSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
- }
-
- @Test
- fun row_defaultArrangementIsEndWithReverseLayout() {
- rule.setContent {
- TvLazyRow(
- reverseLayout = true,
- modifier = Modifier.requiredSize(containerSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr, reverseLayout = true)
- }
-
- @Test
- fun column_whenArrangementChanges() {
- var arrangement by mutableStateOf(Arrangement.Top)
- rule.setContent {
- TvLazyColumn(
- modifier = Modifier.requiredSize(containerSize),
- verticalArrangement = arrangement,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Top)
-
- rule.runOnIdle { arrangement = Arrangement.Bottom }
-
- assertArrangementForTwoItems(Arrangement.Bottom)
- }
-
- @Test
- fun row_whenArrangementChanges() {
- var arrangement by mutableStateOf(Arrangement.Start)
- rule.setContent {
- TvLazyRow(
- modifier = Modifier.requiredSize(containerSize),
- horizontalArrangement = arrangement,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Item(it) }
- }
- }
-
- assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
-
- rule.runOnIdle { arrangement = Arrangement.End }
-
- assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
- }
-
- @Test
- fun column_negativeSpacing_itemsVisible() {
- val state = TvLazyListState()
- val halfItemSize = itemSize / 2
- rule.setContent {
- TvLazyColumn(
- modifier = Modifier.requiredSize(itemSize),
- verticalArrangement = Arrangement.spacedBy(-halfItemSize),
- state = state
- ) {
- items(100) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(halfItemSize)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-
- runBlocking { state.scrollBy(with(rule.density) { halfItemSize.toPx() }) }
-
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(-halfItemSize)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun row_negativeSpacing_itemsVisible() {
- val state = TvLazyListState()
- val halfItemSize = itemSize / 2
- rule.setContent {
- TvLazyRow(
- modifier = Modifier.requiredSize(itemSize),
- horizontalArrangement = Arrangement.spacedBy(-halfItemSize),
- state = state
- ) {
- items(100) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(halfItemSize)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-
- runBlocking { state.scrollBy(with(rule.density) { halfItemSize.toPx() }) }
-
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(-halfItemSize)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun column_negativeSpacingLargerThanItem_itemsVisible() {
- val state = TvLazyListState(firstVisibleItemIndex = 2)
- val largerThanItemSize = itemSize * 1.5f
- rule.setContent {
- TvLazyColumn(
- modifier = Modifier.requiredSize(itemSize),
- verticalArrangement = Arrangement.spacedBy(-largerThanItemSize),
- state = state
- ) {
- items(4) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- repeat(4) { rule.onNodeWithTag("$it").assertTopPositionInRootIsEqualTo(0.dp) }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun row_negativeSpacingLargerThanItem_itemsVisible() {
- val state = TvLazyListState(firstVisibleItemIndex = 2)
- val largerThanItemSize = itemSize * 1.5f
- rule.setContent {
- TvLazyRow(
- modifier = Modifier.requiredSize(itemSize),
- horizontalArrangement = Arrangement.spacedBy(-largerThanItemSize),
- state = state
- ) {
- items(4) { index -> Box(Modifier.size(itemSize).testTag(index.toString())) }
- }
- }
-
- repeat(4) { rule.onNodeWithTag("$it").assertLeftPositionInRootIsEqualTo(0.dp) }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- fun composeColumnWith(arrangement: Arrangement.Vertical) {
- rule.setContent {
- TvLazyColumn(
- verticalArrangement = arrangement,
- modifier = Modifier.requiredSize(containerSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Item(it) }
- }
- }
- }
-
- fun composeRowWith(arrangement: Arrangement.Horizontal, layoutDirection: LayoutDirection) {
- rule.setContent {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- TvLazyRow(
- horizontalArrangement = arrangement,
- modifier = Modifier.requiredSize(containerSize),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) { Item(it) }
- }
- }
- }
- }
-
- @Composable
- fun Item(index: Int) {
- require(index < 2)
- val size = if (index == 0) itemSize else smallerItemSize
- Box(Modifier.requiredSize(size).testTag(index.toString()))
- }
-
- fun assertArrangementForTwoItems(
- arrangement: Arrangement.Vertical,
- reverseLayout: Boolean = false
- ) {
- with(rule.density) {
- val sizes =
- IntArray(2) {
- val index = if (reverseLayout) if (it == 0) 1 else 0 else it
- if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
- }
- val outPositions = IntArray(2) { 0 }
- with(arrangement) { arrange(containerSize.roundToPx(), sizes, outPositions) }
-
- outPositions.forEachIndexed { index, position ->
- val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
- rule.onNodeWithTag("$realIndex").assertTopPositionInRootIsEqualTo(position.toDp())
- }
- }
- }
-
- fun assertArrangementForTwoItems(
- arrangement: Arrangement.Horizontal,
- layoutDirection: LayoutDirection,
- reverseLayout: Boolean = false
- ) {
- with(rule.density) {
- val sizes =
- IntArray(2) {
- val index = if (reverseLayout) if (it == 0) 1 else 0 else it
- if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
- }
- val outPositions = IntArray(2) { 0 }
- with(arrangement) {
- arrange(containerSize.roundToPx(), sizes, layoutDirection, outPositions)
- }
-
- outPositions.forEachIndexed { index, position ->
- val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
- val expectedPosition = position.toDp()
- rule.onNodeWithTag("$realIndex").assertLeftPositionInRootIsEqualTo(expectedPosition)
- }
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
deleted file mode 100644
index af734bf..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
+++ /dev/null
@@ -1,437 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import android.os.Build
-import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.assertPixels
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.collections.removeLast as removeLastKt
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-/**
- * This class contains all LazyColumn-specific tests, as well as (by convention) tests that don't
- * need to be run in both orientations.
- *
- * To have a test run in both orientations (LazyRow and LazyColumn), add it to [LazyListTest]
- */
-class LazyColumnTest {
- private val LazyListTag = "LazyListTag"
-
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun compositionsAreDisposed_whenDataIsChanged() {
- var composed = 0
- var disposals = 0
- val data1 = (1..3).toList()
- val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
-
- var part2 by mutableStateOf(false)
-
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- Modifier.testTag(LazyListTag).fillMaxSize(),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(if (!part2) data1 else data2) {
- DisposableEffect(NeverEqualObject) {
- composed++
- onDispose { disposals++ }
- }
-
- Box(Modifier.height(50.dp).focusable())
- }
- }
- }
-
- rule.runOnIdle {
- assertWithMessage("Not all items were composed").that(composed).isEqualTo(data1.size)
- composed = 0
-
- part2 = true
- }
-
- rule.runOnIdle {
- assertWithMessage(
- "No additional items were composed after data change, something didn't work"
- )
- .that(composed)
- .isEqualTo(data2.size)
-
- // We may need to modify this test once we prefetch/cache items outside the viewport
- assertWithMessage(
- "Not enough compositions were disposed after scrolling, compositions were leaked"
- )
- .that(disposals)
- .isEqualTo(data1.size)
- }
- }
-
- @Test
- fun compositionsAreDisposed_whenLazyListIsDisposed() {
- var emitLazyList by mutableStateOf(true)
- var disposeCalledOnFirstItem = false
- var disposeCalledOnSecondItem = false
-
- rule.setContentWithTestViewConfiguration {
- if (emitLazyList) {
- TvLazyColumn(
- Modifier.fillMaxSize(),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) {
- Box(Modifier.requiredSize(100.dp).focusable())
- DisposableEffect(Unit) {
- onDispose {
- if (it == 1) {
- disposeCalledOnFirstItem = true
- } else {
- disposeCalledOnSecondItem = true
- }
- }
- }
- }
- }
- }
- }
-
- rule.runOnIdle {
- assertWithMessage("First item was incorrectly immediately disposed")
- .that(disposeCalledOnFirstItem)
- .isFalse()
- assertWithMessage("Second item was incorrectly immediately disposed")
- .that(disposeCalledOnFirstItem)
- .isFalse()
- emitLazyList = false
- }
-
- rule.runOnIdle {
- assertWithMessage("First item was not correctly disposed")
- .that(disposeCalledOnFirstItem)
- .isTrue()
- assertWithMessage("Second item was not correctly disposed")
- .that(disposeCalledOnSecondItem)
- .isTrue()
- }
- }
-
- @Test
- fun removeItemsTest() {
- var itemCount by mutableStateOf(3)
- val tag = "List"
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(Modifier.testTag(tag)) {
- items((0 until itemCount).toList()) { BasicText("$it") }
- }
- }
-
- while (itemCount >= 0) {
- // Confirm the children's content
- for (i in 0 until 3) {
- rule.onNodeWithText("$i").apply {
- if (i < itemCount) {
- assertIsPlaced()
- } else {
- assertIsNotPlaced()
- }
- }
- }
- itemCount--
- }
- }
-
- @Test
- fun changeItemsCountAndScrollImmediately() {
- lateinit var state: TvLazyListState
- var count by mutableStateOf(100)
- val composedIndexes = mutableListOf<Int>()
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.fillMaxWidth().height(10.dp),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(count) { index ->
- composedIndexes.add(index)
- Box(Modifier.size(20.dp).focusable())
- }
- }
- }
-
- rule.runOnIdle {
- composedIndexes.clear()
- count = 10
- runBlocking(AutoTestFrameClock()) { state.scrollToItem(50) }
- composedIndexes.forEach { assertThat(it).isLessThan(count) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(9)
- }
- }
-
- @Test
- fun changingDataTest() {
- val dataLists = listOf((1..3).toList(), (4..8).toList(), (3..4).toList())
- var dataModel by mutableStateOf(dataLists[0])
- val tag = "List"
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(Modifier.testTag(tag), pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- items(dataModel) { BasicText("$it") }
- }
- }
-
- for (data in dataLists) {
- rule.runOnIdle { dataModel = data }
-
- // Confirm the children's content
- for (index in 1..8) {
- if (index in data) {
- rule.onNodeWithText("$index").assertIsDisplayed()
- } else {
- rule.onNodeWithText("$index").assertIsNotPlaced()
- }
- }
- }
- }
-
- private val firstItemTag = "firstItemTag"
- private val secondItemTag = "secondItemTag"
-
- private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- Modifier.testTag(LazyListTag).requiredWidth(100.dp),
- horizontalAlignment = horizontalGravity,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(listOf(1, 2)) {
- if (it == 1) {
- Box(Modifier.size(50.dp).testTag(firstItemTag).focusable())
- } else {
- Box(Modifier.size(70.dp).testTag(secondItemTag).focusable())
- }
- }
- }
- }
-
- rule.onNodeWithTag(firstItemTag).assertIsDisplayed()
-
- rule.onNodeWithTag(secondItemTag).assertIsDisplayed()
-
- val lazyColumnBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
-
- with(rule.density) {
- // Verify the width of the column
- assertThat(lazyColumnBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
- assertThat(lazyColumnBounds.right.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
- }
- }
-
- @Test
- fun lazyColumnAlignmentCenterHorizontally() {
- prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
-
- rule.onNodeWithTag(firstItemTag).assertPositionInRootIsEqualTo(25.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag).assertPositionInRootIsEqualTo(15.dp, 50.dp)
- }
-
- @Test
- fun lazyColumnAlignmentStart() {
- prepareLazyColumnsItemsAlignment(Alignment.Start)
-
- rule.onNodeWithTag(firstItemTag).assertPositionInRootIsEqualTo(0.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag).assertPositionInRootIsEqualTo(0.dp, 50.dp)
- }
-
- @Test
- fun lazyColumnAlignmentEnd() {
- prepareLazyColumnsItemsAlignment(Alignment.End)
-
- rule.onNodeWithTag(firstItemTag).assertPositionInRootIsEqualTo(50.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag).assertPositionInRootIsEqualTo(30.dp, 50.dp)
- }
-
- @FlakyTest(bugId = 259297305)
- @Test
- fun removalWithMutableStateListOf() {
- val items = mutableStateListOf("1", "2", "3")
-
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn { items(items) { item -> Spacer(Modifier.size(itemSize).testTag(item)) } }
- }
-
- rule.runOnIdle { items.removeLastKt() }
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertIsNotPlaced()
- }
-
- @Test
- fun recompositionOrder() {
- val outerState = mutableStateOf(0)
- val innerState = mutableStateOf(0)
- val recompositions = mutableListOf<Pair<Int, Int>>()
-
- rule.setContent {
- val localOuterState = outerState.value
- TvLazyColumn {
- items(count = 1) {
- recompositions.add(localOuterState to innerState.value)
- Box(Modifier.fillMaxSize())
- }
- }
- }
-
- rule.runOnIdle {
- innerState.value++
- outerState.value++
- }
-
- rule.runOnIdle { assertThat(recompositions).isEqualTo(listOf(0 to 0, 1 to 1)) }
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- @Test
- fun scrolledAwayItemIsNotDisplayedAnymore() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.requiredSize(10.dp)
- .testTag(LazyListTag)
- .graphicsLayer()
- .background(Color.Blue),
- state = state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(2) {
- val size = if (it == 0) 5.dp else 100.dp
- val color = if (it == 0) Color.Red else Color.Transparent
- Box(
- Modifier.fillMaxWidth()
- .height(size)
- .background(color)
- .testTag("$it")
- .focusable()
- )
- }
- }
- }
-
- rule.runOnIdle {
- with(rule.density) {
- runBlocking {
- // we scroll enough to make the Red item not visible anymore
- state.scrollBy(6.dp.toPx())
- }
- }
- }
-
- // and verify there is no Red item displayed
- rule.onNodeWithTag(LazyListTag).captureToImage().assertPixels { Color.Blue }
- }
-
- @Test
- fun wrappedNestedLazyRowDisplayCorrectContent() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.size(20.dp),
- state = state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) { LazyRowWrapped { BasicText("$it", Modifier.size(21.dp)) } }
- }
- }
-
- (1..10).forEach { item ->
- rule.runOnIdle { runBlocking { state.scrollToItem(item) } }
-
- rule.onNodeWithText("$item").assertIsDisplayed()
- }
- }
-
- @Composable
- private fun LazyRowWrapped(content: @Composable () -> Unit) {
- TvLazyRow { items(count = 1) { content() } }
- }
-}
-
-internal fun Modifier.drawOutsideOfBounds() = drawBehind {
- val inflate = 20.dp.roundToPx().toFloat()
- drawRect(
- Color.Red,
- Offset(-inflate, -inflate),
- Size(size.width + inflate * 2, size.height + inflate * 2)
- )
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyCustomKeysTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyCustomKeysTest.kt
deleted file mode 100644
index 51f3155..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyCustomKeysTest.kt
+++ /dev/null
@@ -1,386 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.PivotOffsets
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyCustomKeysTest {
-
- @get:Rule val rule = createComposeRule()
-
- val itemSize = with(rule.density) { 100.toDp() }
-
- @Test
- fun itemsWithKeysAreLaidOutCorrectly() {
- val list = listOf(MyClass(0), MyClass(1), MyClass(2))
-
- rule.setContent { TvLazyColumn { items(list, key = { it.id }) { Item("${it.id}") } } }
-
- assertItems("0", "1", "2")
- }
-
- @Test
- fun removing_statesAreMoved() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
-
- rule.setContent {
- TvLazyColumn { items(list, key = { it.id }) { Item(remember { "${it.id}" }) } }
- }
-
- rule.runOnIdle { list = listOf(list[0], list[2]) }
-
- assertItems("0", "2")
- }
-
- @Test
- fun reordering_statesAreMoved_list() {
- testReordering { list -> items(list, key = { it.id }) { Item(remember { "${it.id}" }) } }
- }
-
- @Test
- fun reordering_statesAreMoved_list_indexed() {
- testReordering { list ->
- itemsIndexed(list, key = { _, item -> item.id }) { _, item ->
- Item(remember { "${item.id}" })
- }
- }
- }
-
- @Test
- fun reordering_statesAreMoved_array() {
- testReordering { list ->
- val array = list.toTypedArray()
- items(array, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- @Test
- fun reordering_statesAreMoved_array_indexed() {
- testReordering { list ->
- val array = list.toTypedArray()
- itemsIndexed(array, key = { _, item -> item.id }) { _, item ->
- Item(remember { "${item.id}" })
- }
- }
- }
-
- @Test
- fun reordering_statesAreMoved_itemsWithCount() {
- testReordering { list ->
- items(list.size, key = { list[it].id }) { Item(remember { "${list[it].id}" }) }
- }
- }
-
- @Test
- fun fullyReplacingTheList() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- var counter = 0
-
- rule.setContent {
- TvLazyColumn {
- items(list, key = { it.id }) { Item(remember { counter++ }.toString()) }
- }
- }
-
- rule.runOnIdle { list = listOf(MyClass(3), MyClass(4), MyClass(5), MyClass(6)) }
-
- assertItems("3", "4", "5", "6")
- }
-
- @Test
- fun keepingOneItem() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- var counter = 0
-
- rule.setContent {
- TvLazyColumn {
- items(list, key = { it.id }) { Item(remember { counter++ }.toString()) }
- }
- }
-
- rule.runOnIdle { list = listOf(MyClass(1)) }
-
- assertItems("1")
- }
-
- @Test
- fun keepingOneItemAndAddingMore() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- var counter = 0
-
- rule.setContent {
- TvLazyColumn {
- items(list, key = { it.id }) { Item(remember { counter++ }.toString()) }
- }
- }
-
- rule.runOnIdle { list = listOf(MyClass(1), MyClass(3)) }
-
- assertItems("1", "3")
- }
-
- @Test
- fun mixingKeyedItemsAndNot() {
- testReordering { list ->
- item { Item("${list.first().id}") }
- items(list.subList(fromIndex = 1, toIndex = list.size), key = { it.id }) {
- Item(remember { "${it.id}" })
- }
- }
- }
-
- @Test
- fun updatingTheDataSetIsCorrectlyApplied() {
- val state = mutableStateOf(emptyList<Int>())
-
- rule.setContent {
- LaunchedEffect(Unit) { state.value = listOf(4, 1, 3) }
-
- val list = state.value
-
- TvLazyColumn(Modifier.fillMaxSize(), pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- items(list, key = { it }) { Item(it.toString()) }
- }
- }
-
- assertItems("4", "1", "3")
-
- rule.runOnIdle { state.value = listOf(2, 4, 6, 1, 3, 5) }
-
- assertItems("2", "4", "6", "1", "3", "5")
- }
-
- @Test
- fun reordering_usingMutableStateListOf() {
- val list = mutableStateListOf(MyClass(0), MyClass(1), MyClass(2))
-
- rule.setContent {
- TvLazyColumn { items(list, key = { it.id }) { Item(remember { "${it.id}" }) } }
- }
-
- rule.runOnIdle { list.add(list.removeAt(1)) }
-
- assertItems("0", "2", "1")
- }
-
- @Test
- fun keysInLazyListItemInfoAreCorrect() {
- val list = listOf(MyClass(0), MyClass(1), MyClass(2))
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(state = state, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- items(list, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- rule.runOnIdle { assertThat(state.visibleKeys).isEqualTo(listOf(0, 1, 2)) }
- }
-
- @Test
- fun keysInLazyListItemInfoAreCorrectAfterReordering() {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(state = state, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- items(list, key = { it.id }) { Item(remember { "${it.id}" }) }
- }
- }
-
- rule.runOnIdle { list = listOf(list[0], list[2], list[1]) }
-
- rule.runOnIdle { assertThat(state.visibleKeys).isEqualTo(listOf(0, 2, 1)) }
- }
-
- @Test
- fun addingItemsBeforeWithoutKeysIsMaintainingTheIndex() {
- var list by mutableStateOf((10..15).toList())
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.size(itemSize * 2.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(list) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..15).toList() }
-
- rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(0) }
- }
-
- @Test
- fun addingItemsBeforeKeepingThisItemFirst() {
- var list by mutableStateOf((10..15).toList())
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.size(itemSize * 2.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..15).toList() }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(10)
- assertThat(state.visibleKeys).isEqualTo(listOf(10, 11, 12))
- }
- }
-
- @Test
- fun addingItemsRightAfterKeepingThisItemFirst() {
- var list by mutableStateOf((0..5).toList() + (10..15).toList())
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState(5)
- TvLazyColumn(
- Modifier.size(itemSize * 2.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..15).toList() }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(5)
- assertThat(state.visibleKeys).isEqualTo(listOf(5, 6, 7))
- }
- }
-
- @Test
- fun addingItemsBeforeWhileCurrentItemIsNotInTheBeginning() {
- var list by mutableStateOf((10..30).toList())
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState(10) // key 20 is the first item
- TvLazyColumn(
- Modifier.size(itemSize * 2.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..30).toList() }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(20)
- assertThat(state.visibleKeys).isEqualTo(listOf(20, 21, 22))
- }
- }
-
- @Test
- fun removingTheCurrentItemMaintainsTheIndex() {
- var list by mutableStateOf((0..20).toList())
- lateinit var state: TvLazyListState
-
- rule.setContent {
- state = rememberTvLazyListState(5)
- TvLazyColumn(
- Modifier.size(itemSize * 2.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(list, key = { it }) { Item(remember { "$it" }) }
- }
- }
-
- rule.runOnIdle { list = (0..20) - 5 }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(5)
- assertThat(state.visibleKeys).isEqualTo(listOf(6, 7, 8))
- }
- }
-
- private fun testReordering(content: TvLazyListScope.(List<MyClass>) -> Unit) {
- var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
-
- rule.setContent { TvLazyColumn { content(list) } }
-
- rule.runOnIdle { list = listOf(list[0], list[2], list[1]) }
-
- assertItems("0", "2", "1")
- }
-
- private fun assertItems(vararg tags: String) {
- var currentTop = 0.dp
- tags.forEach {
- rule
- .onNodeWithTag(it)
- .assertTopPositionInRootIsEqualTo(currentTop)
- .assertHeightIsEqualTo(itemSize)
- currentTop += itemSize
- }
- }
-
- @Composable
- private fun Item(tag: String) {
- Spacer(Modifier.testTag(tag).size(itemSize))
- }
-
- private class MyClass(val id: Int)
-}
-
-val TvLazyListState.visibleKeys: List<Any>
- get() = layoutInfo.visibleItemsInfo.map { it.key }
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
deleted file mode 100644
index 5325ab6..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
+++ /dev/null
@@ -1,1493 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.requiredWidthIn
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntRect
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
-import androidx.test.filters.LargeTest
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.math.roundToInt
-import kotlinx.coroutines.runBlocking
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTvFoundationApi::class)
-class LazyListAnimateItemPlacementTest(private val config: Config) {
-
- private val isVertical: Boolean
- get() = config.isVertical
-
- private val reverseLayout: Boolean
- get() = config.reverseLayout
-
- @get:Rule val rule = createComposeRule()
-
- private val itemSize: Float = 50f
- private var itemSizeDp: Dp = Dp.Infinity
- private val itemSize2: Float = 30f
- private var itemSize2Dp: Dp = Dp.Infinity
- private val itemSize3: Float = 20f
- private var itemSize3Dp: Dp = Dp.Infinity
- private val containerSize: Float = itemSize * 5
- private var containerSizeDp: Dp = Dp.Infinity
- private val spacing: Float = 10f
- private var spacingDp: Dp = Dp.Infinity
- private val itemSizePlusSpacing = itemSize + spacing
- private var itemSizePlusSpacingDp = Dp.Infinity
- private lateinit var state: TvLazyListState
-
- @Before
- fun before() {
- rule.mainClock.autoAdvance = false
- with(rule.density) {
- itemSizeDp = itemSize.toDp()
- itemSize2Dp = itemSize2.toDp()
- itemSize3Dp = itemSize3.toDp()
- containerSizeDp = containerSize.toDp()
- spacingDp = spacing.toDp()
- itemSizePlusSpacingDp = itemSizePlusSpacing.toDp()
- }
- }
-
- @Test
- fun reorderTwoItems() {
- var list by mutableStateOf(listOf(0, 1))
- rule.setContent { LazyList { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(0 to 0f, 1 to itemSize)
-
- rule.runOnUiThread { list = listOf(1, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0 + itemSize * fraction,
- 1 to itemSize - itemSize * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun reorderTwoItems_layoutInfoHasFinalPositions() {
- var list by mutableStateOf(listOf(0, 1))
- rule.setContent { LazyList { items(list, key = { it }) { Item(it) } } }
-
- assertLayoutInfoPositions(0 to 0f, 1 to itemSize)
-
- rule.runOnUiThread { list = listOf(1, 0) }
-
- onAnimationFrame {
- // fraction doesn't affect the offsets in layout info
- assertLayoutInfoPositions(1 to 0f, 0 to itemSize)
- }
- }
-
- @Test
- fun reorderFirstAndLastItems() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent { LazyList { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(
- 0 to 0f,
- 1 to itemSize,
- 2 to itemSize * 2,
- 3 to itemSize * 3,
- 4 to itemSize * 4,
- )
-
- rule.runOnUiThread { list = listOf(4, 1, 2, 3, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0 + itemSize * 4 * fraction,
- 1 to itemSize,
- 2 to itemSize * 2,
- 3 to itemSize * 3,
- 4 to itemSize * 4 - itemSize * 4 * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveFirstItemToEndCausingAllItemsToAnimate() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent { LazyList { items(list, key = { it }) { Item(it) } } }
-
- assertPositions(
- 0 to 0f,
- 1 to itemSize,
- 2 to itemSize * 2,
- 3 to itemSize * 3,
- 4 to itemSize * 4,
- )
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0 + itemSize * 4 * fraction,
- 1 to itemSize - itemSize * fraction,
- 2 to itemSize * 2 - itemSize * fraction,
- 3 to itemSize * 3 - itemSize * fraction,
- 4 to itemSize * 4 - itemSize * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun itemSizeChangeAnimatesNextItems() {
- var size by mutableStateOf(itemSizeDp)
- rule.setContent {
- LazyList(minSize = itemSizeDp * 5, maxSize = itemSizeDp * 5) {
- items(listOf(0, 1, 2, 3), key = { it }) {
- Item(it, size = if (it == 1) size else itemSizeDp)
- }
- }
- }
-
- rule.runOnUiThread { size = itemSizeDp * 2 }
- rule.mainClock.advanceTimeByFrame()
-
- rule.onNodeWithTag("1").assertMainAxisSizeIsEqualTo(size)
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0f,
- 1 to itemSize,
- 2 to itemSize * 2 + itemSize * fraction,
- 3 to itemSize * 3 + itemSize * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun onlyItemsWithModifierAnimates() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent {
- LazyList {
- items(list, key = { it }) {
- Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
- }
- }
- }
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to itemSize * 4,
- 1 to itemSize - itemSize * fraction,
- 2 to itemSize,
- 3 to itemSize * 3 - itemSize * fraction,
- 4 to itemSize * 3,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animationsWithDifferentDurations() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent {
- LazyList {
- items(list, key = { it }) {
- val duration = if (it == 1 || it == 3) Duration * 2 else Duration
- Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
- }
- }
- }
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 4, 0) }
-
- onAnimationFrame(duration = Duration * 2) { fraction ->
- val shorterAnimFraction = (fraction * 2).coerceAtMost(1f)
- assertPositions(
- 0 to 0 + itemSize * 4 * shorterAnimFraction,
- 1 to itemSize - itemSize * fraction,
- 2 to itemSize * 2 - itemSize * shorterAnimFraction,
- 3 to itemSize * 3 - itemSize * fraction,
- 4 to itemSize * 4 - itemSize * shorterAnimFraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun multipleChildrenPerItem() {
- var list by mutableStateOf(listOf(0, 2))
- rule.setContent {
- LazyList {
- items(list, key = { it }) {
- Item(it)
- Item(it + 1)
- }
- }
- }
-
- assertPositions(
- 0 to 0f,
- 1 to itemSize,
- 2 to itemSize * 2,
- 3 to itemSize * 3,
- )
-
- rule.runOnUiThread { list = listOf(2, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0 + itemSize * 2 * fraction,
- 1 to itemSize + itemSize * 2 * fraction,
- 2 to itemSize * 2 - itemSize * 2 * fraction,
- 3 to itemSize * 3 - itemSize * 2 * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun multipleChildrenPerItemSomeDoNotAnimate() {
- var list by mutableStateOf(listOf(0, 2))
- rule.setContent {
- LazyList {
- items(list, key = { it }) {
- Item(it)
- Item(it + 1, animSpec = null)
- }
- }
- }
-
- rule.runOnUiThread { list = listOf(2, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0 + itemSize * 2 * fraction,
- 1 to itemSize * 3,
- 2 to itemSize * 2 - itemSize * 2 * fraction,
- 3 to itemSize,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animateArrangementChange() {
- var arrangement by mutableStateOf(Arrangement.Center)
- rule.setContent {
- LazyList(
- arrangement = arrangement,
- minSize = itemSizeDp * 5,
- maxSize = itemSizeDp * 5
- ) {
- items(listOf(1, 2, 3), key = { it }) { Item(it) }
- }
- }
-
- assertPositions(
- 1 to itemSize,
- 2 to itemSize * 2,
- 3 to itemSize * 3,
- )
-
- rule.runOnUiThread { arrangement = Arrangement.SpaceBetween }
- rule.mainClock.advanceTimeByFrame()
-
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to itemSize - itemSize * fraction,
- 2 to itemSize * 2,
- 3 to itemSize * 3 + itemSize * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveItemToTheBottomOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- val listSize = itemSize * 3
- val listSizeDp = with(rule.density) { listSize.toDp() }
- rule.setContent {
- LazyList(maxSize = listSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(0 to 0f, 1 to itemSize, 2 to itemSize * 2)
-
- rule.runOnUiThread { list = listOf(0, 4, 2, 3, 1, 5) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to and item 4 moves from `listSize`, right after the end edge
- val item1Offset = itemSize + (listSize - itemSize) * fraction
- val item4Offset = listSize - (listSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- add(0 to 0f)
- if (item1Offset < itemSize * 3) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(2 to itemSize * 2)
- if (item4Offset < itemSize * 3) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(3 to 0f, 4 to itemSize, 5 to itemSize * 2)
-
- rule.runOnUiThread { list = listOf(2, 4, 0, 3, 1, 5) }
-
- onAnimationFrame { fraction ->
- // item 1 moves from and item 4 moves to `0 - itemSize`, right before the start edge
- val item1Offset = -itemSize + itemSize * 2 * fraction
- val item4Offset = itemSize - itemSize * 2 * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- if (item4Offset > -itemSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- add(3 to 0f)
- if (item1Offset > -itemSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(5 to itemSize * 2)
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds_withStickyHeader() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 2f, startIndex = 4) {
- // the existence of this header shouldn't affect the animation aside from
- // the fact that we need to adjust startIndex because of it`s existence.
- stickyHeader {}
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(3 to 0f, 4 to itemSize)
-
- rule.runOnUiThread { list = listOf(2, 4, 0, 3, 1) }
-
- onAnimationFrame { fraction ->
- // item 1 moves from and item 4 moves to `0 - itemSize`, right before the start edge
- val item1Offset = -itemSize + itemSize * 2 * fraction
- val item4Offset = itemSize - itemSize * 2 * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- if (item4Offset > -itemSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- add(3 to 0f)
- if (item1Offset > -itemSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveFirstItemToEndCausingAllItemsToAnimate_withSpacing() {
- var list by mutableStateOf(listOf(0, 1, 2, 3))
- rule.setContent {
- LazyList(arrangement = Arrangement.spacedBy(spacingDp)) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { list = listOf(1, 2, 3, 0) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to 0 + itemSizePlusSpacing * 3 * fraction,
- 1 to itemSizePlusSpacing - itemSizePlusSpacing * fraction,
- 2 to itemSizePlusSpacing * 2 - itemSizePlusSpacing * fraction,
- 3 to itemSizePlusSpacing * 3 - itemSizePlusSpacing * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveItemToTheBottomOutsideOfBounds_withSpacing() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- val listSize = itemSize * 3 + spacing * 2
- val listSizeDp = with(rule.density) { listSize.toDp() }
- rule.setContent {
- LazyList(maxSize = listSizeDp, arrangement = Arrangement.spacedBy(spacingDp)) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(0 to 0f, 1 to itemSizePlusSpacing, 2 to itemSizePlusSpacing * 2)
-
- rule.runOnUiThread { list = listOf(0, 4, 2, 3, 1, 5) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to and item 4 moves from `listSize`, right after the end edge
- val item1Offset = itemSizePlusSpacing + (listSize - itemSizePlusSpacing) * fraction
- val item4Offset = listSize - (listSize - itemSizePlusSpacing) * fraction
- val screenSize = itemSize * 3 + spacing * 2
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- add(0 to 0f)
- if (item1Offset < screenSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(2 to itemSizePlusSpacing * 2)
- if (item4Offset < screenSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds_withSpacing() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
- rule.setContent {
- LazyList(
- maxSize = itemSizeDp * 3 + spacingDp * 2,
- startIndex = 3,
- arrangement = Arrangement.spacedBy(spacingDp)
- ) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(3 to 0f, 4 to itemSizePlusSpacing, 5 to itemSizePlusSpacing * 2)
-
- rule.runOnUiThread { list = listOf(2, 4, 0, 3, 1, 5, 6, 7) }
-
- onAnimationFrame { fraction ->
- // item 4 moves to and item 1 moves from `-itemSize`, right before the start edge
- val item1Offset = -itemSize + (itemSize + itemSizePlusSpacing) * fraction
- val item4Offset = itemSizePlusSpacing - (itemSize + itemSizePlusSpacing) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- if (item4Offset > -itemSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- add(3 to 0f)
- if (item1Offset > -itemSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(5 to itemSizePlusSpacing * 2)
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheTopOutsideOfBounds_differentSizes() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyList(maxSize = itemSize2Dp + itemSize3Dp + itemSizeDp, startIndex = 3) {
- items(list, key = { it }) {
- val size =
- if (it == 3) itemSize2Dp else if (it == 1) itemSize3Dp else itemSizeDp
- Item(it, size = size)
- }
- }
- }
-
- val item3Size = itemSize2
- val item4Size = itemSize
- assertPositions(3 to 0f, 4 to item3Size, 5 to item3Size + item4Size)
-
- rule.runOnUiThread {
- // swap 4 and 1
- list = listOf(0, 4, 2, 3, 1, 5)
- }
-
- onAnimationFrame { fraction ->
- // item 2 was between 1 and 3 but we don't compose it
- rule.onNodeWithTag("2").assertDoesNotExist()
- val item1Size = itemSize3 /* the real size of the item 1 */
- // item 1 moves from and item 4 moves to `0 - item size`, right before the start edge
- val startItem1Offset = -item1Size
- val item1Offset = startItem1Offset + (itemSize2 - startItem1Offset) * fraction
- val endItem4Offset = -item4Size
- val item4Offset = item3Size - (item3Size - endItem4Offset) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- if (item4Offset > -item4Size) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- add(3 to 0f)
- if (item1Offset > -item1Size) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(5 to item3Size + item4Size - (item4Size - item1Size) * fraction)
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveItemToTheBottomOutsideOfBounds_differentSizes() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- val listSize = itemSize2 + itemSize3 + itemSize
- val listSizeDp = with(rule.density) { listSize.toDp() }
- rule.setContent {
- LazyList(maxSize = listSizeDp) {
- items(list, key = { it }) {
- val size =
- if (it == 0) itemSize2Dp else if (it == 4) itemSize3Dp else itemSizeDp
- Item(it, size = size)
- }
- }
- }
-
- val item0Size = itemSize2
- val item1Size = itemSize
- assertPositions(0 to 0f, 1 to item0Size, 2 to item0Size + item1Size)
-
- rule.runOnUiThread { list = listOf(0, 4, 2, 3, 1, 5) }
-
- onAnimationFrame { fraction ->
- // item 1 moves from and item 4 moves to `listSize`, right after the end edge
- val startItem4Offset = listSize
- val endItem1Offset = listSize
- val item4Size = itemSize3
- val item1Offset = item0Size + (endItem1Offset - item0Size) * fraction
- val item4Offset = startItem4Offset - (startItem4Offset - item0Size) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- add(0 to 0f)
- if (item1Offset < listSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- add(2 to item0Size + item1Size - (item1Size - item4Size) * fraction)
- if (item4Offset < listSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun animateAlignmentChange() {
- var alignment by mutableStateOf(CrossAxisAlignment.End)
- rule.setContent {
- LazyList(crossAxisAlignment = alignment, crossAxisSize = itemSizeDp) {
- items(listOf(1, 2, 3), key = { it }) {
- val crossAxisSize =
- if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- Item(it, crossAxisSize = crossAxisSize)
- }
- }
- }
-
- val item2Start = itemSize - itemSize2
- val item3Start = itemSize - itemSize3
- assertPositions(
- 1 to 0f,
- 2 to itemSize,
- 3 to itemSize * 2,
- crossAxis =
- listOf(
- 1 to 0f,
- 2 to item2Start,
- 3 to item3Start,
- )
- )
-
- rule.runOnUiThread { alignment = CrossAxisAlignment.Center }
- rule.mainClock.advanceTimeByFrame()
-
- val item2End = itemSize / 2 - itemSize2 / 2
- val item3End = itemSize / 2 - itemSize3 / 2
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to 0f,
- 2 to itemSize,
- 3 to itemSize * 2,
- crossAxis =
- listOf(
- 1 to 0f,
- 2 to item2Start + (item2End - item2Start) * fraction,
- 3 to item3Start + (item3End - item3Start) * fraction,
- ),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animateAlignmentChange_multipleChildrenPerItem() {
- var alignment by mutableStateOf(CrossAxisAlignment.Start)
- rule.setContent {
- LazyList(crossAxisAlignment = alignment, crossAxisSize = itemSizeDp * 2) {
- items(1) {
- listOf(1, 2, 3).forEach {
- val crossAxisSize =
- if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- Item(it, crossAxisSize = crossAxisSize)
- }
- }
- }
- }
-
- rule.runOnUiThread { alignment = CrossAxisAlignment.End }
- rule.mainClock.advanceTimeByFrame()
-
- val containerSize = itemSize * 2
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to 0f,
- 2 to itemSize,
- 3 to itemSize * 2,
- crossAxis =
- listOf(
- 1 to (containerSize - itemSize) * fraction,
- 2 to (containerSize - itemSize2) * fraction,
- 3 to (containerSize - itemSize3) * fraction
- ),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun animateAlignmentChange_rtl() {
- // this test is not applicable to LazyRow
- assumeTrue(isVertical)
-
- var alignment by mutableStateOf(CrossAxisAlignment.End)
- rule.setContent {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- LazyList(crossAxisAlignment = alignment, crossAxisSize = itemSizeDp) {
- items(listOf(1, 2, 3), key = { it }) {
- val crossAxisSize =
- if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
- Item(it, crossAxisSize = crossAxisSize)
- }
- }
- }
- }
-
- assertPositions(
- 1 to 0f,
- 2 to itemSize,
- 3 to itemSize * 2,
- crossAxis =
- listOf(
- 1 to 0f,
- 2 to 0f,
- 3 to 0f,
- )
- )
-
- rule.runOnUiThread { alignment = CrossAxisAlignment.Center }
- rule.mainClock.advanceTimeByFrame()
-
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to 0f,
- 2 to itemSize,
- 3 to itemSize * 2,
- crossAxis =
- listOf(
- 1 to 0f,
- 2 to (itemSize / 2 - itemSize2 / 2) * fraction,
- 3 to (itemSize / 2 - itemSize3 / 2) * fraction
- ),
- fraction = fraction
- )
- }
- }
-
- @Test
- fun moveItemToEndCausingNextItemsToAnimate_withContentPadding() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val rawStartPadding = 8f
- val rawEndPadding = 12f
- val (startPaddingDp, endPaddingDp) =
- with(rule.density) { rawStartPadding.toDp() to rawEndPadding.toDp() }
- rule.setContent {
- LazyList(startPadding = startPaddingDp, endPadding = endPaddingDp) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
- assertPositions(
- 0 to startPadding,
- 1 to startPadding + itemSize,
- 2 to startPadding + itemSize * 2,
- 3 to startPadding + itemSize * 3,
- 4 to startPadding + itemSize * 4,
- )
-
- rule.runOnUiThread { list = listOf(0, 2, 3, 4, 1) }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to startPadding,
- 1 to startPadding + itemSize + itemSize * 3 * fraction,
- 2 to startPadding + itemSize * 2 - itemSize * fraction,
- 3 to startPadding + itemSize * 3 - itemSize * fraction,
- 4 to startPadding + itemSize * 4 - itemSize * fraction,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
-
- var measurePasses = 0
- rule.setContent {
- LazyList { items(list, key = { it }) { Item(it) } }
- LaunchedEffect(Unit) { snapshotFlow { state.layoutInfo }.collect { measurePasses++ } }
- }
-
- rule.runOnUiThread { list = listOf(4, 1, 2, 3, 0) }
-
- var startMeasurePasses = Int.MIN_VALUE
- onAnimationFrame { fraction ->
- if (fraction == 0f) {
- startMeasurePasses = measurePasses
- }
- }
- rule.mainClock.advanceTimeByFrame()
- // new layoutInfo is produced on every remeasure of Lazy lists.
- // but we want to avoid remeasuring and only do relayout on each animation frame.
- // two extra measures are possible as we switch inProgress flag.
- assertThat(measurePasses).isAtMost(startMeasurePasses + 2)
- }
-
- @Test
- fun noAnimationWhenScrolledToOtherPosition() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollToItem(0, (itemSize / 2).roundToInt()) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to -itemSize / 2,
- 1 to itemSize / 2,
- 2 to itemSize * 3 / 2,
- 3 to itemSize * 5 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardBySmallOffset() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize / 2f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to -itemSize / 2,
- 1 to itemSize / 2,
- 2 to itemSize * 3 / 2,
- 3 to itemSize * 5 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardBySmallOffset() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3, startIndex = 2) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(-itemSize / 2f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 1 to -itemSize / 2,
- 2 to itemSize / 2,
- 3 to itemSize * 3 / 2,
- 4 to itemSize * 5 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 2 to -itemSize / 2,
- 3 to itemSize / 2,
- 4 to itemSize * 3 / 2,
- 5 to itemSize * 5 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3, startIndex = 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) { Item(it) }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(-itemSize * 2.5f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to -itemSize / 2,
- 1 to itemSize / 2,
- 2 to itemSize * 3 / 2,
- 3 to itemSize * 5 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollForwardByLargeOffset_differentSizes() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
- Item(it, size = if (it % 2 == 0) itemSizeDp else itemSize2Dp)
- }
- }
- }
-
- rule.runOnUiThread { runBlocking { state.scrollBy(itemSize + itemSize2 + itemSize / 2f) } }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 2 to -itemSize / 2,
- 3 to itemSize / 2,
- 4 to itemSize2 + itemSize / 2,
- 5 to itemSize2 + itemSize * 3 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun noAnimationWhenScrollBackwardByLargeOffset_differentSizes() {
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3, startIndex = 3) {
- items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
- Item(it, size = if (it % 2 == 0) itemSizeDp else itemSize2Dp)
- }
- }
- }
-
- rule.runOnUiThread {
- runBlocking { state.scrollBy(-(itemSize + itemSize2 + itemSize / 2f)) }
- }
-
- onAnimationFrame { fraction ->
- assertPositions(
- 0 to -itemSize / 2,
- 1 to itemSize / 2,
- 2 to itemSize2 + itemSize / 2,
- 3 to itemSize2 + itemSize * 3 / 2,
- fraction = fraction
- )
- }
- }
-
- @Test
- fun itemWithSpecsIsMovingOut() {
- var list by mutableStateOf(listOf(0, 1, 2, 3))
- val listSize = itemSize * 2
- val listSizeDp = with(rule.density) { listSize.toDp() }
- rule.setContent {
- LazyList(maxSize = listSizeDp) {
- items(list, key = { it }) { Item(it, animSpec = if (it == 1) AnimSpec else null) }
- }
- }
-
- rule.runOnUiThread { list = listOf(0, 2, 3, 1) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to `listSize`
- val item1Offset = itemSize + (listSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- add(0 to 0f)
- if (item1Offset < listSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheTopOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(3 to 0f, 4 to itemSize, 5 to itemSize * 2)
-
- rule.runOnUiThread { list = listOf(0, 4, 5, 3, 1, 2) }
-
- onAnimationFrame { fraction ->
- // item 2 moves from and item 5 moves to `-itemSize`, right before the start edge
- val item2Offset = -itemSize + itemSize * 3 * fraction
- val item5Offset = itemSize * 2 - itemSize * 3 * fraction
- // item 1 moves from and item 4 moves to `-itemSize * 2`, right before item 2
- val item1Offset = -itemSize * 2 + itemSize * 3 * fraction
- val item4Offset = itemSize - itemSize * 3 * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- if (item1Offset > -itemSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset > -itemSize) {
- add(2 to item2Offset)
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- add(3 to 0f)
- if (item4Offset > -itemSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- if (item5Offset > -itemSize) {
- add(5 to item5Offset)
- } else {
- rule.onNodeWithTag("5").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheTopOutsideOfBounds_withReordering() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
- rule.setContent {
- LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
- items(list, key = { it }) { Item(it) }
- }
- }
-
- assertPositions(3 to 0f, 4 to itemSize, 5 to itemSize * 2)
-
- rule.runOnUiThread { list = listOf(0, 5, 4, 3, 2, 1) }
-
- onAnimationFrame { fraction ->
- // item 2 moves from and item 4 moves to `-itemSize`, right before the start edge
- val item2Offset = -itemSize + itemSize * 2 * fraction
- val item4Offset = itemSize - itemSize * 2 * fraction
- // item 1 moves from and item 5 moves to `-itemSize * 2`, right before item 2
- val item1Offset = -itemSize * 2 + itemSize * 4 * fraction
- val item5Offset = itemSize * 2 - itemSize * 4 * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- if (item1Offset > -itemSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset > -itemSize) {
- add(2 to item2Offset)
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- add(3 to 0f)
- if (item4Offset > -itemSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- if (item5Offset > -itemSize) {
- add(5 to item5Offset)
- } else {
- rule.onNodeWithTag("5").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheBottomOutsideOfBounds() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val listSize = itemSize * 3
- val listSizeDp = with(rule.density) { listSize.toDp() }
- rule.setContent {
- LazyList(maxSize = listSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(0 to 0f, 1 to itemSize, 2 to itemSize * 2)
-
- rule.runOnUiThread { list = listOf(0, 3, 4, 1, 2) }
-
- onAnimationFrame { fraction ->
- // item 1 moves to and item 3 moves from `listSize`, right after the end edge
- val item1Offset = itemSize + (listSize - itemSize) * fraction
- val item3Offset = listSize - (listSize - itemSize) * fraction
- // item 2 moves to and item 4 moves from `listSize + itemSize`, right after item 4
- val item2Offset = itemSize * 2 + (listSize - itemSize) * fraction
- val item4Offset = listSize + itemSize - (listSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- add(0 to 0f)
- if (item1Offset < listSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset < listSize) {
- add(2 to item2Offset)
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- if (item3Offset < listSize) {
- add(3 to item3Offset)
- } else {
- rule.onNodeWithTag("3").assertIsNotDisplayed()
- }
- if (item4Offset < listSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun moveTwoItemsToTheBottomOutsideOfBounds_withReordering() {
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- val listSize = itemSize * 3
- val listSizeDp = with(rule.density) { listSize.toDp() }
- rule.setContent {
- LazyList(maxSize = listSizeDp) { items(list, key = { it }) { Item(it) } }
- }
-
- assertPositions(0 to 0f, 1 to itemSize, 2 to itemSize * 2)
-
- rule.runOnUiThread { list = listOf(0, 4, 3, 2, 1) }
-
- onAnimationFrame { fraction ->
- // item 2 moves to and item 3 moves from `listSize`, right after the end edge
- val item2Offset = itemSize * 2 + (listSize - itemSize * 2) * fraction
- val item3Offset = listSize - (listSize - itemSize * 2) * fraction
- // item 1 moves to and item 4 moves from `listSize + itemSize`, right after item 4
- val item1Offset = itemSize + (listSize + itemSize - itemSize) * fraction
- val item4Offset = listSize + itemSize - (listSize + itemSize - itemSize) * fraction
- val expected =
- mutableListOf<Pair<Any, Float>>().apply {
- add(0 to 0f)
- if (item1Offset < listSize) {
- add(1 to item1Offset)
- } else {
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- }
- if (item2Offset < listSize) {
- add(2 to item2Offset)
- } else {
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- if (item3Offset < listSize) {
- add(3 to item3Offset)
- } else {
- rule.onNodeWithTag("3").assertIsNotDisplayed()
- }
- if (item4Offset < listSize) {
- add(4 to item4Offset)
- } else {
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
- }
- assertPositions(expected = expected.toTypedArray(), fraction = fraction)
- }
- }
-
- @Test
- fun noAnimationWhenParentSizeShrinks() {
- var size by mutableStateOf(itemSizeDp * 3)
- rule.setContent {
- LazyList(maxSize = size) { items(listOf(0, 1, 2), key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { size = itemSizeDp * 2 }
-
- onAnimationFrame { fraction ->
- assertPositions(0 to 0f, 1 to itemSize, fraction = fraction)
- rule.onNodeWithTag("2").assertIsNotDisplayed()
- }
- }
-
- @Test
- fun noAnimationWhenParentSizeExpands() {
- var size by mutableStateOf(itemSizeDp * 2)
- rule.setContent {
- LazyList(maxSize = size) { items(listOf(0, 1, 2), key = { it }) { Item(it) } }
- }
-
- rule.runOnUiThread { size = itemSizeDp * 3 }
-
- onAnimationFrame { fraction ->
- assertPositions(0 to 0f, 1 to itemSize, 2 to itemSize * 2, fraction = fraction)
- }
- }
-
- private fun assertPositions(
- vararg expected: Pair<Any, Float>,
- crossAxis: List<Pair<Any, Float>>? = null,
- fraction: Float? = null,
- autoReverse: Boolean = reverseLayout
- ) {
- val roundedExpected = expected.map { it.first to it.second.roundToInt() }
- val actualBounds =
- rule
- .onAllNodes(NodesWithTagMatcher)
- .fetchSemanticsNodes()
- .associateBy(
- keySelector = { it.config.get(SemanticsProperties.TestTag) },
- valueTransform = { IntRect(it.positionInRoot.round(), it.size) }
- )
- val actualOffsets =
- expected.map {
- it.first to
- actualBounds.getValue(it.first.toString()).let { bounds ->
- if (isVertical) bounds.top else bounds.left
- }
- }
- val subject =
- if (fraction == null) {
- assertThat(actualOffsets)
- } else {
- assertWithMessage("Fraction=$fraction").that(actualOffsets)
- }
- subject.isEqualTo(
- roundedExpected.let { list ->
- if (!autoReverse) {
- list
- } else {
- val containerSize =
- actualBounds.getValue(ContainerTag).let { bounds ->
- if (isVertical) bounds.height else bounds.width
- }
- list.map {
- val itemSize =
- actualBounds.getValue(it.first.toString()).let { bounds ->
- if (isVertical) bounds.height else bounds.width
- }
- it.first to (containerSize - itemSize - it.second)
- }
- }
- }
- )
- if (crossAxis != null) {
- val actualCrossOffset =
- expected.map {
- it.first to
- actualBounds.getValue(it.first.toString()).let { bounds ->
- if (isVertical) bounds.left else bounds.top
- }
- }
- assertWithMessage("CrossAxis" + if (fraction != null) "for fraction=$fraction" else "")
- .that(actualCrossOffset)
- .isEqualTo(crossAxis.map { it.first to it.second.roundToInt() })
- }
- }
-
- private fun assertLayoutInfoPositions(vararg offsets: Pair<Any, Float>) {
- rule.runOnIdle {
- assertThat(visibleItemsOffsets)
- .isEqualTo(offsets.map { it.first to it.second.roundToInt() })
- }
- }
-
- private val visibleItemsOffsets: List<Pair<Any, Int>>
- get() = state.layoutInfo.visibleItemsInfo.map { it.key to it.offset }
-
- private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
- require(duration.mod(FrameDuration) == 0L)
- rule.waitForIdle()
- rule.mainClock.advanceTimeByFrame()
- var expectedTime = rule.mainClock.currentTime
- for (i in 0..duration step FrameDuration) {
- val fraction = i / duration.toFloat()
- onFrame(fraction)
- rule.mainClock.advanceTimeBy(FrameDuration)
- expectedTime += FrameDuration
- assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
- }
- }
-
- @Composable
- private fun LazyList(
- arrangement: Arrangement.HorizontalOrVertical? = null,
- minSize: Dp = 0.dp,
- maxSize: Dp = containerSizeDp,
- startIndex: Int = 0,
- crossAxisSize: Dp = Dp.Unspecified,
- crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Start,
- startPadding: Dp = 0.dp,
- endPadding: Dp = 0.dp,
- content: TvLazyListScope.() -> Unit
- ) {
- state = rememberTvLazyListState(startIndex)
- if (isVertical) {
- val verticalArrangement =
- arrangement ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom
- val horizontalAlignment =
- if (crossAxisAlignment == CrossAxisAlignment.Start) {
- Alignment.Start
- } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
- Alignment.CenterHorizontally
- } else {
- Alignment.End
- }
- TvLazyColumn(
- state = state,
- modifier =
- Modifier.requiredHeightIn(min = minSize, max = maxSize)
- .then(
- if (crossAxisSize != Dp.Unspecified) {
- Modifier.requiredWidth(crossAxisSize)
- } else {
- Modifier.fillMaxWidth()
- }
- )
- .testTag(ContainerTag),
- verticalArrangement = verticalArrangement,
- horizontalAlignment = horizontalAlignment,
- reverseLayout = reverseLayout,
- contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
- content = content
- )
- } else {
- val horizontalArrangement =
- arrangement ?: if (!reverseLayout) Arrangement.Start else Arrangement.End
- val verticalAlignment =
- if (crossAxisAlignment == CrossAxisAlignment.Start) {
- Alignment.Top
- } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
- Alignment.CenterVertically
- } else {
- Alignment.Bottom
- }
- TvLazyRow(
- state = state,
- modifier =
- Modifier.requiredWidthIn(min = minSize, max = maxSize)
- .then(
- if (crossAxisSize != Dp.Unspecified) {
- Modifier.requiredHeight(crossAxisSize)
- } else {
- Modifier.fillMaxHeight()
- }
- )
- .testTag(ContainerTag),
- horizontalArrangement = horizontalArrangement,
- verticalAlignment = verticalAlignment,
- reverseLayout = reverseLayout,
- contentPadding = PaddingValues(start = startPadding, end = endPadding),
- content = content
- )
- }
- }
-
- @Composable
- private fun TvLazyListItemScope.Item(
- tag: Int,
- size: Dp = itemSizeDp,
- crossAxisSize: Dp = size,
- animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
- ) {
- Box(
- Modifier.then(
- if (isVertical) {
- Modifier.requiredHeight(size).requiredWidth(crossAxisSize)
- } else {
- Modifier.requiredWidth(size).requiredHeight(crossAxisSize)
- }
- )
- .testTag(tag.toString())
- .then(
- if (animSpec != null) {
- Modifier.animateItemPlacement(animSpec)
- } else {
- Modifier
- }
- )
- )
- }
-
- private fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(
- expected: Dp
- ): SemanticsNodeInteraction {
- return if (isVertical) assertHeightIsEqualTo(expected) else assertWidthIsEqualTo(expected)
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params() =
- arrayOf(
- Config(isVertical = true, reverseLayout = false),
- Config(isVertical = false, reverseLayout = false),
- Config(isVertical = true, reverseLayout = true),
- Config(isVertical = false, reverseLayout = true),
- )
-
- class Config(val isVertical: Boolean, val reverseLayout: Boolean) {
- override fun toString() =
- (if (isVertical) "LazyColumn" else "LazyRow") +
- (if (reverseLayout) "(reverse)" else "")
- }
- }
-}
-
-private val FrameDuration = 16L
-private val Duration = 64L // 4 frames, so we get 0f, 0.25f, 0.5f, 0.75f and 1f fractions
-private val AnimSpec = tween<IntOffset>(Duration.toInt(), easing = LinearEasing)
-private val ContainerTag = "container"
-private val NodesWithTagMatcher =
- SemanticsMatcher("NodesWithTag") { it.config.contains(SemanticsProperties.TestTag) }
-
-private enum class CrossAxisAlignment {
- Start,
- End,
- Center
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
deleted file mode 100644
index cd8efc0..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ /dev/null
@@ -1,590 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.findRootCoordinates
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.node.LayoutAwareModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.lazy.grid.keyPress
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyListBeyondBoundsTest(param: Param) {
-
- @get:Rule val rule = createComposeRule()
-
- // We need to wrap the inline class parameter in another class because Java can't instantiate
- // the inline class.
- class Param(
- val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
- val reverseLayout: Boolean,
- val layoutDirection: LayoutDirection,
- ) {
- override fun toString() =
- "beyondBoundsLayoutDirection=$beyondBoundsLayoutDirection " +
- "reverseLayout=$reverseLayout " +
- "layoutDirection=$layoutDirection"
- }
-
- private val beyondBoundsLayoutDirection = param.beyondBoundsLayoutDirection
- private val reverseLayout = param.reverseLayout
- private val layoutDirection = param.layoutDirection
- private val placedItems = sortedMapOf<Int, Rect>()
- private var beyondBoundsLayout: BeyondBoundsLayout? = null
- private lateinit var lazyListState: TvLazyListState
- private val placementComparator =
- PlacementComparator(beyondBoundsLayoutDirection, layoutDirection, reverseLayout)
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters() = buildList {
- for (beyondBoundsLayoutDirection in listOf(Left, Right, Above, Below, Before, After)) {
- for (reverseLayout in listOf(false, true)) {
- for (layoutDirection in listOf(Ltr, Rtl)) {
- add(Param(beyondBoundsLayoutDirection, reverseLayout, layoutDirection))
- }
- }
- }
- }
- }
-
- @Test
- fun onlyOneVisibleItemIsPlaced() {
- // Arrange.
- rule.setLazyContent(size = 10.toDp(), firstVisibleItem = 0) {
- items(100) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(0)
- assertThat(visibleItems).containsExactly(0)
- }
- }
-
- @Test
- fun onlyTwoVisibleItemsArePlaced() {
- // Arrange.
- rule.setLazyContent(size = 20.toDp(), firstVisibleItem = 0) {
- items(100) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(0, 1)
- assertThat(visibleItems).containsExactly(0, 1)
- }
- }
-
- @Test
- fun onlyThreeVisibleItemsArePlaced() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
- items(100) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(0, 1, 2)
- assertThat(visibleItems).containsExactly(0, 1, 2)
- }
- }
-
- @Test
- fun emptyLazyList_doesNotCrash() {
- // Arrange.
- var addItems by mutableStateOf(true)
- lateinit var beyondBoundsLayoutRef: BeyondBoundsLayout
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 0) {
- if (addItems) {
- item {
- Box(
- Modifier.modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- }
- }
- rule.runOnIdle {
- beyondBoundsLayoutRef = beyondBoundsLayout!!
- addItems = false
- }
-
- // Act.
- val hasMoreContent =
- rule.runOnIdle {
- beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) { hasMoreContent }
- }
-
- // Assert.
- rule.runOnIdle { assertThat(hasMoreContent).isFalse() }
- }
-
- @Test
- fun oneExtraItemBeyondVisibleBounds() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp()).trackPlaced(5).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
- }
- assertThat(visibleItems).containsExactly(5, 6, 7)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Just return true so that we stop as soon as we run this once.
- // This should result in one extra item being added.
- true
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
-
- @Test
- fun twoExtraItemsBeyondVisibleBounds() {
- // Arrange.
- var extraItemCount = 2
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp()).trackPlaced(5).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- if (--extraItemCount > 0) {
- // Return null to continue the search.
- null
- } else {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(3, 4, 5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9)
- }
- assertThat(visibleItems).containsExactly(5, 6, 7)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Return true to stop the search.
- true
- }
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
-
- @Test
- fun allBeyondBoundsItemsInSpecifiedDirection() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp())
- .modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- .trackPlaced(5)
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- if (hasMoreContent) {
- // Just return null so that we keep adding more items till we reach the end.
- null
- } else {
- // Assert that the beyond bounds items are present.
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8, 9, 10)
- }
- assertThat(visibleItems).containsExactly(5, 6, 7)
-
- assertThat(placedItems.values).isInOrder(placementComparator)
-
- // Return true to end the search.
- true
- }
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle { assertThat(placedItems.keys).containsExactly(5, 6, 7) }
- }
-
- @Test
- fun beyondBoundsLayoutRequest_inDirectionPerpendicularToLazyListOrientation() {
- // Arrange.
- var beyondBoundsLayoutCount = 0
- rule.setLazyContentInPerpendicularDirection(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp()).trackPlaced(5).modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
-
- // Act.
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- beyondBoundsLayoutCount++
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Above,
- Below -> {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- Before,
- After -> {
- if (expectedExtraItemsBeforeVisibleBounds()) {
- assertThat(placedItems.keys).containsExactly(4, 5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- } else {
- assertThat(placedItems.keys).containsExactly(5, 6, 7, 8)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
- }
- // Just return true so that we stop as soon as we run this once.
- // This should result in one extra item being added.
- true
- }
- }
-
- rule.runOnIdle {
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Above,
- Below -> {
- assertThat(beyondBoundsLayoutCount).isEqualTo(0)
- }
- Before,
- After -> {
- assertThat(beyondBoundsLayoutCount).isEqualTo(1)
-
- // Assert that the beyond bounds items are removed.
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- else -> error("Unsupported BeyondBoundsLayoutDirection")
- }
- }
- }
-
- @Test
- fun returningNullDoesNotCauseInfiniteLoop() {
- // Arrange.
- rule.setLazyContent(size = 30.toDp(), firstVisibleItem = 5) {
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index)) }
- item {
- Box(
- Modifier.size(10.toDp())
- .modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
- .trackPlaced(5)
- )
- }
- items(5) { index -> Box(Modifier.size(10.toDp()).trackPlaced(index + 6)) }
- }
-
- // Act.
- var count = 0
- rule.runOnUiThread {
- beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
- // Assert that we don't keep iterating when there is no ending condition.
- assertThat(count++).isLessThan(lazyListState.layoutInfo.totalItemsCount)
- // Always return null to continue the search.
- null
- }
- }
-
- // Assert that the beyond bounds items are removed.
- rule.runOnIdle {
- assertThat(placedItems.keys).containsExactly(5, 6, 7)
- assertThat(visibleItems).containsExactly(5, 6, 7)
- }
- }
-
- @Test
- fun emptyRowInColumn_focusSearchDoesNotCrash() {
- val buttonFocusRequester = FocusRequester()
- rule.setContent {
- Column {
- BasicText(
- text = "Outer button",
- Modifier.focusRequester(buttonFocusRequester).focusable()
- )
-
- TvLazyColumn {
- items(3) {
- if (it == 2) {
- // intentional empty row
- TvLazyRow(Modifier.height(200.dp).width(2000.dp)) {}
- } else {
- TvLazyRow {
- items(30) {
- Box(Modifier.size(200.dp)) { BasicText(text = it.toString()) }
- }
- }
- }
- }
- }
- }
- }
-
- rule.runOnIdle { buttonFocusRequester.requestFocus() }
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN)
- }
-
- private fun ComposeContentTestRule.setLazyContent(
- size: Dp,
- firstVisibleItem: Int,
- content: TvLazyListScope.() -> Unit
- ) {
- setContent {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- lazyListState = rememberTvLazyListState(firstVisibleItem)
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Before,
- After ->
- TvLazyRow(
- modifier = Modifier.size(size),
- state = lazyListState,
- reverseLayout = reverseLayout,
- content = content
- )
- Above,
- Below ->
- TvLazyColumn(
- modifier = Modifier.size(size),
- state = lazyListState,
- reverseLayout = reverseLayout,
- content = content
- )
- else -> unsupportedDirection()
- }
- }
- }
- }
-
- private fun ComposeContentTestRule.setLazyContentInPerpendicularDirection(
- size: Dp,
- firstVisibleItem: Int,
- content: TvLazyListScope.() -> Unit
- ) {
- setContent {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- lazyListState = rememberTvLazyListState(firstVisibleItem)
- when (beyondBoundsLayoutDirection) {
- Left,
- Right,
- Before,
- After ->
- TvLazyColumn(
- modifier = Modifier.size(size),
- state = lazyListState,
- reverseLayout = reverseLayout,
- content = content
- )
- Above,
- Below ->
- TvLazyRow(
- modifier = Modifier.size(size),
- state = lazyListState,
- reverseLayout = reverseLayout,
- content = content
- )
- else -> unsupportedDirection()
- }
- }
- }
- }
-
- private fun Int.toDp(): Dp = with(rule.density) { toDp() }
-
- private val visibleItems: List<Int>
- get() = lazyListState.layoutInfo.visibleItemsInfo.map { it.index }
-
- private fun expectedExtraItemsBeforeVisibleBounds() =
- when (beyondBoundsLayoutDirection) {
- Right -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
- Left -> if (layoutDirection == Ltr) !reverseLayout else reverseLayout
- Above -> !reverseLayout
- Below -> reverseLayout
- After -> false
- Before -> true
- else -> error("Unsupported BeyondBoundsDirection")
- }
-
- private fun unsupportedDirection(): Nothing =
- error("Lazy list does not support beyond bounds layout for the specified direction")
-
- private fun Modifier.trackPlaced(index: Int): Modifier =
- this then TrackPlacedElement(index, placedItems)
-}
-
-internal data class TrackPlacedElement(var index: Int, var placedItems: MutableMap<Int, Rect>) :
- ModifierNodeElement<TrackPlacedNode>() {
- override fun create() = TrackPlacedNode(index, placedItems)
-
- override fun update(node: TrackPlacedNode) {
- node.index = index
- node.placedItems = placedItems
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "trackPlaced"
- properties["index"] = index
- properties["placedItems"] = placedItems
- }
-}
-
-internal class TrackPlacedNode(var index: Int, var placedItems: MutableMap<Int, Rect>) :
- LayoutAwareModifierNode, Modifier.Node() {
- override fun onPlaced(coordinates: LayoutCoordinates) {
- placedItems[index] =
- coordinates.findRootCoordinates().localBoundingBoxOf(coordinates, false)
- }
-
- override fun onDetach() {
- placedItems.remove(index)
- }
-}
-
-internal class PlacementComparator(
- val beyondBoundsLayoutDirection: BeyondBoundsLayout.LayoutDirection,
- val layoutDirection: LayoutDirection,
- val reverseLayout: Boolean
-) : Comparator<Rect> {
- private fun itemsInReverseOrder() =
- when (beyondBoundsLayoutDirection) {
- Above,
- Below -> reverseLayout
- else -> if (layoutDirection == Ltr) reverseLayout else !reverseLayout
- }
-
- private fun compareOffset(o1: Float, o2: Float): Int {
- return if (itemsInReverseOrder()) o2.compareTo(o1) else o1.compareTo(o2)
- }
-
- override fun compare(o1: Rect?, o2: Rect?): Int {
- if (o1 == null || o2 == null) return 0
- return when (beyondBoundsLayoutDirection) {
- Above,
- Below -> compareOffset(o1.top, o2.top)
- else -> compareOffset(o1.left, o2.left)
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt
deleted file mode 100644
index cd2408c..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListHeadersTest.kt
+++ /dev/null
@@ -1,453 +0,0 @@
-/*
- * Copyright 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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.PivotOffsets
-import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalTvFoundationApi::class)
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyListHeadersTest {
-
- private val TvLazyListTag = "TvLazyList"
-
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun tvLazyColumnShowsHeader_withoutBeyondBoundsItemCount() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
-
- rule.setContent {
- TvLazyColumn(Modifier.height(300.dp), beyondBoundsItemCount = 0) {
- stickyHeader {
- Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(firstHeaderTag))
- }
-
- items(items) { Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it)) }
-
- stickyHeader {
- Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(secondHeaderTag))
- }
- }
- }
-
- rule.onNodeWithTag(firstHeaderTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertDoesNotExist()
- }
-
- @Test
- fun tvLazyColumnPlaceSecondHeader_ifBeyondBoundsItemCountIsUsed() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
-
- rule.setContent {
- TvLazyColumn(Modifier.height(300.dp), beyondBoundsItemCount = 1) {
- stickyHeader {
- Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(firstHeaderTag))
- }
-
- items(items) { Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it)) }
-
- stickyHeader {
- Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(secondHeaderTag))
- }
- }
- }
-
- rule.onNodeWithTag(firstHeaderTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertExists()
- }
-
- @Test
- fun tvLazyColumnShowsHeadersOnScroll() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
- lateinit var state: TvLazyListState
-
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- Modifier.height(300.dp).testTag(TvLazyListTag).border(2.dp, Color.Black),
- rememberTvLazyListState().also { state = it }
- ) {
- stickyHeader {
- Spacer(
- Modifier.height(101.dp)
- .fillParentMaxWidth()
- .border(2.dp, Color.Green)
- .testTag(firstHeaderTag)
- )
- }
-
- items(items) {
- Spacer(
- Modifier.height(101.dp)
- .fillParentMaxWidth()
- .border(2.dp, Color.Blue)
- .testTag(it)
- )
- }
-
- stickyHeader {
- Spacer(
- Modifier.height(101.dp)
- .fillParentMaxWidth()
- .border(2.dp, Color.Yellow)
- .testTag(secondHeaderTag)
- )
- }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule
- .onNodeWithTag(firstHeaderTag)
- .assertIsDisplayed()
- .assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.runOnIdle {
- assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
- assertEquals(0, state.layoutInfo.visibleItemsInfo.first().offset)
- }
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
- }
-
- @Test
- fun tvLazyColumnHeaderIsReplaced() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
- lateinit var state: TvLazyListState
-
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- TvLazyColumn(modifier = Modifier.height(300.dp).testTag(TvLazyListTag), state = state) {
- stickyHeader {
- Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(firstHeaderTag))
- }
-
- stickyHeader {
- Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(secondHeaderTag))
- }
-
- items(items) { Spacer(Modifier.height(101.dp).fillParentMaxWidth().testTag(it)) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule.onNodeWithTag(firstHeaderTag).assertIsNotDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
- }
-
- @Test
- fun tvLazyRowShowsHeader_withoutOffscreenItens() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
-
- rule.setContent {
- TvLazyRow(Modifier.width(300.dp), beyondBoundsItemCount = 0) {
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(firstHeaderTag))
- }
-
- items(items) { Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it)) }
-
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(secondHeaderTag))
- }
- }
- }
-
- rule.onNodeWithTag(firstHeaderTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertDoesNotExist()
- }
-
- @Test
- fun tvLazyRowPlaceSecondHeader_ifBeyondBoundsItemCountIsUsed() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
-
- rule.setContent {
- TvLazyRow(Modifier.width(300.dp), beyondBoundsItemCount = 1) {
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(firstHeaderTag))
- }
-
- items(items) { Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it)) }
-
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(secondHeaderTag))
- }
- }
- }
-
- rule.onNodeWithTag(firstHeaderTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertExists()
- }
-
- @Test
- fun tvLazyRowShowsHeadersOnScroll() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
- lateinit var state: TvLazyListState
-
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- Modifier.width(300.dp).testTag(TvLazyListTag),
- rememberTvLazyListState().also { state = it }
- ) {
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(firstHeaderTag))
- }
-
- items(items) { Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it)) }
-
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(secondHeaderTag))
- }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule.onNodeWithTag(TvLazyListTag).scrollBy(x = 102.dp, density = rule.density)
-
- rule
- .onNodeWithTag(firstHeaderTag)
- .assertIsDisplayed()
- .assertLeftPositionInRootIsEqualTo(0.dp)
-
- rule.runOnIdle {
- assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
- assertEquals(0, state.layoutInfo.visibleItemsInfo.first().offset)
- }
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
- }
-
- @Test
- fun tvLazyRowHeaderIsReplaced() {
- val items = (1..2).map { it.toString() }
- val firstHeaderTag = "firstHeaderTag"
- val secondHeaderTag = "secondHeaderTag"
- lateinit var state: TvLazyListState
-
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- TvLazyRow(modifier = Modifier.width(300.dp).testTag(TvLazyListTag), state = state) {
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(firstHeaderTag))
- }
-
- stickyHeader {
- Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(secondHeaderTag))
- }
-
- items(items) { Spacer(Modifier.width(101.dp).fillParentMaxHeight().testTag(it)) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule.onNodeWithTag(firstHeaderTag).assertIsNotDisplayed()
-
- rule.onNodeWithTag(secondHeaderTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
- }
-
- @Test
- fun headerIsDisplayedWhenItIsFullyInContentPadding() {
- val headerTag = "header"
- val itemIndexPx = 100
- val itemIndexDp = with(rule.density) { itemIndexPx.toDp() }
- lateinit var state: TvLazyListState
-
- rule.setContent {
- TvLazyColumn(
- Modifier.requiredSize(itemIndexDp * 4),
- state = rememberTvLazyListState().also { state = it },
- contentPadding = PaddingValues(top = itemIndexDp * 2)
- ) {
- stickyHeader { Spacer(Modifier.requiredSize(itemIndexDp).testTag(headerTag)) }
-
- items((0..4).toList()) { Spacer(Modifier.requiredSize(itemIndexDp).testTag("$it")) }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1, itemIndexPx / 2) } }
-
- rule.onNodeWithTag(headerTag).assertTopPositionInRootIsEqualTo(itemIndexDp / 2)
-
- rule.runOnIdle {
- assertEquals(0, state.layoutInfo.visibleItemsInfo.first().index)
- assertEquals(
- itemIndexPx / 2 - /* content padding size */ itemIndexPx * 2,
- state.layoutInfo.visibleItemsInfo.first().offset
- )
- }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemIndexDp * 3 / 2)
- }
-}
-
-@Composable
-private fun TvLazyColumn(
- modifier: Modifier = Modifier,
- state: TvLazyListState = rememberTvLazyListState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- verticalArrangement: Arrangement.Vertical =
- if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
- horizontalAlignment: Alignment.Horizontal = Alignment.Start,
- userScrollEnabled: Boolean = true,
- beyondBoundsItemCount: Int,
- content: TvLazyListScope.() -> Unit
-) {
- LazyList(
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- horizontalAlignment = horizontalAlignment,
- verticalArrangement = verticalArrangement,
- isVertical = true,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- beyondBoundsItemCount = beyondBoundsItemCount,
- content = content,
- pivotOffsets = PivotOffsets()
- )
-}
-
-@Composable
-private fun TvLazyRow(
- modifier: Modifier = Modifier,
- state: TvLazyListState = rememberTvLazyListState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- horizontalArrangement: Arrangement.Horizontal =
- if (!reverseLayout) Arrangement.Start else Arrangement.End,
- verticalAlignment: Alignment.Vertical = Alignment.Top,
- userScrollEnabled: Boolean = true,
- beyondBoundsItemCount: Int,
- content: TvLazyListScope.() -> Unit
-) {
- LazyList(
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- verticalAlignment = verticalAlignment,
- horizontalArrangement = horizontalArrangement,
- isVertical = false,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- beyondBoundsItemCount = beyondBoundsItemCount,
- content = content,
- pivotOffsets = PivotOffsets()
- )
-}
-
-internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
- performTouchInput {
- with(density) {
- val touchSlop = TestTouchSlop.toInt()
- val xPx = x.roundToPx()
- val yPx = y.roundToPx()
- val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
- val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
- swipeWithVelocity(
- start = center,
- end = Offset(center.x - offsetX, center.y - offsetY),
- endVelocity = 0f
- )
- }
- }
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListLayoutInfoTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListLayoutInfoTest.kt
deleted file mode 100644
index a25291c..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListLayoutInfoTest.kt
+++ /dev/null
@@ -1,435 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyListLayoutInfoTest(param: LayoutInfoTestParam) :
- BaseLazyListTestWithOrientation(param.orientation) {
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters(): Array<Any> =
- arrayOf(
- LayoutInfoTestParam(Orientation.Vertical, false),
- LayoutInfoTestParam(Orientation.Vertical, true),
- LayoutInfoTestParam(Orientation.Horizontal, false),
- LayoutInfoTestParam(Orientation.Horizontal, true),
- )
- }
-
- private val reverseLayout = param.reverseLayout
-
- private var itemSizePx: Int = 50
- private var itemSizeDp: Dp = Dp.Infinity
-
- @Before
- fun before() {
- with(rule.density) { itemSizeDp = itemSizePx.toDp() }
- }
-
- @Test
- fun visibleItemsAreCorrect() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
- ) {
- items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle { state.layoutInfo.assertVisibleItems(count = 4) }
- }
-
- @Test
- fun visibleItemsAreCorrectAfterScroll() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
- ) {
- items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollToItem(1, 10) }
- state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1, startOffset = -10)
- }
- }
-
- @Test
- fun visibleItemsAreCorrectWithSpacing() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- spacedBy = itemSizeDp,
- modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
- ) {
- items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle { state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx) }
- }
-
- @Composable
- fun ObservingFun(state: TvLazyListState, currentInfo: StableRef<TvLazyListLayoutInfo?>) {
- currentInfo.value = state.layoutInfo
- }
-
- @Test
- fun visibleItemsAreObservableWhenWeScroll() {
- lateinit var state: TvLazyListState
- val currentInfo = StableRef<TvLazyListLayoutInfo?>(null)
- rule.setContent {
- LazyColumnOrRow(
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
- ) {
- items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
- }
- ObservingFun(state, currentInfo)
- }
-
- rule.runOnIdle {
- // empty it here and scrolling should invoke observingFun again
- currentInfo.value = null
- runBlocking { state.scrollToItem(1, 0) }
- }
-
- rule.runOnIdle {
- assertThat(currentInfo.value).isNotNull()
- currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
- }
- }
-
- @Test
- fun visibleItemsAreObservableWhenResize() {
- lateinit var state: TvLazyListState
- var size by mutableStateOf(itemSizeDp * 2)
- var currentInfo: TvLazyListLayoutInfo? = null
- @Composable
- fun observingFun() {
- currentInfo = state.layoutInfo
- }
- rule.setContent {
- LazyColumnOrRow(
- reverseLayout = reverseLayout,
- state = rememberTvLazyListState().also { state = it }
- ) {
- item { Box(Modifier.requiredSize(size)) }
- }
- observingFun()
- }
-
- rule.runOnIdle {
- assertThat(currentInfo).isNotNull()
- currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
- currentInfo = null
- size = itemSizeDp
- }
-
- rule.runOnIdle {
- assertThat(currentInfo).isNotNull()
- currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
- }
- }
-
- @Test
- fun totalCountIsCorrect() {
- var count by mutableStateOf(10)
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- reverseLayout = reverseLayout,
- state = rememberTvLazyListState().also { state = it }
- ) {
- items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
- count = 20
- }
-
- rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
- }
-
- @Test
- fun viewportOffsetsAndSizeAreCorrect() {
- val sizePx = 45
- val sizeDp = with(rule.density) { sizePx.toDp() }
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
- reverseLayout = reverseLayout,
- state = rememberTvLazyListState().also { state = it }
- ) {
- items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
- )
- }
- }
-
- @Test
- fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
- val sizePx = 45
- val startPaddingPx = 10
- val endPaddingPx = 15
- val sizeDp = with(rule.density) { sizePx.toDp() }
- val beforeContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
- }
- val afterContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
- }
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
- contentPadding =
- PaddingValues(
- beforeContent = beforeContentPaddingDp,
- afterContent = afterContentPaddingDp,
- beforeContentCrossAxis = 2.dp,
- afterContentCrossAxis = 2.dp
- ),
- reverseLayout = reverseLayout,
- state = rememberTvLazyListState().also { state = it }
- ) {
- items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
- )
- }
- }
-
- @Test
- fun emptyItemsInVisibleItemsInfo() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(state = rememberTvLazyListState().also { state = it }) {
- item { Box(Modifier) }
- item {}
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
- assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
- assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
- }
- }
-
- @Test
- fun emptyContent() {
- lateinit var state: TvLazyListState
- val sizePx = 45
- val startPaddingPx = 10
- val endPaddingPx = 15
- val sizeDp = with(rule.density) { sizePx.toDp() }
- val beforeContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
- }
- val afterContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
- }
- rule.setContent {
- LazyColumnOrRow(
- Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- contentPadding =
- PaddingValues(
- beforeContent = beforeContentPaddingDp,
- afterContent = afterContentPaddingDp
- )
- ) {}
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
- assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
- )
- }
- }
-
- @Test
- fun viewportIsLargerThenTheContent() {
- lateinit var state: TvLazyListState
- val sizePx = 45
- val startPaddingPx = 10
- val endPaddingPx = 15
- val sizeDp = with(rule.density) { sizePx.toDp() }
- val beforeContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
- }
- val afterContentPaddingDp =
- with(rule.density) {
- if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
- }
- rule.setContent {
- LazyColumnOrRow(
- Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- contentPadding =
- PaddingValues(
- beforeContent = beforeContentPaddingDp,
- afterContent = afterContentPaddingDp
- )
- ) {
- item { Box(Modifier.size(sizeDp / 2)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
- assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
- assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
- assertThat(state.layoutInfo.viewportSize)
- .isEqualTo(
- if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
- )
- }
- }
-
- @Test
- fun reverseLayoutIsCorrect() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- state = rememberTvLazyListState().also { state = it },
- reverseLayout = reverseLayout,
- modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
- ) {
- items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle { assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout) }
- }
-
- @Test
- fun orientationIsCorrect() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
- ) {
- items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.layoutInfo.orientation)
- .isEqualTo(if (vertical) Orientation.Vertical else Orientation.Horizontal)
- }
- }
-
- private fun TvLazyListLayoutInfo.assertVisibleItems(
- count: Int,
- startIndex: Int = 0,
- startOffset: Int = 0,
- expectedSize: Int = itemSizePx,
- spacing: Int = 0
- ) {
- assertThat(visibleItemsInfo.size).isEqualTo(count)
- var currentIndex = startIndex
- var currentOffset = startOffset
- visibleItemsInfo.forEach {
- assertThat(it.index).isEqualTo(currentIndex)
- assertWithMessage("Offset of item $currentIndex")
- .that(it.offset)
- .isEqualTo(currentOffset)
- assertThat(it.size).isEqualTo(expectedSize)
- currentIndex++
- currentOffset += it.size + spacing
- }
- }
-}
-
-class LayoutInfoTestParam(val orientation: Orientation, val reverseLayout: Boolean) {
- override fun toString(): String {
- return "orientation=$orientation;reverseLayout=$reverseLayout"
- }
-}
-
-@Stable class StableRef<T>(var value: T)
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt
deleted file mode 100644
index 274ab81..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt
+++ /dev/null
@@ -1,583 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.LocalPinnableContainer
-import androidx.compose.ui.layout.PinnableContainer
-import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.collections.removeFirst as removeFirstKt
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-
-@MediumTest
-class LazyListPinnableContainerTest {
-
- @get:Rule val rule = createComposeRule()
-
- private var pinnableContainer: PinnableContainer? = null
-
- private val itemSizePx = 10
- private var itemSize = Dp.Unspecified
-
- private val composed = mutableSetOf<Int>()
-
- @Before
- fun setup() {
- itemSize = with(rule.density) { itemSizePx.toDp() }
- }
-
- @Composable
- fun Item(index: Int) {
- Box(Modifier.size(itemSize).testTag("$index"))
- DisposableEffect(index) {
- composed.add(index)
- onDispose { composed.remove(index) }
- }
- }
-
- @Test
- fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
- val state = TvLazyListState()
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(1)
- runBlocking { state.scrollToItem(3) }
- }
-
- rule.waitUntil {
- // not visible items were disposed
- !composed.contains(0)
- }
-
- rule.runOnIdle {
- // item 1 is still pinned
- assertThat(composed).contains(1)
- }
-
- rule.onNodeWithTag("1").assertExists().assertIsNotDisplayed().assertIsPlaced()
- }
-
- @Test
- fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
- val state = TvLazyListState()
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(4) } }
-
- rule.waitUntil {
- // not visible items were disposed
- !composed.contains(0)
- }
-
- rule.runOnIdle {
- assertThat(composed).doesNotContain(0)
- assertThat(composed).contains(1)
- assertThat(composed).doesNotContain(2)
- assertThat(composed).doesNotContain(3)
- assertThat(composed).contains(4)
- }
- }
-
- @Test
- fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
- val state = TvLazyListState()
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 4) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(4) } }
-
- rule.waitUntil {
- // wait for not visible items to be disposed
- !composed.contains(1)
- }
-
- rule.runOnIdle {
- requireNotNull(pinnableContainer).pin()
- assertThat(composed).contains(5)
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(0) } }
-
- rule.waitUntil {
- // wait for not visible items to be disposed
- !composed.contains(5)
- }
-
- rule.runOnIdle {
- assertThat(composed).contains(0)
- assertThat(composed).contains(1)
- assertThat(composed).doesNotContain(2)
- assertThat(composed).doesNotContain(3)
- assertThat(composed).contains(4)
- assertThat(composed).doesNotContain(5)
- }
- }
-
- @Test
- fun pinnedItemCanBeUnpinned() {
- val state = TvLazyListState()
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- val handle = rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(3) } }
-
- rule.waitUntil {
- // wait for not visible items to be disposed
- !composed.contains(0)
- }
-
- rule.runOnIdle { handle.release() }
-
- rule.waitUntil {
- // wait for unpinned item to be disposed
- !composed.contains(1)
- }
-
- rule.onNodeWithTag("1").assertIsNotPlaced()
- }
-
- @Ignore // b/268720713
- @Test
- fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
- val state = TvLazyListState()
- var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 3), state = state) {
- items(list, key = { it }) { index ->
- if (index == 2) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle {
- assertThat(composed).containsExactly(0, 1, 2)
- requireNotNull(pinnableContainer).pin()
- }
-
- rule.runOnIdle { list = listOf(0, 3, 4, 1, 2) }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(1)
- }
-
- rule.runOnIdle {
- assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
- }
-
- rule.onNodeWithTag("2").assertIsPlaced()
- }
-
- @Test
- fun unpinnedWhenTvLazyListStateChanges() {
- var state by mutableStateOf(TvLazyListState(firstVisibleItemIndex = 2))
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 2) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(3)
- runBlocking { state.scrollToItem(0) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(3)
- }
-
- rule.runOnIdle {
- assertThat(composed).contains(2)
- state = TvLazyListState()
- }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(2)
- }
-
- rule.onNodeWithTag("2").assertIsNotPlaced()
- }
-
- @Test
- fun pinAfterTvLazyListStateChange() {
- var state by mutableStateOf(TvLazyListState())
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 0) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { state = TvLazyListState() }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(1)
- runBlocking { state.scrollToItem(2) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(1)
- }
-
- rule.runOnIdle { assertThat(composed).contains(0) }
- }
-
- @Test
- fun itemsArePinnedBasedOnGlobalIndexes() {
- val state = TvLazyListState(firstVisibleItemIndex = 3)
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- repeat(100) { index ->
- item {
- if (index == 3) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(composed).contains(4)
- runBlocking { state.scrollToItem(6) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(4)
- }
-
- rule.runOnIdle { assertThat(composed).contains(3) }
-
- rule.onNodeWithTag("3").assertExists().assertIsNotDisplayed().assertIsPlaced()
- }
-
- @Test
- fun pinnedItemIsRemovedWhenNotVisible() {
- val state = TvLazyListState(3)
- var itemCount by mutableStateOf(10)
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(itemCount) { index ->
- if (index == 3) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle {
- requireNotNull(pinnableContainer).pin()
- assertThat(composed).contains(4)
- runBlocking { state.scrollToItem(0) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(4)
- }
-
- rule.runOnIdle { itemCount = 3 }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(3)
- }
-
- rule.onNodeWithTag("3").assertIsNotPlaced()
- }
-
- @Test
- fun pinnedItemIsRemovedWhenVisible() {
- val state = TvLazyListState(0)
- var items by mutableStateOf(listOf(0, 1, 2))
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(items) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle { items = listOf(0, 2) }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(1)
- }
-
- rule.onNodeWithTag("1").assertIsNotPlaced()
- }
-
- @Test
- fun pinnedMultipleTimes() {
- val state = TvLazyListState(0)
- // Arrange.
- rule.setContent {
- TvLazyColumn(Modifier.size(itemSize * 2), state = state) {
- items(100) { index ->
- if (index == 1) {
- pinnableContainer = LocalPinnableContainer.current
- }
- Item(index)
- }
- }
- }
-
- val handles = mutableListOf<PinnedHandle>()
- rule.runOnIdle {
- handles.add(requireNotNull(pinnableContainer).pin())
- handles.add(requireNotNull(pinnableContainer).pin())
- }
-
- rule.runOnIdle {
- // pinned 3 times in total
- handles.add(requireNotNull(pinnableContainer).pin())
- assertThat(composed).contains(0)
- runBlocking { state.scrollToItem(3) }
- }
-
- rule.waitUntil {
- // wait for not visible item to be disposed
- !composed.contains(0)
- }
-
- while (handles.isNotEmpty()) {
- rule.runOnIdle {
- assertThat(composed).contains(1)
- handles.removeFirstKt().release()
- }
- }
-
- rule.waitUntil {
- // wait for pinned item to be disposed
- !composed.contains(1)
- }
- }
-
- @Test
- fun pinningIsPropagatedToParentContainer() {
- var parentPinned = false
- val parentContainer =
- object : PinnableContainer {
- override fun pin(): PinnedHandle {
- parentPinned = true
- return PinnedHandle { parentPinned = false }
- }
- }
- // Arrange.
- rule.setContent {
- CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
- TvLazyColumn {
- item {
- pinnableContainer = LocalPinnableContainer.current
- Box(Modifier.size(itemSize))
- }
- }
- }
- }
-
- val handle = rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(parentPinned).isTrue()
- handle.release()
- }
-
- rule.runOnIdle { assertThat(parentPinned).isFalse() }
- }
-
- @Test
- fun parentContainerChange_pinningIsMaintained() {
- var parent1Pinned = false
- val parent1Container =
- object : PinnableContainer {
- override fun pin(): PinnedHandle {
- parent1Pinned = true
- return PinnedHandle { parent1Pinned = false }
- }
- }
- var parent2Pinned = false
- val parent2Container =
- object : PinnableContainer {
- override fun pin(): PinnedHandle {
- parent2Pinned = true
- return PinnedHandle { parent2Pinned = false }
- }
- }
- var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
- // Arrange.
- rule.setContent {
- CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
- TvLazyColumn {
- item {
- pinnableContainer = LocalPinnableContainer.current
- Box(Modifier.size(itemSize))
- }
- }
- }
- }
-
- rule.runOnIdle { requireNotNull(pinnableContainer).pin() }
-
- rule.runOnIdle {
- assertThat(parent1Pinned).isTrue()
- assertThat(parent2Pinned).isFalse()
- parentContainer = parent2Container
- }
-
- rule.runOnIdle {
- assertThat(parent1Pinned).isFalse()
- assertThat(parent2Pinned).isTrue()
- }
- }
-}
-
-/**
- * Asserts that the current semantics node is not placed.
- *
- * Throws [AssertionError] if the node is placed.
- */
-internal fun SemanticsNodeInteraction.assertIsNotPlaced() {
- // TODO(b/187188981): We don't have a non-throwing API to check whether an item exists.
- // So until this bug is fixed, we are going to catch the assertion error and then check
- // whether the node is placed or not.
- try {
- // If the node does not exist, it implies that it is also not placed.
- assertDoesNotExist()
- } catch (e: AssertionError) {
- // If the node exists, we need to assert that it is not placed.
- val errorMessageOnFail = "Assert failed: The component is placed!"
- if (fetchSemanticsNode().layoutInfo.isPlaced) {
- throw AssertionError(errorMessageOnFail)
- }
- }
-}
-
-/**
- * Asserts that the current semantics node is placed.
- *
- * Throws [AssertionError] if the node is not placed.
- */
-internal fun SemanticsNodeInteraction.assertIsPlaced(): SemanticsNodeInteraction {
- val errorMessageOnFail = "Assert failed: The component is not placed!"
- if (!fetchSemanticsNode(errorMessageOnFail).layoutInfo.isPlaced) {
- throw AssertionError(errorMessageOnFail)
- }
- return this
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPrefetcherTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPrefetcherTest.kt
deleted file mode 100644
index 4ae6239..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPrefetcherTest.kt
+++ /dev/null
@@ -1,350 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.Remeasurement
-import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyListPrefetcherTest(orientation: Orientation) :
- BaseLazyListTestWithOrientation(orientation) {
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun initParameters(): Array<Any> =
- arrayOf(
- Orientation.Vertical,
- Orientation.Horizontal,
- )
- }
-
- val itemsSizePx = 30
- val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
-
- lateinit var state: TvLazyListState
-
- @Test
- fun notPrefetchingForwardInitially() {
- composeList()
-
- rule.onNodeWithTag("2").assertDoesNotExist()
- }
-
- @Test
- fun notPrefetchingBackwardInitially() {
- composeList(firstItem = 2)
-
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAfterSmallScroll() {
- composeList()
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(2)
-
- rule.onNodeWithTag("2").assertExists()
- rule.onNodeWithTag("3").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingBackwardAfterSmallScroll() {
- composeList(firstItem = 2, itemOffset = 10)
-
- rule.runOnIdle { runBlocking { state.scrollBy(-5f) } }
-
- waitForPrefetch(1)
-
- rule.onNodeWithTag("1").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAndBackward() {
- composeList(firstItem = 1)
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(3)
-
- rule.onNodeWithTag("3").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(-2f)
- state.scrollBy(-1f)
- }
- }
-
- waitForPrefetch(0)
-
- rule.onNodeWithTag("0").assertExists()
- rule.onNodeWithTag("3").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardTwice() {
- composeList()
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(2)
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(itemsSizePx / 2f)
- state.scrollBy(itemsSizePx / 2f)
- }
- }
-
- waitForPrefetch(3)
-
- rule.onNodeWithTag("2").assertIsDisplayed()
- rule.onNodeWithTag("3").assertExists()
- rule.onNodeWithTag("4").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingBackwardTwice() {
- composeList(firstItem = 4)
-
- rule.runOnIdle { runBlocking { state.scrollBy(-5f) } }
-
- waitForPrefetch(2)
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(-itemsSizePx / 2f)
- state.scrollBy(-itemsSizePx / 2f)
- }
- }
-
- waitForPrefetch(1)
-
- rule.onNodeWithTag("2").assertIsDisplayed()
- rule.onNodeWithTag("1").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAndBackwardReverseLayout() {
- composeList(firstItem = 1, reverseLayout = true)
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(3)
-
- rule.onNodeWithTag("3").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollBy(-2f)
- state.scrollBy(-1f)
- }
- }
-
- waitForPrefetch(0)
-
- rule.onNodeWithTag("0").assertExists()
- rule.onNodeWithTag("3").assertDoesNotExist()
- }
-
- @Test
- fun prefetchingForwardAndBackwardWithContentPadding() {
- val halfItemSize = itemsSizeDp / 2f
- composeList(
- firstItem = 2,
- itemOffset = 5,
- contentPadding = PaddingValues(mainAxis = halfItemSize)
- )
-
- rule.onNodeWithTag("1").assertIsDisplayed()
- rule.onNodeWithTag("2").assertIsDisplayed()
- rule.onNodeWithTag("3").assertIsDisplayed()
- rule.onNodeWithTag("0").assertDoesNotExist()
- rule.onNodeWithTag("4").assertDoesNotExist()
-
- rule.runOnIdle { runBlocking { state.scrollBy(5f) } }
-
- waitForPrefetch(3)
-
- rule.onNodeWithTag("4").assertExists()
- rule.onNodeWithTag("0").assertDoesNotExist()
-
- rule.runOnIdle { runBlocking { state.scrollBy(-2f) } }
-
- waitForPrefetch(0)
-
- rule.onNodeWithTag("0").assertExists()
- }
-
- @Test
- fun disposingWhilePrefetchingScheduled() {
- var emit = true
- lateinit var remeasure: Remeasurement
- rule.setContent {
- SubcomposeLayout(
- modifier =
- object : RemeasurementModifier {
- override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
- remeasure = remeasurement
- }
- }
- ) { constraints ->
- val placeable =
- if (emit) {
- subcompose(Unit) {
- state = rememberTvLazyListState()
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemsSizeDp * 1.5f),
- state,
- ) {
- items(1000) {
- Spacer(
- Modifier.mainAxisSize(itemsSizeDp)
- .then(fillParentMaxCrossAxis())
- )
- }
- }
- }
- .first()
- .measure(constraints)
- } else {
- null
- }
- layout(constraints.maxWidth, constraints.maxHeight) { placeable?.place(0, 0) }
- }
- }
-
- rule.runOnIdle {
- // this will schedule the prefetching
- runBlocking(AutoTestFrameClock()) { state.scrollBy(itemsSizePx.toFloat()) }
- // then we synchronously dispose LazyColumn
- emit = false
- remeasure.forceRemeasure()
- }
-
- rule.runOnIdle {}
- }
-
- @Test
- fun scrollingByListSizeCancelsPreviousPrefetch() {
- composeList()
-
- // now we have items 0-1 visible
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) {
- // this will move the viewport so items 1-2 are visible
- // and schedule a prefetching for 3
- state.scrollBy(itemsSizePx.toFloat())
-
- // move viewport by screen size to items 4-5, so item 3 is just behind
- // the first visible item
- state.scrollBy(itemsSizePx * 3f)
-
- // move scroll further to items 5-6, so item 3 is reused
- state.scrollBy(itemsSizePx.toFloat())
- }
- }
-
- waitForPrefetch(7)
-
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) {
- // scroll again to ensure item 3 was dropped
- state.scrollBy(itemsSizePx * 100f)
- }
- }
-
- rule.runOnIdle { assertThat(activeNodes).doesNotContain(3) }
- }
-
- private fun waitForPrefetch(index: Int) {
- rule.waitUntil { activeNodes.contains(index) && activeMeasuredNodes.contains(index) }
- }
-
- private val activeNodes = mutableSetOf<Int>()
- private val activeMeasuredNodes = mutableSetOf<Int>()
-
- private fun composeList(
- firstItem: Int = 0,
- itemOffset: Int = 0,
- reverseLayout: Boolean = false,
- contentPadding: PaddingValues = PaddingValues(0.dp)
- ) {
- rule.setContent {
- state =
- rememberTvLazyListState(
- initialFirstVisibleItemIndex = firstItem,
- initialFirstVisibleItemScrollOffset = itemOffset
- )
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemsSizeDp * 1.5f),
- state,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding
- ) {
- items(100) {
- DisposableEffect(it) {
- activeNodes.add(it)
- onDispose {
- activeNodes.remove(it)
- activeMeasuredNodes.remove(it)
- }
- }
- Spacer(
- Modifier.mainAxisSize(itemsSizeDp)
- .fillMaxCrossAxis()
- .testTag("$it")
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- activeMeasuredNodes.add(it)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- )
- }
- }
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt
deleted file mode 100644
index ff1d704..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt
+++ /dev/null
@@ -1,492 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.tv.foundation.PivotOffsets
-import com.google.common.truth.Truth
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyListSlotsReuseTest {
-
- @get:Rule val rule = createComposeRule()
-
- val itemsSizePx = 30f
- val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
-
- @Test
- fun scroll1ItemScrolledOffItemIsKeptForReuse() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
-
- val id0 = rule.onNodeWithTag("0").semanticsId()
- rule.onNodeWithTag("0").assertIsDisplayed()
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onNodeWithTag("1").assertIsDisplayed()
- }
-
- @Test
- fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
- // Semantics IDs must be fetched before scrolling.
- val id0 = rule.onNodeWithTag("0").semanticsId()
- val id1 = rule.onNodeWithTag("1").semanticsId()
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.runOnIdle { runBlocking { state.scrollToItem(2) } }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id1)
- rule.onNodeWithTag("2").assertIsDisplayed()
- }
-
- @Test
- fun checkMaxItemsKeptForReuse() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
- // Semantics IDs must be fetched before scrolling.
- val deactivatedIds = mutableListOf<Int>()
- repeat(DefaultMaxItemsToRetain) {
- deactivatedIds.add(rule.onNodeWithTag("$it").semanticsId())
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(DefaultMaxItemsToRetain + 1) } }
-
- deactivatedIds.fastForEach {
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(it)
- }
- rule.onNodeWithTag("$DefaultMaxItemsToRetain").assertDoesNotExist()
- rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}").assertIsDisplayed()
- }
-
- @Test
- fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
-
- val id0 = rule.onNodeWithTag("0").semanticsId()
- val id1 = rule.onNodeWithTag("1").semanticsId()
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.runOnIdle {
- runBlocking {
- // after this step 0 and 1 are in reusable buffer
- state.scrollToItem(2)
-
- // this step requires one item and will take the last item from the buffer - item
- // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
- state.scrollToItem(3)
- }
- }
-
- // recycled
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id1)
-
- // visible
- rule.onNodeWithTag("3").assertIsDisplayed()
- rule.onNodeWithTag("4").assertIsDisplayed()
- }
-
- @Test
- fun doMultipleScrollsOneByOne() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(1) // buffer is [0]
- state.scrollToItem(2) // 0 used, buffer is [1]
- }
- }
-
- // 3 should be visible at this point, so save its ID to check later
- val id3 = rule.onNodeWithTag("3").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(3) // 1 used, buffer is [2]
- state.scrollToItem(4) // 2 used, buffer is [3]
- }
- }
-
- // recycled
- rule.onNodeWithTag("0").assertDoesNotExist()
- rule.onNodeWithTag("1").assertDoesNotExist()
- rule.onNodeWithTag("2").assertDoesNotExist()
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id3)
-
- // visible
- rule.onNodeWithTag("4").assertIsDisplayed()
- rule.onNodeWithTag("5").assertIsDisplayed()
- }
-
- @Test
- fun scrollBackwardOnce() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState(10)
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
-
- val id10 = rule.onNodeWithTag("10").semanticsId()
- val id11 = rule.onNodeWithTag("11").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(8) // buffer is [10, 11]
- }
- }
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id10)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id11)
-
- // visible
- rule.onNodeWithTag("8").assertIsDisplayed()
- rule.onNodeWithTag("9").assertIsDisplayed()
- }
-
- @Test
- fun scrollBackwardOneByOne() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState(10)
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it").focusable()
- )
- }
- }
- }
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(9) // buffer is [11]
- state.scrollToItem(7) // 11 reused, buffer is [9]
- }
- }
- // 8 should be visible at this point, so save its ID to check later
- val id8 = rule.onNodeWithTag("8").semanticsId()
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(6) // 9 reused, buffer is [8]
- }
- }
-
- // in buffer
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id8)
-
- // visible
- rule.onNodeWithTag("6").assertIsDisplayed()
- rule.onNodeWithTag("7").assertIsDisplayed()
- }
-
- @Test
- fun scrollingBackReusesTheSameSlot() {
- lateinit var state: TvLazyListState
- var counter0 = 0
- var counter1 = 0
-
- val measureCountModifier0 =
- Modifier.layout { measurable, constraints ->
- counter0++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(IntOffset.Zero) }
- }
-
- val measureCountModifier1 =
- Modifier.layout { measurable, constraints ->
- counter1++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(IntOffset.Zero) }
- }
-
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 1.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100) {
- val modifier =
- when (it) {
- 0 -> measureCountModifier0
- 1 -> measureCountModifier1
- else -> Modifier
- }
- Spacer(
- Modifier.height(itemsSizeDp)
- .fillParentMaxWidth()
- .testTag("$it")
- .then(modifier)
- )
- }
- }
- }
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(2) // buffer is [0, 1]
- }
- }
-
- // 2 and 3 should be visible at this point, so save its ID to check later
- val id2 = rule.onNodeWithTag("2").semanticsId()
- val id3 = rule.onNodeWithTag("3").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- counter0 = 0
- counter1 = 0
- state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
- }
- }
-
- rule.runOnIdle {
- Truth.assertWithMessage("Item 0 measured $counter0 times, expected 0.")
- .that(counter0)
- .isEqualTo(0)
- Truth.assertWithMessage("Item 1 measured $counter1 times, expected 0.")
- .that(counter1)
- .isEqualTo(0)
- }
-
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id2)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id3)
- }
-
- @Test
- fun differentContentTypes() {
- lateinit var state: TvLazyListState
- val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
- val startOfType1 = DefaultMaxItemsToRetain + 1
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(100, contentType = { if (it >= startOfType1) 1 else 0 }) {
- Box(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it").focusable())
- }
- }
- }
-
- val deactivatedIds = mutableListOf<Int>()
- for (i in 0 until visibleItemsCount) {
- deactivatedIds.add(rule.onNodeWithTag("$i").semanticsId())
- rule.onNodeWithTag("$i").assertIsDisplayed()
- }
- for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
- deactivatedIds.add(rule.onNodeWithTag("$i").fetchSemanticsNode().id)
- }
-
- rule.runOnIdle { runBlocking { state.scrollToItem(visibleItemsCount) } }
-
- rule.onNodeWithTag("$visibleItemsCount").assertIsDisplayed()
-
- // [DefaultMaxItemsToRetain] items of type 0 are left for reuse and 7 items of type 1
- deactivatedIds.fastForEach {
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(it)
- }
-
- rule.onNodeWithTag("$DefaultMaxItemsToRetain").assertDoesNotExist()
- rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}").assertDoesNotExist()
- }
-
- @Test
- fun differentTypesFromDifferentItemCalls() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- TvLazyColumn(
- Modifier.height(itemsSizeDp * 2.5f),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- val content =
- @Composable { tag: String ->
- Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag).focusable())
- }
- item(contentType = "not-to-reuse-0") { content("0") }
- item(contentType = "reuse") { content("1") }
- items(
- List(100) { it + 2 },
- contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }
- ) {
- content("$it")
- }
- }
- }
-
- val id0 = rule.onNodeWithTag("0").semanticsId()
- val id1 = rule.onNodeWithTag("1").semanticsId()
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(2)
- // now items 0 and 1 are put into reusables
- }
- }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id1)
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(9)
- // item 10 should reuse slot 1
- }
- }
-
- rule.onRoot().fetchSemanticsNode().assertLayoutDeactivatedById(id0)
- rule.onNodeWithTag("1").assertDoesNotExist()
- rule.onNodeWithTag("9").assertIsDisplayed()
- rule.onNodeWithTag("10").assertIsDisplayed()
- rule.onNodeWithTag("11").assertIsDisplayed()
- }
-
- private fun SemanticsNode.assertLayoutDeactivatedById(id: Int) {
- children.fastForEach {
- if (it.id == id) {
- assert(it.layoutInfo.isDeactivated)
- }
- }
- }
-}
-
-private val DefaultMaxItemsToRetain = 7
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
deleted file mode 100644
index 3d39fbe..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
+++ /dev/null
@@ -1,1648 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import android.os.Build
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
-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.requiredHeight
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.requiredSizeIn
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.testutils.WithTouchSlop
-import androidx.compose.testutils.assertPixels
-import androidx.compose.testutils.assertShape
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
-import androidx.compose.ui.test.SemanticsMatcher.Companion.keyNotDefined
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import androidx.tv.foundation.lazy.grid.keyPress
-import com.google.common.collect.Range
-import com.google.common.truth.IntegerSubject
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import java.util.concurrent.CountDownLatch
-import kotlin.math.roundToInt
-import kotlinx.coroutines.runBlocking
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(orientation) {
- @Suppress("PrivatePropertyName") private val LazyListTag = "LazyListTag"
- @Suppress("PrivatePropertyName") private val FirstItemTag = "firstItemTag"
-
- @Test
- fun lazyListShowsCombinedItems() {
- val itemTestTag = "itemTestTag"
- val items = listOf(1, 2).map { it.toString() }
- val indexedItems = listOf(3, 4, 5)
-
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.mainAxisSize(200.dp)) {
- item {
- Spacer(
- Modifier.mainAxisSize(40.dp)
- .then(fillParentMaxCrossAxis())
- .testTag(itemTestTag)
- )
- }
- items(items) {
- Spacer(Modifier.mainAxisSize(40.dp).then(fillParentMaxCrossAxis()).testTag(it))
- }
- itemsIndexed(indexedItems) { index, item ->
- Spacer(
- Modifier.mainAxisSize(41.dp)
- .then(fillParentMaxCrossAxis())
- .testTag("$index-$item")
- )
- }
- }
- }
-
- rule.onNodeWithTag(itemTestTag).assertIsDisplayed()
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("0-3").assertIsDisplayed()
-
- rule.onNodeWithTag("1-4").assertIsDisplayed()
-
- rule.onNodeWithTag("2-5").assertDoesNotExist()
- }
-
- @Test
- fun lazyListAllowEmptyListItems() {
- val itemTag = "itemTag"
-
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow {
- items(emptyList<Any>()) {}
- item { Spacer(Modifier.size(10.dp).testTag(itemTag)) }
- }
- }
-
- rule.onNodeWithTag(itemTag).assertIsDisplayed()
- }
-
- @Test
- fun lazyListAllowsNullableItems() {
- val items = listOf("1", null, "3")
- val nullTestTag = "nullTestTag"
-
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.mainAxisSize(200.dp)) {
- items(items) {
- if (it != null) {
- Spacer(
- Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis()).testTag(it)
- )
- } else {
- Spacer(
- Modifier.mainAxisSize(101.dp)
- .then(fillParentMaxCrossAxis())
- .testTag(nullTestTag)
- )
- }
- }
- }
- }
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag(nullTestTag).assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertDoesNotExist()
- }
-
- @Test
- fun lazyListOnlyVisibleItemsAdded() {
- val items = (1..4).map { it.toString() }
-
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.mainAxisSize(200.dp)) {
- LazyColumnOrRow(pivotOffsets = PivotOffsets(parentFraction = 0.4f)) {
- items(items) {
- Spacer(
- Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis()).testTag(it)
- )
- }
- }
- }
- }
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertDoesNotExist()
-
- rule.onNodeWithTag("4").assertDoesNotExist()
- }
-
- @Test
- fun lazyListScrollToShowItems123() {
- val items = (1..4).map { it.toString() }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.mainAxisSize(200.dp)) {
- LazyColumnOrRow(
- modifier = Modifier.testTag(LazyListTag),
- pivotOffsets = PivotOffsets(parentFraction = 0.3f)
- ) {
- items(items) {
- Box(
- Modifier.mainAxisSize(101.dp)
- .then(fillParentMaxCrossAxis())
- .testTag(it)
- .focusable()
- .border(3.dp, Color.Red)
- ) {
- BasicText(it)
- }
- }
- }
- }
- }
-
- rule.keyPress(2)
-
- rule.onNodeWithTag("1").assertIsDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertIsDisplayed()
-
- rule.onNodeWithTag("4").assertIsNotDisplayed()
- }
-
- @Test
- fun lazyListScrollToHideFirstItem() {
- val items = (1..4).map { it.toString() }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.mainAxisSize(200.dp)) {
- LazyColumnOrRow(modifier = Modifier.testTag(LazyListTag)) {
- items(items) {
- Box(
- Modifier.mainAxisSize(101.dp)
- .then(fillParentMaxCrossAxis())
- .testTag(it)
- .focusable()
- )
- }
- }
- }
- }
-
- rule.keyPress(2)
-
- rule.onNodeWithTag("1").assertIsNotDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertIsDisplayed()
- }
-
- @Test
- fun lazyListScrollToShowItems234() {
- val items = (1..4).map { it.toString() }
-
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.mainAxisSize(200.dp)) {
- LazyColumnOrRow(
- modifier = Modifier.testTag(LazyListTag),
- pivotOffsets = PivotOffsets(parentFraction = 0.3f)
- ) {
- items(items) {
- Box(
- Modifier.mainAxisSize(101.dp)
- .then(fillParentMaxCrossAxis())
- .testTag(it)
- .focusable()
- )
- }
- }
- }
- }
-
- rule.keyPress(3)
-
- rule.onNodeWithTag("1").assertIsNotDisplayed()
-
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.onNodeWithTag("3").assertIsDisplayed()
-
- rule.onNodeWithTag("4").assertIsDisplayed()
- }
-
- @Test
- fun lazyListWrapsContent() =
- with(rule.density) {
- val itemInsideLazyList = "itemInsideLazyList"
- val itemOutsideLazyList = "itemOutsideLazyList"
- var sameSizeItems by mutableStateOf(true)
-
- rule.setContentWithTestViewConfiguration {
- Column {
- LazyColumnOrRow(Modifier.testTag(LazyListTag)) {
- items(listOf(1, 2)) {
- if (it == 1) {
- Spacer(Modifier.size(50.dp).testTag(itemInsideLazyList))
- } else {
- Spacer(Modifier.size(if (sameSizeItems) 50.dp else 70.dp))
- }
- }
- }
- Spacer(Modifier.size(50.dp).testTag(itemOutsideLazyList))
- }
- }
-
- rule.onNodeWithTag(itemInsideLazyList).assertIsDisplayed()
-
- rule.onNodeWithTag(itemOutsideLazyList).assertIsDisplayed()
-
- var lazyListBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
- var mainAxisEndBound = if (vertical) lazyListBounds.bottom else lazyListBounds.right
- var crossAxisEndBound = if (vertical) lazyListBounds.right else lazyListBounds.bottom
-
- assertThat(lazyListBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
- assertThat(mainAxisEndBound.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
- assertThat(lazyListBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
- assertThat(crossAxisEndBound.roundToPx()).isWithin1PixelFrom(50.dp.roundToPx())
-
- rule.runOnIdle { sameSizeItems = false }
-
- rule.waitForIdle()
-
- rule.onNodeWithTag(itemInsideLazyList).assertIsDisplayed()
-
- rule.onNodeWithTag(itemOutsideLazyList).assertIsDisplayed()
-
- lazyListBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
- mainAxisEndBound = if (vertical) lazyListBounds.bottom else lazyListBounds.right
- crossAxisEndBound = if (vertical) lazyListBounds.right else lazyListBounds.bottom
-
- assertThat(lazyListBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
- assertThat(mainAxisEndBound.roundToPx()).isWithin1PixelFrom(120.dp.roundToPx())
- assertThat(lazyListBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
- assertThat(crossAxisEndBound.roundToPx()).isWithin1PixelFrom(70.dp.roundToPx())
- }
-
- @Test
- fun compositionsAreDisposed_whenNodesAreScrolledOff() {
- var composed: Boolean
- var disposed = false
- // Ten 31dp spacers in a 300dp list
- val latch = CountDownLatch(10)
-
- rule.setContentWithTestViewConfiguration {
- // Fixed size to eliminate device size as a factor
- Box(Modifier.testTag(LazyListTag).mainAxisSize(300.dp)) {
- LazyColumnOrRow(Modifier.fillMaxSize()) {
- items(50) {
- DisposableEffect(NeverEqualObject) {
- composed = true
- // Signal when everything is done composing
- latch.countDown()
- onDispose { disposed = true }
- }
-
- // There will be 10 of these in the 300dp box
- Box(Modifier.mainAxisSize(31.dp).focusable()) { BasicText(it.toString()) }
- }
- }
- }
- }
-
- latch.await()
- composed = false
-
- assertWithMessage("Compositions were disposed before we did any scrolling")
- .that(disposed)
- .isFalse()
-
- // Mostly a validity check, this is not part of the behavior under test
- assertWithMessage("Additional composition occurred for no apparent reason")
- .that(composed)
- .isFalse()
-
- Thread.sleep(5000L)
- rule.keyPress(
- if (vertical) NativeKeyEvent.KEYCODE_DPAD_DOWN else NativeKeyEvent.KEYCODE_DPAD_RIGHT,
- 13
- )
- Thread.sleep(5000L)
-
- rule.waitForIdle()
-
- assertWithMessage("No additional items were composed after scroll, scroll didn't work")
- .that(composed)
- .isTrue()
-
- // We may need to modify this test once we prefetch/cache items outside the viewport
- assertWithMessage("No compositions were disposed after scrolling, compositions were leaked")
- .that(disposed)
- .isTrue()
- }
-
- @Test
- fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
- val thirdTag = "third"
- val items = (1..3).toList()
- var thirdHasSize by mutableStateOf(false)
-
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.fillMaxCrossAxis().mainAxisSize(100.dp).testTag(LazyListTag)) {
- items(items) {
- if (it == 3) {
- Box(
- Modifier.testTag(thirdTag)
- .then(fillParentMaxCrossAxis())
- .mainAxisSize(if (thirdHasSize) 60.dp else 0.dp)
- .focusable()
- )
- } else {
- Box(Modifier.then(fillParentMaxCrossAxis()).mainAxisSize(60.dp).focusable())
- }
- }
- }
- }
-
- rule.waitForIdle()
- rule.keyPress(2)
-
- rule.onNodeWithTag(thirdTag).assertExists().assertIsNotDisplayed()
-
- rule.runOnIdle { thirdHasSize = true }
-
- rule.waitForIdle()
-
- rule.keyPress(2)
-
- rule.onNodeWithTag(thirdTag).assertIsDisplayed()
- }
-
- @Test
- fun itemFillingParentWidth() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
- items(listOf(0)) {
- Spacer(
- Modifier.fillParentMaxWidth().requiredHeight(50.dp).testTag(FirstItemTag)
- )
- }
- }
- }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(100.dp).assertHeightIsEqualTo(50.dp)
- }
-
- @Test
- fun itemFillingParentHeight() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
- items(listOf(0)) {
- Spacer(
- Modifier.requiredWidth(50.dp).fillParentMaxHeight().testTag(FirstItemTag)
- )
- }
- }
- }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(50.dp).assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentSize() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
- items(listOf(0)) { Spacer(Modifier.fillParentMaxSize().testTag(FirstItemTag)) }
- }
- }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(100.dp).assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentWidthFraction() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
- items(listOf(0)) {
- Spacer(
- Modifier.fillParentMaxWidth(0.7f)
- .requiredHeight(50.dp)
- .testTag(FirstItemTag)
- )
- }
- }
- }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(70.dp).assertHeightIsEqualTo(50.dp)
- }
-
- @Test
- fun itemFillingParentHeightFraction() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
- items(listOf(0)) {
- Spacer(
- Modifier.requiredWidth(50.dp)
- .fillParentMaxHeight(0.3f)
- .testTag(FirstItemTag)
- )
- }
- }
- }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(50.dp).assertHeightIsEqualTo(45.dp)
- }
-
- @Test
- fun itemFillingParentSizeFraction() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
- items(listOf(0)) { Spacer(Modifier.fillParentMaxSize(0.5f).testTag(FirstItemTag)) }
- }
- }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(50.dp).assertHeightIsEqualTo(75.dp)
- }
-
- @Test
- fun itemFillingParentSizeParentResized() {
- var parentSize by mutableStateOf(100.dp)
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(parentSize)) {
- items(listOf(0)) { Spacer(Modifier.fillParentMaxSize().testTag(FirstItemTag)) }
- }
- }
-
- rule.runOnIdle { parentSize = 150.dp }
-
- rule.onNodeWithTag(FirstItemTag).assertWidthIsEqualTo(150.dp).assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentSizeParentRecomposed_noRemeasureOnReuse() {
- var counter = 0
- val modifier =
- Modifier.layout { measurable, constraints ->
- counter++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(IntOffset.Zero) }
- }
-
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- LazyColumnOrRow(state = state) {
- items(2) {
- Spacer(
- Modifier.fillParentMaxSize().run {
- then(if (it == 0) modifier else Modifier)
- }
- )
- }
- }
- }
-
- rule.runOnIdle {
- runBlocking {
- state.scrollToItem(1)
- state.scrollToItem(0)
- }
- }
-
- assertThat(counter).isEqualTo(1)
- }
-
- @Test
- fun whenNotAnymoreAvailableItemWasDisplayed() {
- var items by mutableStateOf((1..30).toList())
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(items) { Box(Modifier.requiredSize(20.dp).testTag("$it").focusable()) }
- }
- }
-
- // after scroll we will display items 16-20
- rule.keyPress(17)
-
- rule.runOnIdle { items = (1..10).toList() }
-
- // there is no item 16 anymore so we will just display the last items 6-10
- rule.onNodeWithTag("6").assertStartPositionIsAlmost(0.dp)
- }
-
- @Test
- fun whenFewDisplayedItemsWereRemoved() {
- var items by mutableStateOf((1..10).toList())
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it").focusable()) }
- }
- }
-
- // after scroll we will display items 6-10
- rule.keyPress(5)
- rule.runOnIdle { items = (1..8).toList() }
-
- // there are no more items 9 and 10, so we have to scroll back
- rule.onNodeWithTag("4").assertStartPositionIsAlmost(0.dp)
- }
-
- @Test
- fun whenItemsBecameEmpty() {
- var items by mutableStateOf((1..10).toList())
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- modifier =
- Modifier.requiredSizeIn(maxHeight = 100.dp, maxWidth = 100.dp)
- .testTag(LazyListTag)
- ) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it").focusable()) }
- }
- }
-
- // after scroll we will display items 2-6
- rule.keyPress(2)
-
- rule.runOnIdle { items = emptyList() }
-
- // there are no more items so the lazy list is zero sized
- rule.onNodeWithTag(LazyListTag).assertWidthIsEqualTo(0.dp).assertHeightIsEqualTo(0.dp)
-
- // and has no children
- rule.onNodeWithTag("1").assertIsNotPlaced()
- rule.onNodeWithTag("2").assertIsNotPlaced()
- }
-
- @Test
- fun scrollBackAndForth() {
- val items by mutableStateOf((1..20).toList())
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- // after scroll we will display items 6-10
- rule.keyPress(5)
-
- // and scroll back
- rule.keyPress(5, reverseScroll = true)
-
- rule.onNodeWithTag("1").assertStartPositionIsAlmost(0.dp)
- }
-
- @Test
- fun tryToScrollBackwardWhenAlreadyOnTop() {
- val items by mutableStateOf((1..20).toList())
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(items) { Box(Modifier.requiredSize(20.dp).testTag("$it").focusable()) }
- }
- }
-
- rule.waitForIdle()
-
- // getting focus to the first element
- rule.keyPress(2)
- // we already displaying the first item, so this should do nothing
- rule.keyPress(4, reverseScroll = true)
-
- rule.onNodeWithTag("1").assertStartPositionIsAlmost(0.dp)
- rule.onNodeWithTag("2").assertStartPositionIsAlmost(20.dp)
- rule.onNodeWithTag("3").assertStartPositionIsAlmost(40.dp)
- rule.onNodeWithTag("4").assertStartPositionIsAlmost(60.dp)
- rule.onNodeWithTag("5").assertStartPositionIsAlmost(80.dp)
- }
-
- @Test
- fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
- val items = listOf(NotStable(1), NotStable(2))
- var firstItemRecomposed = 0
- var secondItemRecomposed = 0
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(items) {
- if (it.count == 1) {
- firstItemRecomposed++
- } else {
- secondItemRecomposed++
- }
- Box(Modifier.requiredSize(75.dp).focusable())
- }
- }
- }
-
- rule.runOnIdle {
- assertThat(firstItemRecomposed).isEqualTo(1)
- assertThat(secondItemRecomposed).isEqualTo(1)
- }
-
- rule.keyPress(2)
-
- rule.runOnIdle {
- assertThat(firstItemRecomposed).isEqualTo(1)
- assertThat(secondItemRecomposed).isEqualTo(1)
- }
- }
-
- @Test
- fun onlyOneMeasurePassForScrollEvent() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- state.prefetchingEnabled = false
- LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag), state = state) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- val initialMeasurePasses = state.numMeasurePasses
-
- rule.runOnIdle { with(rule.density) { state.onScroll(-110.dp.toPx()) } }
-
- rule.waitForIdle()
-
- assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
- }
-
- @Test
- fun onlyOneInitialMeasurePass() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag), state = state) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- rule.runOnIdle { assertThat(state.numMeasurePasses).isEqualTo(1) }
- }
-
- @Test
- fun scroll_makeListSmaller_scroll() {
- var items by mutableStateOf((1..100).toList())
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(items) { Box(Modifier.requiredSize(10.dp).testTag("$it").focusable()) }
- }
- }
-
- rule.keyPress(30)
- rule.runOnIdle { items = (1..11).toList() }
-
- rule.waitForIdle()
- // try to scroll after the data set has been updated. this was causing a crash previously
- rule.keyPress(1, reverseScroll = true)
- rule.onNodeWithTag("11").assertIsDisplayed()
- }
-
- @Test
- fun initialScrollIsApplied() {
- val items by mutableStateOf((0..20).toList())
- lateinit var state: TvLazyListState
- val expectedOffset = with(rule.density) { 10.dp.roundToPx() }
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState(2, expectedOffset)
- LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag), state = state) {
- items(items) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
- }
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo((-10).dp)
- }
-
- @Test
- fun stateIsRestored() {
- val restorationTester = StateRestorationTester(rule)
- var state: TvLazyListState? = null
- restorationTester.setContent {
- state = rememberTvLazyListState()
- LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag), state = state!!) {
- items(20) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- rule.keyPress(3)
-
- val (index, scrollOffset) =
- rule.runOnIdle { state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset }
-
- state = null
-
- restorationTester.emulateSavedInstanceStateRestore()
-
- rule.runOnIdle {
- assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
- assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
- }
- }
-
- @Test
- fun snapToItemIndex() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag), state = state) {
- items(20) { Spacer(Modifier.requiredSize(20.dp).testTag("$it")) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollToItem(3, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
- }
-
- // TODO: Needs to be debugged and fixed for TV surfaces.
- /*@Test
- fun itemsAreNotRedrawnDuringScroll() {
- val items = (0..20).toList()
- val redrawCount = Array(6) { 0 }
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag),
- pivotOffsetConfig = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) {
- Box(
- Modifier.requiredSize(20.dp)
- .testTag(it.toString())
- .drawBehind {
- redrawCount[it]++
- if (redrawCount[it] != 1) {
- Log.i("REMOVE_ME", Exception("Redrawn").stackTraceToString())
- }
- }
- .focusable()
- ) {
- BasicText(it.toString())
- }
- }
- }
- }
-
- rule.keyPress(3)
- rule.onNodeWithTag("0").assertIsNotDisplayed()
- rule.runOnIdle {
- redrawCount.forEachIndexed { index, i ->
- assertWithMessage("Item with index $index was redrawn $i times")
- .that(i).isEqualTo(1)
- }
- }
- }*/
-
- @Test
- fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
- val redrawCount = Array(2) { 0 }
- var stateUsedInDrawScope by mutableStateOf(false)
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
- items(2) {
- Spacer(
- Modifier.requiredSize(50.dp).drawBehind {
- redrawCount[it]++
- if (it == 1) {
- stateUsedInDrawScope.hashCode()
- }
- }
- )
- }
- }
- }
-
- rule.runOnIdle { stateUsedInDrawScope = true }
-
- rule.runOnIdle {
- assertWithMessage("First items is not expected to be redrawn")
- .that(redrawCount[0])
- .isEqualTo(1)
- assertWithMessage("Second items is expected to be redrawn")
- .that(redrawCount[1])
- .isEqualTo(2)
- }
- }
-
- @Test
- fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
- val itemSize = with(rule.density) { 30.toDp() }
- val itemSizeMinusOne = with(rule.density) { 29.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSizeMinusOne).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it }
- ) {
- items(2) {
- Spacer(
- if (it == 0) {
- Modifier.crossAxisSize(30.dp).mainAxisSize(itemSizeMinusOne)
- } else {
- Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
- }
- )
- }
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag(LazyListTag).assertCrossAxisSizeIsEqualTo(20.dp)
- }
-
- @Test
- fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
- val items = (0..2).toList()
- val itemSize = with(rule.density) { 30.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSize * 1.75f).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it }
- ) {
- items(items) {
- Spacer(
- when (it) {
- 0 -> Modifier.crossAxisSize(30.dp).mainAxisSize(itemSize / 2)
- 1 -> Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize / 2)
- else -> Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
- }
- )
- }
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag(LazyListTag).assertCrossAxisSizeIsEqualTo(30.dp)
- }
-
- @Test
- fun usedWithArray() {
- val items = arrayOf("1", "2", "3")
-
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow { items(items) { Spacer(Modifier.requiredSize(itemSize).testTag(it)) } }
- }
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize * 2)
- }
-
- @Test
- fun usedWithArrayIndexed() {
- val items = arrayOf("1", "2", "3")
-
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow {
- itemsIndexed(items) { index, item ->
- Spacer(Modifier.requiredSize(itemSize).testTag("$index*$item"))
- }
- }
- }
-
- rule.onNodeWithTag("0*1").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1*2").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("2*3").assertStartPositionInRootIsEqualTo(itemSize * 2)
- }
-
- @Test
- fun changeItemsCountAndScrollImmediately() {
- lateinit var state: TvLazyListState
- var count by mutableStateOf(100)
- val composedIndexes = mutableListOf<Int>()
- rule.setContent {
- state = rememberTvLazyListState()
- LazyColumnOrRow(Modifier.fillMaxCrossAxis().mainAxisSize(10.dp), state) {
- items(count) { index ->
- composedIndexes.add(index)
- Box(Modifier.size(20.dp))
- }
- }
- }
-
- rule.runOnIdle {
- composedIndexes.clear()
- count = 10
- runBlocking(AutoTestFrameClock()) { state.scrollToItem(50) }
- composedIndexes.forEach { assertThat(it).isLessThan(count) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(9)
- }
- }
-
- @Test
- fun overScrollingBackwardFromNotTheFirstPosition() {
- val containerTag = "container"
- val itemSizePx = 10
- val itemSizeDp = with(rule.density) { itemSizePx.toDp() }
- val containerSize = itemSizeDp * 5
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.testTag(containerTag).size(containerSize)) {
- LazyColumnOrRow(
- Modifier.testTag(LazyListTag).background(Color.Blue),
- state = rememberTvLazyListState(2, 5)
- ) {
- items(100) {
- Box(
- Modifier.fillMaxCrossAxis()
- .mainAxisSize(itemSizeDp)
- .testTag("$it")
- .focusable()
- )
- }
- }
- }
- }
-
- rule.keyPress(
- if (vertical) NativeKeyEvent.KEYCODE_DPAD_UP else NativeKeyEvent.KEYCODE_DPAD_LEFT,
- 15
- )
-
- rule.onNodeWithTag(LazyListTag).assertMainAxisSizeIsEqualTo(containerSize)
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("4").assertStartPositionInRootIsEqualTo(containerSize - itemSizeDp)
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- @Test
- fun doesNotClipHorizontalOverdraw() {
- rule.setContent {
- Box(Modifier.size(60.dp).testTag("container").background(Color.Gray)) {
- LazyColumnOrRow(Modifier.padding(20.dp).fillMaxSize(), rememberTvLazyListState(1)) {
- items(4) { Box(Modifier.size(20.dp).drawOutsideOfBounds()) }
- }
- }
- }
-
- val horizontalPadding = if (vertical) 0.dp else 20.dp
- val verticalPadding = if (vertical) 20.dp else 0.dp
-
- rule
- .onNodeWithTag("container")
- .captureToImage()
- .assertShape(
- density = rule.density,
- shape = RectangleShape,
- shapeColor = Color.Red,
- backgroundColor = Color.Gray,
- horizontalPadding = horizontalPadding,
- verticalPadding = verticalPadding
- )
- }
-
- @Test
- fun initialScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() {
- lateinit var state: TvLazyListState
- var itemsCount by mutableStateOf(0)
- rule.setContent {
- state = rememberTvLazyListState(2, 10)
- LazyColumnOrRow(Modifier.fillMaxSize(), state) {
- items(itemsCount) { Box(Modifier.size(20.dp)) }
- }
- }
-
- rule.runOnIdle { itemsCount = 100 }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
- }
-
- @Test
- fun restoredScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() {
- lateinit var state: TvLazyListState
- var itemsCount = 100
- val recomposeCounter = mutableStateOf(0)
- val tester = StateRestorationTester(rule)
- tester.setContent {
- state = rememberTvLazyListState()
- LazyColumnOrRow(Modifier.fillMaxSize(), state) {
- recomposeCounter.value
- items(itemsCount) { Box(Modifier.size(20.dp)) }
- }
- }
-
- rule.runOnIdle {
- runBlocking { state.scrollToItem(2, 10) }
- itemsCount = 0
- }
-
- tester.emulateSavedInstanceStateRestore()
-
- rule.runOnIdle {
- itemsCount = 100
- recomposeCounter.value = 1
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
- }
-
- @Test
- fun animateScrollToItemDoesNotScrollPastItem() {
- lateinit var state: TvLazyListState
- var target = 0
- var reverse = false
- rule.setContent {
- val listState = rememberTvLazyListState()
- SideEffect { state = listState }
- LazyColumnOrRow(Modifier.fillMaxSize(), listState) {
- items(2500) { _ -> Box(Modifier.size(100.dp)) }
- }
-
- if (reverse) {
- assertThat(listState.firstVisibleItemIndex).isAtLeast(target)
- } else {
- assertThat(listState.firstVisibleItemIndex).isAtMost(target)
- }
- }
-
- // Try a bunch of different targets with varying spacing
- listOf(500, 800, 1500, 1600, 1800).forEach {
- target = it
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) { state.animateScrollToItem(target) }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(target)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- reverse = true
-
- listOf(1600, 1500, 800, 500, 0).forEach {
- target = it
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) { state.animateScrollToItem(target) }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(target)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
- }
-
- @Test
- fun animateScrollToTheLastItemWhenItemsAreLargerThenTheScreen() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- LazyColumnOrRow(Modifier.crossAxisSize(150.dp).mainAxisSize(100.dp), state) {
- items(20) { Box(Modifier.size(150.dp)) }
- }
- }
-
- // Try a bunch of different start indexes
- listOf(0, 5, 12).forEach {
- val startIndex = it
- rule.runOnIdle {
- runBlocking(AutoTestFrameClock()) {
- state.scrollToItem(startIndex)
- state.animateScrollToItem(19)
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(19)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
- }
-
- @Test
- fun recreatingContentLambdaTriggersItemRecomposition() {
- val countState = mutableStateOf(0)
- rule.setContent {
- val count = countState.value
- LazyColumnOrRow { item { BasicText(text = "Count $count") } }
- }
-
- rule.onNodeWithText("Count 0").assertIsDisplayed()
-
- rule.runOnIdle { countState.value++ }
-
- rule.onNodeWithText("Count 1").assertIsDisplayed()
- }
-
- @Test
- fun semanticsScroll_isAnimated() {
- rule.mainClock.autoAdvance = false
- val state = TvLazyListState()
-
- rule.setContent {
- LazyColumnOrRow(Modifier.testTag(LazyListTag), state = state) {
- items(50) { Box(Modifier.mainAxisSize(200.dp)) }
- }
- }
-
- rule.waitForIdle()
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-
- rule.onNodeWithTag(LazyListTag).performSemanticsAction(SemanticsActions.ScrollBy) {
- if (vertical) {
- it(0f, 100f)
- } else {
- it(100f, 0f)
- }
- }
-
- // We haven't advanced time yet, make sure it's still zero
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
-
- // Advance and make sure we're partway through
- // Note that we need two frames for the animation to actually happen
- rule.mainClock.advanceTimeByFrame()
- rule.mainClock.advanceTimeByFrame()
-
- // The items are 200dp each, so still the first one, but offset
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- assertThat(state.firstVisibleItemScrollOffset).isLessThan(100)
-
- // Finish the scroll, make sure we're at the target
- rule.mainClock.advanceTimeBy(5000)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(100)
- }
-
- @Test
- fun maxIntElements() {
- val itemSize = with(rule.density) { 15.toDp() }
-
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(itemSize * 3),
- state = TvLazyListState(firstVisibleItemIndex = Int.MAX_VALUE - 3)
- ) {
- items(Int.MAX_VALUE) { Box(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule.onNodeWithTag("${Int.MAX_VALUE - 3}").assertStartPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("${Int.MAX_VALUE - 2}").assertStartPositionInRootIsEqualTo(itemSize)
- rule.onNodeWithTag("${Int.MAX_VALUE - 1}").assertStartPositionInRootIsEqualTo(itemSize * 2)
-
- rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist()
- rule.onNodeWithTag("0").assertDoesNotExist()
- }
-
- @Test
- fun scrollingByExactlyTheItemSize_switchesTheFirstVisibleItem() {
- val itemSize = with(rule.density) { 30.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSize * 3),
- state = rememberTvLazyListState().also { state = it },
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
- }
-
- @Test
- fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
- userScrollEnabled = true,
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it").focusable()) }
- }
- }
-
- rule.keyPress(2)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
- userScrollEnabled = false,
- ) {
- items(5) {
- Box(
- modifier =
- Modifier.size(itemSize)
- .border(2.dp, Color.Blue)
- .testTag("$it")
- .focusable()
- ) {
- BasicText("$it")
- }
- }
- }
- }
-
- rule.onNodeWithTag("2").requestFocus()
- rule.keyPress(2)
-
- rule.onNodeWithTag("1").assertIsDisplayed()
- rule.onNodeWithTag("3").assertIsNotDisplayed()
- }
-
- @Test
- fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSize * 3),
- state = rememberTvLazyListState().also { state = it },
- userScrollEnabled = false,
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
- val itemSize = with(rule.density) { 30.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
- userScrollEnabled = false,
- ) {
- items(5) { Spacer(Modifier.size(itemSize).testTag("$it")) }
- }
- }
-
- rule
- .onNodeWithTag(LazyListTag)
- .assert(keyNotDefined(SemanticsActions.ScrollBy))
- .assert(keyNotDefined(SemanticsActions.ScrollToIndex))
- // but we still have a read only scroll range property
- .assert(
- keyIsDefined(
- if (vertical) {
- SemanticsProperties.VerticalScrollAxisRange
- } else {
- SemanticsProperties.HorizontalScrollAxisRange
- }
- )
- )
- }
-
- @Test
- fun withMissingItems() {
- val itemSize = with(rule.density) { 30.toDp() }
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- LazyColumnOrRow(modifier = Modifier.mainAxisSize(itemSize + 1.dp), state = state) {
- items(4) {
- if (it != 1) {
- Box(Modifier.size(itemSize).testTag(it.toString()).focusable())
- }
- }
- }
- }
-
- rule.onNodeWithTag("0").assertIsDisplayed()
- rule.onNodeWithTag("2").assertIsDisplayed()
-
- rule.runOnIdle { runBlocking { state.scrollToItem(1) } }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
- rule.onNodeWithTag("2").assertIsDisplayed()
- rule.onNodeWithTag("3").assertIsDisplayed()
- }
-
- @Test
- fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
- var remeasureCount = 0
- val layoutModifier =
- Modifier.layout { measurable, constraints ->
- remeasureCount++
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- val counter = mutableStateOf(0)
-
- rule.setContentWithTestViewConfiguration {
- counter.value // just to trigger recomposition
- LazyColumnOrRow(
- // this will return a new object everytime causing Lazy list recomposition
- // without causing remeasure
- Modifier.composed { layoutModifier }
- ) {
- items(1) { Spacer(Modifier.size(10.dp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(remeasureCount).isEqualTo(1)
- counter.value++
- }
-
- rule.runOnIdle { assertThat(remeasureCount).isEqualTo(1) }
- }
-
- @Test
- fun passingNegativeItemsCountIsNotAllowed() {
- var exception: Exception? = null
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow {
- try {
- items(-1) { Box(Modifier) }
- } catch (e: Exception) {
- exception = e
- }
- }
- }
-
- rule.runOnIdle { assertThat(exception).isInstanceOf(IllegalArgumentException::class.java) }
- }
-
- @Test
- fun scrollingALotDoesNotCauseLazyLayoutRecomposition() {
- var recomposeCount = 0
- lateinit var state: TvLazyListState
-
- rule.setContentWithTestViewConfiguration {
- state = rememberTvLazyListState()
- LazyColumnOrRow(
- Modifier.composed {
- recomposeCount++
- Modifier
- },
- state
- ) {
- items(1000) { Spacer(Modifier.size(10.dp)) }
- }
- }
-
- rule.runOnIdle {
- assertThat(recomposeCount).isEqualTo(1)
-
- runBlocking { state.scrollToItem(100) }
- }
-
- rule.runOnIdle { assertThat(recomposeCount).isEqualTo(1) }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun zIndexOnItemAffectsDrawingOrder() {
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.size(6.dp).testTag(LazyListTag)) {
- items(listOf(Color.Blue, Color.Green, Color.Red)) { color ->
- Box(
- Modifier.mainAxisSize(2.dp)
- .crossAxisSize(6.dp)
- .zIndex(if (color == Color.Green) 1f else 0f)
- .drawBehind {
- drawRect(
- color,
- topLeft = Offset(-10.dp.toPx(), -10.dp.toPx()),
- size = Size(20.dp.toPx(), 20.dp.toPx())
- )
- }
- )
- }
- }
- }
-
- rule.onNodeWithTag(LazyListTag).captureToImage().assertPixels { Color.Green }
- }
-
- @Test
- fun increasingConstraintsWhenParentMaxSizeIsUsed_correctlyMaintainsThePosition() {
- val state = TvLazyListState(1, 10)
- var constraints by mutableStateOf(Constraints.fixed(100, 100))
- rule.setContentWithTestViewConfiguration {
- Layout(
- content = {
- LazyColumnOrRow(state = state) {
- items(3) { Box(Modifier.fillParentMaxSize()) }
- }
- }
- ) { measurableList, _ ->
- val placeable = measurableList.first().measure(constraints)
- layout(constraints.maxWidth, constraints.maxHeight) { placeable.place(0, 0) }
- }
- }
-
- rule.runOnIdle { constraints = Constraints.fixed(500, 500) }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
- }
-
- @Test
- fun usingFillParentMaxSizeOnInfinityConstraintsIsIgnored() {
- rule.setContentWithTestViewConfiguration {
- Layout(
- content = {
- LazyColumnOrRow {
- items(1) { Box(Modifier.fillParentMaxSize(0.95f).testTag("item")) }
- }
- }
- ) { measurableList, _ ->
- val crossInfinityConstraints =
- if (vertical) {
- Constraints(maxWidth = Constraints.Infinity, maxHeight = 100)
- } else {
- Constraints(maxWidth = 100, maxHeight = Constraints.Infinity)
- }
- val placeable = measurableList.first().measure(crossInfinityConstraints)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- }
-
- rule
- .onNodeWithTag("item")
- .assertMainAxisSizeIsEqualTo(with(rule.density) { (100 * 0.95f).roundToInt().toDp() })
- .assertCrossAxisSizeIsEqualTo(0.dp)
- }
-
- @Test
- fun fillingFullSize_nextItemIsNotComposed() {
- val state = TvLazyListState()
- state.prefetchingEnabled = false
- val itemSizePx = 5f
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.testTag(LazyListTag).mainAxisSize(itemSize), state = state) {
- items(3) { index ->
- Box(fillParentMaxMainAxis().crossAxisSize(1.dp).testTag("$index"))
- }
- }
- }
-
- repeat(3) { index ->
- rule.onNodeWithTag("$index").assertIsDisplayed()
- rule.onNodeWithTag("${index + 1}").assertDoesNotExist()
- rule.runOnIdle { runBlocking { state.scrollBy(itemSizePx) } }
- }
- }
-
- @Test
- fun fillingFullSize_crossAxisSizeOfVisibleItemIsUsed() {
- val state = TvLazyListState()
- val itemSizePx = 5f
- val itemSize = with(rule.density) { itemSizePx.toDp() }
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(Modifier.testTag(LazyListTag).mainAxisSize(itemSize), state = state) {
- items(5) { index -> Box(fillParentMaxMainAxis().crossAxisSize(index.dp)) }
- }
- }
-
- repeat(5) { index ->
- rule.onNodeWithTag(LazyListTag).assertCrossAxisSizeIsEqualTo(index.dp)
- rule.runOnIdle { runBlocking { state.scrollBy(itemSizePx) } }
- }
- }
-
- // ********************* END OF TESTS *********************
- // Helper functions, etc. live below here
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
- }
-}
-
-internal val NeverEqualObject =
- object {
- override fun equals(other: Any?): Boolean {
- return false
- }
- }
-
-private data class NotStable(val count: Int)
-
-internal const val TestTouchSlop = 18f
-
-internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
- isEqualTo(expected, 1)
-}
-
-internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
- isIn(Range.closed(expected - tolerance, expected + tolerance))
-}
-
-internal fun ComposeContentTestRule.setContentWithTestViewConfiguration(
- composable: @Composable () -> Unit
-) {
- this.setContent { WithTouchSlop(TestTouchSlop, composable) }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsContentPaddingTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsContentPaddingTest.kt
deleted file mode 100644
index e400bb3..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsContentPaddingTest.kt
+++ /dev/null
@@ -1,718 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-class LazyListsContentPaddingTest(orientation: Orientation) :
- BaseLazyListTestWithOrientation(orientation) {
-
- private val LazyListTag = "LazyList"
- private val ItemTag = "item"
- private val ContainerTag = "container"
-
- private var itemSize: Dp = Dp.Infinity
- private var smallPaddingSize: Dp = Dp.Infinity
- private var itemSizePx = 50f
- private var smallPaddingSizePx = 12f
-
- @Before
- fun before() {
- with(rule.density) {
- itemSize = itemSizePx.toDp()
- smallPaddingSize = smallPaddingSizePx.toDp()
- }
- }
-
- @Test
- fun contentPaddingIsApplied() {
- lateinit var state: TvLazyListState
- val containerSize = itemSize * 2
- val largePaddingSize = itemSize
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(containerSize).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it },
- contentPadding =
- PaddingValues(mainAxis = largePaddingSize, crossAxis = smallPaddingSize)
- ) {
- items(listOf(1)) {
- Spacer(
- Modifier.then(fillParentMaxCrossAxis())
- .mainAxisSize(itemSize)
- .testTag(ItemTag)
- )
- }
- }
- }
-
- rule
- .onNodeWithTag(ItemTag)
- .assertCrossAxisStartPositionInRootIsEqualTo(smallPaddingSize)
- .assertStartPositionInRootIsEqualTo(largePaddingSize)
- .assertCrossAxisSizeIsEqualTo(containerSize - smallPaddingSize * 2)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- state.scrollBy(largePaddingSize)
-
- rule
- .onNodeWithTag(ItemTag)
- .assertStartPositionInRootIsEqualTo(0.dp)
- .assertMainAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun contentPaddingIsNotAffectingScrollPosition() {
- lateinit var state: TvLazyListState
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(itemSize * 2).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it },
- contentPadding = PaddingValues(mainAxis = itemSize)
- ) {
- items(listOf(1)) {
- Spacer(
- Modifier.then(fillParentMaxCrossAxis())
- .mainAxisSize(itemSize)
- .testTag(ItemTag)
- )
- }
- }
- }
-
- state.assertScrollPosition(0, 0.dp)
-
- state.scrollBy(itemSize)
-
- state.assertScrollPosition(0, itemSize)
- }
-
- @Test
- fun scrollForwardItemWithinStartPaddingDisplayed() {
- lateinit var state: TvLazyListState
- val padding = itemSize * 1.5f
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(padding * 2 + itemSize).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it },
- contentPadding = PaddingValues(mainAxis = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(padding)
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize + padding)
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize * 2 + padding)
-
- state.scrollBy(padding)
-
- state.assertScrollPosition(1, padding - itemSize)
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize)
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize * 2)
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize * 3)
- }
-
- @Test
- fun scrollBackwardItemWithinStartPaddingDisplayed() {
- lateinit var state: TvLazyListState
- val padding = itemSize * 1.5f
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(itemSize + padding * 2).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it },
- contentPadding = PaddingValues(mainAxis = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
- state.scrollBy(-itemSize * 1.5f)
-
- state.assertScrollPosition(1, itemSize * 0.5f)
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(itemSize * 1.5f - padding)
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize * 2.5f - padding)
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize * 3.5f - padding)
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize * 4.5f - padding)
- }
-
- @Test
- fun scrollForwardTillTheEnd() {
- lateinit var state: TvLazyListState
- val padding = itemSize * 1.5f
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(padding * 2 + itemSize).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it },
- contentPadding = PaddingValues(mainAxis = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
-
- state.assertScrollPosition(3, 0.dp)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize - padding)
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize * 2 - padding)
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize * 3 - padding)
-
- // there are no space to scroll anymore, so it should change nothing
- state.scrollBy(10.dp)
-
- state.assertScrollPosition(3, 0.dp)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize - padding)
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize * 2 - padding)
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize * 3 - padding)
- }
-
- @Test
- fun scrollForwardTillTheEndAndABitBack() {
- lateinit var state: TvLazyListState
- val padding = itemSize * 1.5f
- rule.setContent {
- LazyColumnOrRow(
- modifier = Modifier.requiredSize(padding * 2 + itemSize).testTag(LazyListTag),
- state = rememberTvLazyListState().also { state = it },
- contentPadding = PaddingValues(mainAxis = padding)
- ) {
- items((0..3).toList()) {
- Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
- state.scrollBy(-itemSize / 2)
-
- state.assertScrollPosition(2, itemSize / 2)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize * 1.5f - padding)
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize * 2.5f - padding)
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize * 3.5f - padding)
- }
-
- @Test
- fun contentPaddingAndWrapContent() {
- rule.setContent {
- Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyColumnOrRow(
- contentPadding =
- PaddingValues(
- beforeContentCrossAxis = 2.dp,
- beforeContent = 4.dp,
- afterContentCrossAxis = 6.dp,
- afterContent = 8.dp
- )
- ) {
- items(listOf(1)) { Spacer(Modifier.requiredSize(itemSize).testTag(ItemTag)) }
- }
- }
- }
-
- rule
- .onNodeWithTag(ItemTag)
- .assertCrossAxisStartPositionInRootIsEqualTo(2.dp)
- .assertStartPositionInRootIsEqualTo(4.dp)
- .assertCrossAxisSizeIsEqualTo(itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(itemSize + 2.dp + 6.dp)
- .assertMainAxisSizeIsEqualTo(itemSize + 4.dp + 8.dp)
- }
-
- @Test
- fun contentPaddingAndNoContent() {
- rule.setContent {
- Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyColumnOrRow(
- contentPadding =
- PaddingValues(
- beforeContentCrossAxis = 2.dp,
- beforeContent = 4.dp,
- afterContentCrossAxis = 6.dp,
- afterContent = 8.dp
- )
- ) {}
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(8.dp)
- .assertMainAxisSizeIsEqualTo(12.dp)
- }
-
- @Test
- fun contentPaddingAndZeroSizedItem() {
- rule.setContent {
- Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyColumnOrRow(
- contentPadding =
- PaddingValues(
- beforeContentCrossAxis = 2.dp,
- beforeContent = 4.dp,
- afterContentCrossAxis = 6.dp,
- afterContent = 8.dp
- )
- ) {
- items(0) {}
- }
- }
- }
-
- rule
- .onNodeWithTag(ContainerTag)
- .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
- .assertStartPositionInRootIsEqualTo(0.dp)
- .assertCrossAxisSizeIsEqualTo(8.dp)
- .assertMainAxisSizeIsEqualTo(12.dp)
- }
-
- @Test
- fun contentPaddingAndReverseLayout() {
- val topPadding = itemSize * 2
- val bottomPadding = itemSize / 2
- val listSize = itemSize * 3
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- LazyColumnOrRow(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(listSize),
- contentPadding =
- PaddingValues(beforeContent = topPadding, afterContent = bottomPadding),
- ) {
- items(3) { index -> Box(Modifier.requiredSize(itemSize).testTag("$index")) }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertStartPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
- rule
- .onNodeWithTag("1")
- .assertStartPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
- // Partially visible.
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(-itemSize / 2)
-
- // Scroll to the top.
- state.scrollBy(itemSize * 2.5f)
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(topPadding)
- // Shouldn't be visible
- rule.onNodeWithTag("1").assertIsNotDisplayed()
- rule.onNodeWithTag("0").assertIsNotDisplayed()
- }
-
- @Test
- fun overscrollWithContentPadding() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = smallPaddingSize)
- ) {
- items(2) { Box(Modifier.testTag("$it").fillParentMaxSize()) }
- }
- }
- }
-
- rule
- .onNodeWithTag("0")
- .assertStartPositionInRootIsEqualTo(smallPaddingSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("1")
- .assertStartPositionInRootIsEqualTo(smallPaddingSize + itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule.runOnIdle {
- runBlocking {
- // itemSizePx is the maximum offset, plus if we overscroll the content padding
- // the layout mechanism will decide the item 0 is not needed until we start
- // filling the over scrolled gap.
- state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
- }
- }
-
- rule
- .onNodeWithTag("1")
- .assertStartPositionInRootIsEqualTo(smallPaddingSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
-
- rule
- .onNodeWithTag("0")
- .assertStartPositionInRootIsEqualTo(smallPaddingSize - itemSize)
- .assertMainAxisSizeIsEqualTo(itemSize)
- }
-
- @Test
- fun totalPaddingLargerParentSize_initialState() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- rule.runOnIdle {
- state.assertScrollPosition(0, 0.dp)
- state.assertVisibleItems(0 to 0.dp)
- state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
- }
- }
-
- @Ignore("b/283960394")
- @Test
- fun totalPaddingLargerParentSize_scrollByPadding() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(1, 0.dp)
- state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollToLastItem() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollTo(3)
-
- rule.onNodeWithTag("1").assertDoesNotExist()
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
-
- rule.onNodeWithTag("1").assertIsNotDisplayed()
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun totalPaddingLargerParentSize_scrollTillTheEnd() {
- // the whole end content padding is displayed
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 4.5f)
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.onNodeWithTag("3").assertStartPositionInRootIsEqualTo(-itemSize * 0.5f)
-
- rule.runOnIdle {
- state.assertScrollPosition(3, itemSize * 1.5f)
- state.assertVisibleItems(3 to -itemSize * 1.5f)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_initialState() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(0, 0.dp)
- state.assertVisibleItems(0 to 0.dp)
- state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollByPadding() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 2)
-
- rule.onNodeWithTag("0").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("2").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(2, 0.dp)
- state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollToLastItem() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollTo(3)
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(itemSize * 3)
-
- rule.onNodeWithTag("0").assertIsNotDisplayed()
-
- rule.onNodeWithTag("1").assertStartPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(itemSize)
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(3, 0.dp)
- state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
- }
- }
-
- @Test
- fun eachPaddingLargerParentSize_scrollTillTheEnd() {
- // only the end content padding is displayed
- lateinit var state: TvLazyListState
- rule.setContent {
- state = rememberTvLazyListState()
- Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
- LazyColumnOrRow(
- state = state,
- contentPadding = PaddingValues(mainAxis = itemSize * 2)
- ) {
- items(4) { Box(Modifier.testTag("$it").size(itemSize)) }
- }
- }
- }
-
- state.scrollBy(
- itemSize * 1.5f + // container size
- itemSize * 2 + // start padding
- itemSize * 3 // all items
- )
-
- rule.onNodeWithTag("3").assertIsNotDisplayed()
-
- rule.runOnIdle {
- state.assertScrollPosition(3, itemSize * 3.5f)
- state.assertVisibleItems(3 to -itemSize * 3.5f)
- }
- }
-
- private fun TvLazyListState.assertScrollPosition(index: Int, offset: Dp) =
- with(rule.density) {
- assertThat(firstVisibleItemIndex).isEqualTo(index)
- assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
- }
-
- private fun TvLazyListState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) =
- with(rule.density) {
- assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
- .isEqualTo(from.roundToPx() to to.roundToPx())
- }
-
- private fun TvLazyListState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
- with(rule.density) {
- assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset })
- .isEqualTo(expected.map { it.first to it.second.roundToPx() })
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsIndexedTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsIndexedTest.kt
deleted file mode 100644
index 2556592..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsIndexedTest.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-import org.junit.Rule
-import org.junit.Test
-
-class LazyListsIndexedTest {
-
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun lazyColumnShowsIndexedItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- TvLazyColumn(
- Modifier.height(200.dp),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- itemsIndexed(items) { index, item ->
- Spacer(
- Modifier.height(101.dp)
- .fillParentMaxWidth()
- .testTag("$index-$item")
- .focusable()
- )
- }
- }
- }
-
- rule.onNodeWithTag("0-1").assertIsDisplayed()
-
- rule.onNodeWithTag("1-2").assertIsDisplayed()
-
- rule.onNodeWithTag("2-3").assertDoesNotExist()
-
- rule.onNodeWithTag("3-4").assertDoesNotExist()
- }
-
- @Test
- fun columnWithIndexesComposedWithCorrectIndexAndItem() {
- val items = (0..1).map { it.toString() }
-
- rule.setContent {
- TvLazyColumn(
- Modifier.height(200.dp),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- itemsIndexed(items) { index, item ->
- BasicText(
- "${index}x$item",
- Modifier.fillParentMaxWidth().requiredHeight(100.dp)
- )
- }
- }
- }
-
- rule.onNodeWithText("0x0").assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithText("1x1").assertTopPositionInRootIsEqualTo(100.dp)
- }
-
- @Test
- fun lazyRowShowsIndexedItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- TvLazyRow(Modifier.width(200.dp), pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- itemsIndexed(items) { index, item ->
- Spacer(
- Modifier.width(101.dp)
- .fillParentMaxHeight()
- .testTag("$index-$item")
- .focusable()
- )
- }
- }
- }
-
- rule.onNodeWithTag("0-1").assertIsDisplayed()
-
- rule.onNodeWithTag("1-2").assertIsDisplayed()
-
- rule.onNodeWithTag("2-3").assertDoesNotExist()
-
- rule.onNodeWithTag("3-4").assertDoesNotExist()
- }
-
- @Test
- fun rowWithIndexesComposedWithCorrectIndexAndItem() {
- val items = (0..1).map { it.toString() }
-
- rule.setContent {
- TvLazyRow(Modifier.width(200.dp), pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- itemsIndexed(items) { index, item ->
- BasicText(
- "${index}x$item",
- Modifier.fillParentMaxHeight().requiredWidth(100.dp).focusable()
- )
- }
- }
- }
-
- rule.onNodeWithText("0x0").assertLeftPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithText("1x1").assertLeftPositionInRootIsEqualTo(100.dp)
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt
deleted file mode 100644
index 567d80c..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt
+++ /dev/null
@@ -1,454 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.grid.keyPress
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyListsReverseLayoutTest {
-
- @Suppress("PrivatePropertyName") private val ContainerTag = "ContainerTag"
-
- @get:Rule val rule = createComposeRule()
-
- private var itemSize: Dp = Dp.Infinity
-
- @Before
- fun before() {
- with(rule.density) { itemSize = 50.toDp() }
- }
-
- @Test
- fun column_emitTwoElementsAsOneItem_positionedReversed() {
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(reverseLayout = true, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item {
- Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
- Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
- }
- }
- }
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun column_emitTwoItems_positionedReversed() {
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(reverseLayout = true, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item { Box(Modifier.requiredSize(itemSize).testTag("0").focusable()) }
- item { Box(Modifier.requiredSize(itemSize).testTag("1").focusable()) }
- }
- }
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun column_initialScrollPositionIs0() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
- }
-
- @Test
- fun column_scrollInWrongDirectionDoesNothing() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- // we scroll down and as the scrolling is reversed it shouldn't affect anything
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize)
- }
-
- @FlakyTest(bugId = 313465577)
- @Test
- fun column_scrollForwardHalfWay() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0.3f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 1)
-
- val scrolled =
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
- }
-
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(scrolled)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize + scrolled)
- }
-
- @Test
- fun column_scrollForwardTillTheEnd() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items((0..3).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- // we scroll a bit more than it is possible just to make sure we would stop correctly
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 6)
-
- rule.runOnIdle {
- with(rule.density) {
- val realOffset =
- state.firstVisibleItemScrollOffset.toDp() +
- itemSize * state.firstVisibleItemIndex
- assertThat(realOffset).isEqualTo(itemSize * 2)
- }
- }
-
- rule.onNodeWithTag("3").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun row_emitTwoElementsAsOneItem_positionedReversed() {
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(reverseLayout = true, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item {
- Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
- Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
- }
- }
- }
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun row_emitTwoItems_positionedReversed() {
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(reverseLayout = true, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item { Box(Modifier.requiredSize(itemSize).testTag("0").focusable()) }
- item { Box(Modifier.requiredSize(itemSize).testTag("1")) }
- }
- }
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun row_initialScrollPositionIs0() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
- }
-
- @Test
- fun row_scrollInWrongDirectionDoesNothing() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- // we scroll down and as the scrolling is reversed it shouldn't affect anything
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- @FlakyTest(bugId = 313465577)
- @Test
- fun row_scrollForwardHalfWay() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0.3f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 1)
-
- val scrolled =
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
- }
-
- rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(scrolled)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
- }
-
- @Test
- fun row_scrollForwardTillTheEnd() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items((0..3).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
-
- // we scroll a bit more than it is possible just to make sure we would stop correctly
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 6)
- rule.runOnIdle {
- with(rule.density) {
- val realOffset =
- state.firstVisibleItemScrollOffset.toDp() +
- itemSize * state.firstVisibleItemIndex
- assertThat(realOffset).isEqualTo(itemSize * 2)
- }
- }
-
- rule.onNodeWithTag("3").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
- rule.setContentWithTestViewConfiguration {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- TvLazyRow(reverseLayout = true, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item {
- Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
- Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
- }
- }
- }
- }
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun row_rtl_emitTwoItems_positionedReversed() {
- rule.setContentWithTestViewConfiguration {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- TvLazyRow(reverseLayout = true, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item { Box(Modifier.requiredSize(itemSize).testTag("0").focusable()) }
- item { Box(Modifier.requiredSize(itemSize).testTag("1").focusable()) }
- }
- }
- }
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun row_rtl_scrollForwardHalfWay() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- TvLazyRow(
- reverseLayout = true,
- state = rememberTvLazyListState().also { state = it },
- modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
- pivotOffsets = PivotOffsets(parentFraction = 0.3f)
- ) {
- items((0..2).toList()) {
- Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
- }
- }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
-
- val scrolled =
- rule.runOnIdle {
- assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
- }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(-scrolled)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
- rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
- }
-
- @Test
- fun column_whenParameterChanges() {
- var reverse by mutableStateOf(true)
- rule.setContentWithTestViewConfiguration {
- TvLazyColumn(
- reverseLayout = reverse,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- item {
- Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
- Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
- }
- }
- }
-
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle { reverse = false }
-
- rule.onNodeWithTag("0").assertTopPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertTopPositionInRootIsEqualTo(itemSize)
- }
-
- @Test
- fun row_whenParameterChanges() {
- var reverse by mutableStateOf(true)
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(reverseLayout = reverse, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- item {
- Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
- Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
- }
- }
- }
-
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(itemSize)
-
- rule.runOnIdle { reverse = false }
-
- rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(0.dp)
- rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(itemSize)
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyNestedScrollingTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyNestedScrollingTest.kt
deleted file mode 100644
index 86dd2ea..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyNestedScrollingTest.kt
+++ /dev/null
@@ -1,303 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.grid.keyPress
-import com.google.common.truth.Truth
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyNestedScrollingTest {
- private val LazyTag = "LazyTag"
-
- @get:Rule val rule = createComposeRule()
-
- private val expectedDragOffset = 20f
- private val dragOffsetWithTouchSlop = expectedDragOffset + TestTouchSlop
-
- @Test
- fun column_nestedScrollingBackwardInitially() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyColumn(
- Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Box(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = 100f + TestTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(100f) }
- }
-
- @Test
- fun column_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyColumn(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Box(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- // scroll forward
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
-
- // scroll back so we again on 0 position
- // we scroll one extra dp to prevent rounding issues
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- draggedOffset = 0f
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = dragOffsetWithTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset) }
- }
-
- @Test
- fun column_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
- val items = (1..2).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyColumn(
- Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Box(Modifier.requiredSize(40.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset) }
- }
-
- @Test
- fun column_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Vertical, state = scrollable)) {
- TvLazyColumn(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Box(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- // scroll till the end
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- draggedOffset = 0f
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset) }
- }
-
- @Test
- fun row_nestedScrollingBackwardInitially() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Horizontal, state = scrollable)) {
- TvLazyRow(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset) }
- }
-
- @Test
- fun row_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Horizontal, state = scrollable)) {
- TvLazyRow(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- // scroll forward
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
-
- // scroll back so we again on 0 position
- // we scroll one extra dp to prevent rounding issues
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 2)
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- draggedOffset = 0f
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset) }
- }
-
- @Test
- fun row_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
- val items = (1..2).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Horizontal, state = scrollable)) {
- TvLazyRow(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Spacer(Modifier.requiredSize(40.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset) }
- }
-
- @Test
- fun row_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
- val items = (1..3).toList()
- var draggedOffset = 0f
- val scrollable = ScrollableState {
- draggedOffset += it
- it
- }
- rule.setContentWithTestViewConfiguration {
- Box(Modifier.scrollable(orientation = Orientation.Horizontal, state = scrollable)) {
- TvLazyRow(
- modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(items) { Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable()) }
- }
- }
- }
-
- // scroll till the end
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
-
- rule.onNodeWithTag(LazyTag).performTouchInput {
- draggedOffset = 0f
- down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
- up()
- }
-
- rule.runOnIdle { Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset) }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt
deleted file mode 100644
index faee75b..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt
+++ /dev/null
@@ -1,147 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.grid.keyPress
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyRowTest {
- @Suppress("PrivatePropertyName") private val LazyListTag = "LazyListTag"
-
- @get:Rule val rule = createComposeRule()
-
- private val firstItemTag = "firstItemTag"
- private val secondItemTag = "secondItemTag"
-
- private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
- rule.setContentWithTestViewConfiguration {
- TvLazyRow(
- Modifier.testTag(LazyListTag).requiredHeight(100.dp),
- verticalAlignment = verticalGravity,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(listOf(1, 2)) {
- if (it == 1) {
- Box(Modifier.size(50.dp).testTag(firstItemTag).focusable())
- } else {
- Box(Modifier.size(70.dp).testTag(secondItemTag).focusable())
- }
- }
- }
- }
-
- rule.onNodeWithTag(firstItemTag).assertIsDisplayed()
-
- rule.onNodeWithTag(secondItemTag).assertIsDisplayed()
-
- val lazyRowBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
-
- with(rule.density) {
- // Verify the height of the row
- assertThat(lazyRowBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
- assertThat(lazyRowBounds.bottom.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
- }
- }
-
- @Test
- fun lazyRowAlignmentCenterVertically() {
- prepareLazyRowForAlignment(Alignment.CenterVertically)
-
- rule.onNodeWithTag(firstItemTag).assertPositionInRootIsEqualTo(0.dp, 25.dp)
-
- rule.onNodeWithTag(secondItemTag).assertPositionInRootIsEqualTo(50.dp, 15.dp)
- }
-
- @Test
- fun lazyRowAlignmentTop() {
- prepareLazyRowForAlignment(Alignment.Top)
-
- rule.onNodeWithTag(firstItemTag).assertPositionInRootIsEqualTo(0.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag).assertPositionInRootIsEqualTo(50.dp, 0.dp)
- }
-
- @Test
- fun lazyRowAlignmentBottom() {
- prepareLazyRowForAlignment(Alignment.Bottom)
-
- rule.onNodeWithTag(firstItemTag).assertPositionInRootIsEqualTo(0.dp, 50.dp)
-
- rule.onNodeWithTag(secondItemTag).assertPositionInRootIsEqualTo(50.dp, 30.dp)
- }
-
- @Test
- fun scrollsLeftInRtl() {
- lateinit var state: TvLazyListState
- rule.setContentWithTestViewConfiguration {
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(Modifier.width(100.dp)) {
- state = rememberTvLazyListState()
- TvLazyRow(
- Modifier.testTag(LazyListTag),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(4) {
- Box(
- Modifier.width(101.dp)
- .fillParentMaxHeight()
- .testTag("$it")
- .focusable()
- )
- }
- }
- }
- }
- }
-
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 2)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
- }
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
deleted file mode 100644
index 0c221a6..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
+++ /dev/null
@@ -1,361 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.animation.core.FloatSpringSpec
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.unit.Dp
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.AutoTestFrameClock
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import java.util.concurrent.TimeUnit
-import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class LazyScrollTest(private val orientation: Orientation) {
- @get:Rule val rule = createComposeRule()
-
- private val vertical: Boolean
- get() = orientation == Orientation.Vertical
-
- private val itemsCount = 20
- private lateinit var state: TvLazyListState
-
- private val itemSizePx = 100
- private var itemSizeDp = Dp.Unspecified
- private var containerSizeDp = Dp.Unspecified
-
- lateinit var scope: CoroutineScope
-
- @Before
- fun setup() {
- with(rule.density) {
- itemSizeDp = itemSizePx.toDp()
- containerSizeDp = itemSizeDp * 3
- }
- rule.setContent {
- state = rememberTvLazyListState()
- scope = rememberCoroutineScope()
- TestContent()
- }
- }
-
- @Test
- fun setupWorks() {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun scrollToItem() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(3) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun scrollToItemWithOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(3, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
-
- @Test
- fun scrollToItemWithNegativeOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(3, -10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- val item3Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 3 }.offset
- assertThat(item3Offset).isEqualTo(10)
- }
-
- @Test
- fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(itemsCount - 3, 10)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
- }
-
- @Test
- fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(1, -(itemSizePx + 10))
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
- }
-
- @Test
- fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(itemsCount + 2) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
- }
-
- @Test
- fun animateScrollBy() = runBlocking {
- val scrollDistance = 320
-
- val expectedIndex = scrollDistance / itemSizePx // resolves to 3
- val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
-
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollBy(scrollDistance.toFloat())
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(expectedIndex)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
- }
-
- @Test
- fun animateScrollToItem() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.animateScrollToItem(5, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(5)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
-
- @Test
- fun animateScrollToItemWithOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.animateScrollToItem(3, 10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
-
- @Test
- fun animateScrollToItemWithNegativeOffset() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.animateScrollToItem(3, -10) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- val item3Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 3 }.offset
- assertThat(item3Offset).isEqualTo(10)
- }
-
- @Test
- fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(itemsCount - 3, 10)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
- }
-
- @Test
- fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(1, -(itemSizePx + 10))
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
- }
-
- @Test
- fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(itemsCount + 2)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
- }
-
- @Test
- fun animatePerFrameForwardToVisibleItem() {
- assertSpringAnimation(toIndex = 2)
- }
-
- @Test
- fun animatePerFrameForwardToVisibleItemWithOffset() {
- assertSpringAnimation(toIndex = 2, toOffset = 35)
- }
-
- @Test
- fun animatePerFrameForwardToNotVisibleItem() {
- assertSpringAnimation(toIndex = 8)
- }
-
- @Test
- fun animatePerFrameForwardToNotVisibleItemWithOffset() {
- assertSpringAnimation(toIndex = 10, toOffset = 35)
- }
-
- @Test
- fun animatePerFrameBackward() {
- assertSpringAnimation(toIndex = 1, fromIndex = 6)
- }
-
- @Test
- fun animatePerFrameBackwardWithOffset() {
- assertSpringAnimation(toIndex = 1, fromIndex = 5, fromOffset = 58)
- }
-
- @Test
- fun animatePerFrameBackwardWithInitialOffset() {
- assertSpringAnimation(toIndex = 0, toOffset = 20, fromIndex = 8)
- }
-
- @Test
- fun animateScrollToItemWithOffsetLargerThanItemSize_forward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.animateScrollToItem(10, -itemSizePx * 3)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(7)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) {
- state.scrollToItem(10)
- state.animateScrollToItem(0, itemSizePx * 3)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- @Test
- fun canScrollForward() = runBlocking {
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- assertThat(state.canScrollForward).isTrue()
- assertThat(state.canScrollBackward).isFalse()
- }
-
- @Test
- fun canScrollBackward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(itemsCount) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
- assertThat(state.canScrollForward).isFalse()
- assertThat(state.canScrollBackward).isTrue()
- }
-
- @Test
- fun canScrollForwardAndBackward() = runBlocking {
- withContext(Dispatchers.Main + AutoTestFrameClock()) { state.scrollToItem(1) }
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- assertThat(state.canScrollForward).isTrue()
- assertThat(state.canScrollBackward).isTrue()
- }
-
- private fun assertSpringAnimation(
- toIndex: Int,
- toOffset: Int = 0,
- fromIndex: Int = 0,
- fromOffset: Int = 0
- ) {
- if (fromIndex != 0 || fromOffset != 0) {
- rule.runOnIdle { runBlocking { state.scrollToItem(fromIndex, fromOffset) } }
- }
- rule.waitForIdle()
-
- assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
-
- rule.mainClock.autoAdvance = false
-
- scope.launch { state.animateScrollToItem(toIndex, toOffset) }
-
- while (!state.isScrollInProgress) {
- Thread.sleep(5)
- }
-
- val startOffset = (fromIndex * itemSizePx + fromOffset).toFloat()
- val endOffset = (toIndex * itemSizePx + toOffset).toFloat()
- val spec = FloatSpringSpec()
-
- val duration =
- TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
- rule.mainClock.advanceTimeByFrame()
- var expectedTime = rule.mainClock.currentTime
- for (i in 0..duration step FrameDuration) {
- val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
- val expectedValue = spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
- val actualValue =
- (state.firstVisibleItemIndex * itemSizePx + state.firstVisibleItemScrollOffset)
- assertWithMessage(
- "On animation frame at $i index=${state.firstVisibleItemIndex} " +
- "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
- )
- .that(actualValue)
- .isEqualTo(expectedValue.roundToInt(), tolerance = 1)
-
- rule.mainClock.advanceTimeBy(FrameDuration)
- expectedTime += FrameDuration
- assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
- rule.waitForIdle()
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
- }
-
- @Composable
- private fun TestContent() {
- if (vertical) {
- TvLazyColumn(
- Modifier.height(containerSizeDp),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(itemsCount) { ItemContent() }
- }
- } else {
- TvLazyRow(
- Modifier.width(containerSizeDp),
- state,
- pivotOffsets = PivotOffsets(parentFraction = 0f)
- ) {
- items(itemsCount) { ItemContent() }
- }
- }
- }
-
- @Composable
- private fun ItemContent() {
- val modifier =
- if (vertical) {
- Modifier.height(itemSizeDp)
- } else {
- Modifier.width(itemSizeDp)
- }
- Spacer(modifier)
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
- }
-}
-
-private val FrameDuration = 16L
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazySemanticsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazySemanticsTest.kt
deleted file mode 100644
index da3a4ec..0000000
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazySemanticsTest.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
-import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
-import androidx.compose.ui.semantics.getOrNull
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.tv.foundation.PivotOffsets
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Tests the semantics properties defined on a LazyList:
- * - GetIndexForKey
- * - ScrollToIndex
- *
- * GetIndexForKey: Create a lazy list, iterate over all indices, verify key of each of them
- *
- * ScrollToIndex: Create a lazy list, scroll to an item off screen, verify shown items
- *
- * All tests performed in [runTest], scenarios set up in the test methods.
- */
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazySemanticsTest {
- private val N = 20
- private val LazyListTag = "lazy_list"
- private val LazyListModifier = Modifier.testTag(LazyListTag).requiredSize(100.dp)
-
- private fun tag(index: Int): String = "tag_$index"
-
- private fun key(index: Int): String = "key_$index"
-
- @get:Rule val rule = createComposeRule()
-
- @Test
- fun itemSemantics_column() {
- rule.setContent {
- TvLazyColumn(LazyListModifier, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- repeat(N) { item(key = key(it)) { SpacerInColumn(it) } }
- }
- }
- runTest()
- }
-
- @Test
- fun itemsSemantics_column() {
- rule.setContent {
- TvLazyColumn(LazyListModifier, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- items(items = List(N) { it }, key = { key(it) }) { SpacerInColumn(it) }
- }
- }
- runTest()
- }
-
- @Test
- fun itemSemantics_row() {
- rule.setContent {
- TvLazyRow(LazyListModifier, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- repeat(N) { item(key = key(it)) { SpacerInRow(it) } }
- }
- }
- runTest()
- }
-
- @Test
- fun itemsSemantics_row() {
- rule.setContent {
- TvLazyRow(LazyListModifier, pivotOffsets = PivotOffsets(parentFraction = 0f)) {
- items(items = List(N) { it }, key = { key(it) }) { SpacerInRow(it) }
- }
- }
- runTest()
- }
-
- private fun runTest() {
- checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)
-
- // Verify IndexForKey
- rule
- .onNodeWithTag(LazyListTag)
- .assert(
- SemanticsMatcher.keyIsDefined(IndexForKey)
- .and(
- SemanticsMatcher("keys match") { node ->
- val actualIndex = node.config.getOrNull(IndexForKey)!!
- (0 until N).all { expectedIndex ->
- expectedIndex == actualIndex.invoke(key(expectedIndex))
- }
- }
- )
- )
-
- // Verify ScrollToIndex
- rule.onNodeWithTag(LazyListTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))
-
- invokeScrollToIndex(targetIndex = 10)
- checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)
-
- invokeScrollToIndex(targetIndex = N - 1)
- checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
- }
-
- private fun invokeScrollToIndex(targetIndex: Int) {
- val node =
- rule.onNodeWithTag(LazyListTag).fetchSemanticsNode("Failed: invoke ScrollToIndex")
- rule.runOnUiThread { node.config[ScrollToIndex].action!!.invoke(targetIndex) }
- }
-
- private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
- if (firstExpectedItem > 0) {
- rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
- }
- (firstExpectedItem..lastExpectedItem).forEach { rule.onNodeWithTag(tag(it)).assertExists() }
- if (firstExpectedItem < N - 1) {
- rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
- }
- }
-
- @Composable
- private fun SpacerInColumn(index: Int) {
- Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
- }
-
- @Composable
- private fun SpacerInRow(index: Int) {
- Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
- }
-}
diff --git a/tv/tv-foundation/src/main/baseline-prof.txt b/tv/tv-foundation/src/main/baseline-prof.txt
index 1fde2ad..aa12116 100644
--- a/tv/tv-foundation/src/main/baseline-prof.txt
+++ b/tv/tv-foundation/src/main/baseline-prof.txt
@@ -1,37 +1,2 @@
# Baseline profile rules for androidx.tv.tv-foundation
# =====================================================
-
-HSPLandroidx/tv/foundation/PivotOffsets;->**(**)**
-HSPLandroidx/tv/foundation/TvBringIntoViewSpec;->**(**)**
-Landroidx/tv/foundation/lazy/grid/LazyGridIntervalContent;
-HSPLandroidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator;->**(**)**
-Landroidx/tv/foundation/lazy/grid/LazyGridItemProviderImpl;
-HSPLandroidx/tv/foundation/lazy/grid/LazyGridKt**->**(**)**
-HSPLandroidx/tv/foundation/lazy/grid/LazyGridScrollPosition;->**(**)**
-Landroidx/tv/foundation/lazy/grid/TvGridItemSpan;
-Landroidx/tv/foundation/lazy/grid/TvLazyGridItemSpanScope;
-Landroidx/tv/foundation/lazy/grid/TvLazyGridScope;
-HSPLandroidx/tv/foundation/lazy/grid/TvLazyGridState;->**(**)**
-HSPLandroidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier;->**(**)**
-HPLandroidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo;->**(**)**
-Landroidx/tv/foundation/lazy/layout/LazyLayoutKeyIndexMap;
-HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState;->**(**)**
-Landroidx/tv/foundation/lazy/layout/LazyLayoutSemanticState;
-HSPLandroidx/tv/foundation/lazy/layout/LazyLayoutSemanticsKt**->**(**)**
-HSPLandroidx/tv/foundation/lazy/layout/NearestRangeKeyIndexMap;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/EmptyLazyListLayoutInfo;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsModifierLocal;->**(**)**
-Landroidx/tv/foundation/lazy/list/LazyLayoutBeyondBoundsState;
-HSPLandroidx/tv/foundation/lazy/list/LazyListBeyondBoundsState;->**(**)**
-Landroidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator;
-HSPLandroidx/tv/foundation/lazy/list/LazyListItemProviderImpl;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/LazyListKt**->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/LazyListMeasureResult;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/LazyListMeasuredItem;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/LazyListStateKt**->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/TvLazyListInterval;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/TvLazyListIntervalContent;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl;->**(**)**
-Landroidx/tv/foundation/lazy/list/TvLazyListLayoutInfo;
-HSPLandroidx/tv/foundation/lazy/list/TvLazyListScope;->**(**)**
-HSPLandroidx/tv/foundation/lazy/list/TvLazyListState;->**(**)**
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
deleted file mode 100644
index 1ec72fe..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
+++ /dev/null
@@ -1,59 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation
-
-import androidx.annotation.FloatRange
-import androidx.compose.runtime.Immutable
-
-/**
- * Holds the offsets needed for scrolling-with-offset.
- *
- * @property parentFraction defines the offset of the starting edge of the child element from the
- * starting edge of the parent element. This value should be between 0 and 1.
- * @property childFraction defines the offset of the starting edge of the child from the pivot
- * defined by parentFraction. This value should be between 0 and 1.
- */
-@Deprecated(
- "BringIntoViewSpec should be used to control the position of the " +
- "focused item while scrolling."
-)
-@Immutable
-class PivotOffsets
-constructor(
- @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
- val parentFraction: Float = 0.3f,
- @FloatRange(from = 0.0, to = 1.0, fromInclusive = true, toInclusive = true)
- val childFraction: Float = 0f,
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is PivotOffsets) return false
-
- if (parentFraction != other.parentFraction) return false
- if (childFraction != other.childFraction) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = parentFraction.hashCode()
- result = 31 * result + childFraction.hashCode()
- return result
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
deleted file mode 100644
index 66cbd7b..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
+++ /dev/null
@@ -1,132 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.BringIntoViewSpec
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.platform.inspectable
-import kotlin.math.abs
-
-/* Copied from
-compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/
-Scrollable.kt and modified */
-
-/**
- * Configure touch scrolling and flinging for the UI element in a single [Orientation].
- *
- * Users should update their state themselves using default [ScrollableState] and its
- * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect
- * their own state in UI when using this component.
- *
- * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be
- * interpreted by the user land logic and contains useful information about on-going events.
- * @param orientation orientation of the scrolling
- * @param pivotOffsets offsets of child element within the parent and starting edge of the child
- * from the pivot defined by the parentOffset.
- * @param enabled whether or not scrolling in enabled
- * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave
- * like bottom to top and left to right will behave like right to left. drag events when this
- * scrollable is being dragged.
- */
-@Deprecated(
- "scrollableWithPivot has been deprecated. Construct a custom bringIntoViewSpec to scroll with an offset. To learn how you can control offset during scrolling, refer PivotBringIntoViewSpec: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt;l=48;drc=dcaa116fbfda77e64a319e1668056ce3b032469f",
- replaceWith =
- ReplaceWith(
- "scrollable(" +
- "state = state, " +
- "orientation = orientation, " +
- "enabled = enabled, " +
- "reverseDirection = reverseDirection" +
- ")"
- )
-)
-@OptIn(ExperimentalFoundationApi::class)
-@ExperimentalTvFoundationApi
-@Suppress("DEPRECATION")
-fun Modifier.scrollableWithPivot(
- state: ScrollableState,
- orientation: Orientation,
- pivotOffsets: PivotOffsets,
- enabled: Boolean = true,
- reverseDirection: Boolean = false
-): Modifier =
- this then
- Modifier.inspectable(
- debugInspectorInfo {
- name = "scrollableWithPivot"
- properties["orientation"] = orientation
- properties["state"] = state
- properties["enabled"] = enabled
- properties["reverseDirection"] = reverseDirection
- properties["pivotOffsets"] = pivotOffsets
- }
- ) {
- Modifier.scrollable(
- state = state,
- orientation = orientation,
- enabled = enabled,
- reverseDirection = reverseDirection,
- overscrollEffect = null,
- bringIntoViewSpec = TvBringIntoViewSpec(pivotOffsets, enabled)
- )
- }
-
-@OptIn(ExperimentalFoundationApi::class)
-private class TvBringIntoViewSpec(val pivotOffsets: PivotOffsets, val userScrollEnabled: Boolean) :
- BringIntoViewSpec {
-
- override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float {
- if (!userScrollEnabled) return 0f
- val leadingEdgeOfItemRequestingFocus = offset
- val trailingEdgeOfItemRequestingFocus = offset + size
-
- val sizeOfItemRequestingFocus =
- abs(trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus)
- val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
- val initialTargetForLeadingEdge =
- pivotOffsets.parentFraction * containerSize -
- (pivotOffsets.childFraction * sizeOfItemRequestingFocus)
- val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
-
- val targetForLeadingEdge =
- if (childSmallerThanParent && spaceAvailableToShowItem < sizeOfItemRequestingFocus) {
- containerSize - sizeOfItemRequestingFocus
- } else {
- initialTargetForLeadingEdge
- }
-
- return leadingEdgeOfItemRequestingFocus - targetForLeadingEdge
- }
-
- override fun hashCode(): Int {
- var result = pivotOffsets.hashCode()
- result = 31 * result + userScrollEnabled.hashCode()
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (other !is TvBringIntoViewSpec) return false
- return pivotOffsets == other.pivotOffsets && userScrollEnabled == other.userScrollEnabled
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
deleted file mode 100644
index bdd3e55..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
+++ /dev/null
@@ -1,435 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.checkScrollableContainerConstraints
-import androidx.compose.foundation.clipScrollableContainer
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.foundation.lazy.layout.LazyLayout
-import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.offset
-import androidx.compose.ui.util.fastForEach
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.layout.lazyLayoutSemantics
-import androidx.tv.foundation.lazy.list.LazyLayoutBeyondBoundsModifierLocal
-import androidx.tv.foundation.lazy.list.LazyLayoutBeyondBoundsState
-import androidx.tv.foundation.lazy.list.calculateLazyLayoutPinnedIndices
-import androidx.tv.foundation.scrollableWithPivot
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTvFoundationApi::class)
-@Composable
-internal fun LazyGrid(
- /** Modifier to be applied for the inner layout */
- modifier: Modifier = Modifier,
- /** State controlling the scroll position */
- state: TvLazyGridState,
- /** Prefix sums of cross axis sizes of slots per line, e.g. the columns for vertical grid. */
- slots: Density.(Constraints) -> LazyGridSlots,
- /** The inner padding to be added for the whole content (not for each individual item) */
- contentPadding: PaddingValues = PaddingValues(0.dp),
- /** reverse the direction of scrolling and layout */
- reverseLayout: Boolean = false,
- /** The layout orientation of the grid */
- isVertical: Boolean,
- /** Whether scrolling via the user gestures is allowed. */
- userScrollEnabled: Boolean,
- /** The vertical arrangement for items/lines. */
- verticalArrangement: Arrangement.Vertical,
- /** The horizontal arrangement for items/lines. */
- horizontalArrangement: Arrangement.Horizontal,
- /**
- * offsets of child element within the parent and starting edge of the child from the pivot
- * defined by the parentOffset
- */
- pivotOffsets: PivotOffsets,
- /** The content of the grid */
- content: TvLazyGridScope.() -> Unit
-) {
- val itemProviderLambda = rememberLazyGridItemProviderLambda(state, content)
-
- val semanticState = rememberLazyGridSemanticState(state, reverseLayout)
-
- val measurePolicy =
- rememberLazyGridMeasurePolicy(
- itemProviderLambda,
- state,
- slots,
- contentPadding,
- reverseLayout,
- isVertical,
- horizontalArrangement,
- verticalArrangement,
- )
-
- state.isVertical = isVertical
-
- ScrollPositionUpdater(itemProviderLambda, state)
-
- val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
- LazyLayout(
- modifier =
- modifier
- .then(state.remeasurementModifier)
- .then(state.awaitLayoutModifier)
- .lazyLayoutSemantics(
- itemProviderLambda = itemProviderLambda,
- state = semanticState,
- orientation = orientation,
- userScrollEnabled = userScrollEnabled,
- reverseScrolling = reverseLayout
- )
- .clipScrollableContainer(orientation)
- .lazyGridBeyondBoundsModifier(state, reverseLayout, orientation)
- .scrollableWithPivot(
- orientation = orientation,
- reverseDirection =
- ScrollableDefaults.reverseDirection(
- LocalLayoutDirection.current,
- orientation,
- reverseLayout
- ),
- state = state,
- enabled = userScrollEnabled,
- pivotOffsets = pivotOffsets
- ),
- prefetchState = state.prefetchState,
- measurePolicy = measurePolicy,
- itemProvider = itemProviderLambda
- )
-}
-
-/** Extracted to minimize the recomposition scope */
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun ScrollPositionUpdater(
- itemProviderLambda: () -> LazyGridItemProvider,
- state: TvLazyGridState
-) {
- val itemProvider = itemProviderLambda()
- if (itemProvider.itemCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
- }
-}
-
-/** lazy grid slots configuration */
-internal class LazyGridSlots(val sizes: IntArray, val positions: IntArray)
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun rememberLazyGridMeasurePolicy(
- /** Items provider of the list. */
- itemProviderLambda: () -> LazyGridItemProvider,
- /** The state of the list. */
- state: TvLazyGridState,
- /** Prefix sums of cross axis sizes of slots of the grid. */
- slots: Density.(Constraints) -> LazyGridSlots,
- /** The inner padding to be added for the whole content(nor for each individual item) */
- contentPadding: PaddingValues,
- /** reverse the direction of scrolling and layout */
- reverseLayout: Boolean,
- /** The layout orientation of the list */
- isVertical: Boolean,
- /** The horizontal arrangement for items. Required when isVertical is false */
- horizontalArrangement: Arrangement.Horizontal? = null,
- /** The vertical arrangement for items. Required when isVertical is true */
- verticalArrangement: Arrangement.Vertical? = null,
-) =
- remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
- state,
- slots,
- contentPadding,
- reverseLayout,
- isVertical,
- horizontalArrangement,
- verticalArrangement
- ) {
- { containerConstraints ->
- checkScrollableContainerConstraints(
- containerConstraints,
- if (isVertical) Orientation.Vertical else Orientation.Horizontal
- )
-
- // resolve content paddings
- val startPadding =
- if (isVertical) {
- contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
- } else {
- // in horizontal configuration, padding is reversed by placeRelative
- contentPadding.calculateStartPadding(layoutDirection).roundToPx()
- }
-
- val endPadding =
- if (isVertical) {
- contentPadding.calculateRightPadding(layoutDirection).roundToPx()
- } else {
- // in horizontal configuration, padding is reversed by placeRelative
- contentPadding.calculateEndPadding(layoutDirection).roundToPx()
- }
- val topPadding = contentPadding.calculateTopPadding().roundToPx()
- val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
- val totalVerticalPadding = topPadding + bottomPadding
- val totalHorizontalPadding = startPadding + endPadding
- val totalMainAxisPadding =
- if (isVertical) totalVerticalPadding else totalHorizontalPadding
- val beforeContentPadding =
- when {
- isVertical && !reverseLayout -> topPadding
- isVertical && reverseLayout -> bottomPadding
- !isVertical && !reverseLayout -> startPadding
- else -> endPadding // !isVertical && reverseLayout
- }
- val afterContentPadding = totalMainAxisPadding - beforeContentPadding
- val contentConstraints =
- containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
-
- val itemProvider = itemProviderLambda()
-
- val spanLayoutProvider = itemProvider.spanLayoutProvider
- val resolvedSlots = slots(containerConstraints)
- val slotsPerLine = resolvedSlots.sizes.size
- spanLayoutProvider.slotsPerLine = slotsPerLine
-
- // Update the state's cached Density and slotsPerLine
- state.density = this
- state.slotsPerLine = slotsPerLine
-
- val spaceBetweenLinesDp =
- if (isVertical) {
- requireNotNull(verticalArrangement) {
- "null verticalArrangement when isVertical == true"
- }
- .spacing
- } else {
- requireNotNull(horizontalArrangement) {
- "null horizontalArrangement when isVertical == false"
- }
- .spacing
- }
- val spaceBetweenLines = spaceBetweenLinesDp.roundToPx()
- val itemsCount = itemProvider.itemCount
-
- // can be negative if the content padding is larger than the max size from constraints
- val mainAxisAvailableSize =
- if (isVertical) {
- containerConstraints.maxHeight - totalVerticalPadding
- } else {
- containerConstraints.maxWidth - totalHorizontalPadding
- }
- val visualItemOffset =
- if (!reverseLayout || mainAxisAvailableSize > 0) {
- IntOffset(startPadding, topPadding)
- } else {
- // When layout is reversed and paddings together take >100% of the available
- // space,
- // layout size is coerced to 0 when positioning. To take that space into
- // account,
- // we offset start padding by negative space between paddings.
- IntOffset(
- if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
- if (isVertical) topPadding + mainAxisAvailableSize else topPadding
- )
- }
-
- val measuredItemProvider =
- object : LazyGridMeasuredItemProvider(itemProvider, this, spaceBetweenLines) {
- override fun createItem(
- index: Int,
- key: Any,
- contentType: Any?,
- crossAxisSize: Int,
- mainAxisSpacing: Int,
- placeables: List<Placeable>
- ) =
- LazyGridMeasuredItem(
- index = index,
- key = key,
- isVertical = isVertical,
- crossAxisSize = crossAxisSize,
- mainAxisSpacing = mainAxisSpacing,
- reverseLayout = reverseLayout,
- layoutDirection = layoutDirection,
- beforeContentPadding = beforeContentPadding,
- afterContentPadding = afterContentPadding,
- visualOffset = visualItemOffset,
- placeables = placeables,
- contentType = contentType
- )
- }
- val measuredLineProvider =
- object :
- LazyGridMeasuredLineProvider(
- isVertical = isVertical,
- slots = resolvedSlots,
- gridItemsCount = itemsCount,
- spaceBetweenLines = spaceBetweenLines,
- measuredItemProvider = measuredItemProvider,
- spanLayoutProvider = spanLayoutProvider
- ) {
- override fun createLine(
- index: Int,
- items: Array<LazyGridMeasuredItem>,
- spans: List<TvGridItemSpan>,
- mainAxisSpacing: Int
- ) =
- LazyGridMeasuredLine(
- index = index,
- items = items,
- spans = spans,
- slots = resolvedSlots,
- isVertical = isVertical,
- mainAxisSpacing = mainAxisSpacing,
- )
- }
- state.prefetchInfoRetriever = { line ->
- val lineConfiguration = spanLayoutProvider.getLineConfiguration(line)
- var index = lineConfiguration.firstItemIndex
- var slot = 0
- val result = ArrayList<Pair<Int, Constraints>>(lineConfiguration.spans.size)
- lineConfiguration.spans.fastForEach {
- val span = it.currentLineSpan
- result.add(index to measuredLineProvider.childConstraints(slot, span))
- ++index
- slot += span
- }
- result
- }
-
- val firstVisibleLineIndex: Int
- val firstVisibleLineScrollOffset: Int
- Snapshot.withoutReadObservation {
- val index =
- state.updateScrollPositionIfTheFirstItemWasMoved(
- itemProvider,
- state.firstVisibleItemIndex
- )
- if (index < itemsCount || itemsCount <= 0) {
- firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(index)
- firstVisibleLineScrollOffset = state.firstVisibleItemScrollOffset
- } else {
- // the data set has been updated and now we have less items that we were
- // scrolled to before
- firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(itemsCount - 1)
- firstVisibleLineScrollOffset = 0
- }
- }
-
- val pinnedItems =
- itemProvider.calculateLazyLayoutPinnedIndices(
- state.pinnedItems,
- state.beyondBoundsInfo
- )
-
- measureLazyGrid(
- itemsCount = itemsCount,
- measuredLineProvider = measuredLineProvider,
- measuredItemProvider = measuredItemProvider,
- mainAxisAvailableSize = mainAxisAvailableSize,
- beforeContentPadding = beforeContentPadding,
- afterContentPadding = afterContentPadding,
- spaceBetweenLines = spaceBetweenLines,
- firstVisibleLineIndex = firstVisibleLineIndex,
- firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
- scrollToBeConsumed = state.scrollToBeConsumed,
- constraints = contentConstraints,
- isVertical = isVertical,
- verticalArrangement = verticalArrangement,
- horizontalArrangement = horizontalArrangement,
- reverseLayout = reverseLayout,
- density = this,
- placementAnimator = state.placementAnimator,
- spanLayoutProvider = spanLayoutProvider,
- pinnedItems = pinnedItems,
- layout = { width, height, placement ->
- layout(
- containerConstraints.constrainWidth(width + totalHorizontalPadding),
- containerConstraints.constrainHeight(height + totalVerticalPadding),
- emptyMap(),
- placement
- )
- }
- )
- .also { state.applyMeasureResult(it) }
- }
- }
-
-/**
- * This modifier is used to measure and place additional items when the lazyList receives a request
- * to layout items beyond the visible bounds.
- */
-@Suppress("ComposableModifierFactory")
-@Composable
-internal fun Modifier.lazyGridBeyondBoundsModifier(
- state: TvLazyGridState,
- reverseLayout: Boolean,
- orientation: Orientation
-): Modifier {
- val layoutDirection = LocalLayoutDirection.current
- val beyondBoundsState = remember(state) { LazyGridBeyondBoundsState(state) }
- return this then
- remember(state, beyondBoundsState, reverseLayout, layoutDirection, orientation) {
- LazyLayoutBeyondBoundsModifierLocal(
- beyondBoundsState,
- state.beyondBoundsInfo,
- reverseLayout,
- layoutDirection,
- orientation
- )
- }
-}
-
-internal class LazyGridBeyondBoundsState(
- val state: TvLazyGridState,
-) : LazyLayoutBeyondBoundsState {
-
- override fun remeasure() {
- state.remeasurement?.forceRemeasure()
- }
-
- override val itemCount: Int
- get() = state.layoutInfo.totalItemsCount
-
- override val hasVisibleItems: Boolean
- get() = state.layoutInfo.visibleItemsInfo.isNotEmpty()
-
- override val firstPlacedIndex: Int
- get() = state.firstVisibleItemIndex
-
- override val lastPlacedIndex: Int
- get() = state.layoutInfo.visibleItemsInfo.last().index
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
deleted file mode 100644
index e0ca00d..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ /dev/null
@@ -1,126 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
-import kotlin.math.abs
-import kotlin.math.max
-
-internal class LazyGridAnimateScrollScope(private val state: TvLazyGridState) :
- LazyAnimateScrollScope {
- override val density: Density
- get() = state.density
-
- override val firstVisibleItemIndex: Int
- get() = state.firstVisibleItemIndex
-
- override val firstVisibleItemScrollOffset: Int
- get() = state.firstVisibleItemScrollOffset
-
- override val lastVisibleItemIndex: Int
- get() = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
-
- override val itemCount: Int
- get() = state.layoutInfo.totalItemsCount
-
- override fun getTargetItemOffset(index: Int): Int? =
- state.layoutInfo.visibleItemsInfo
- .fastFirstOrNull { it.index == index }
- ?.let { item ->
- if (state.isVertical) {
- item.offset.y
- } else {
- item.offset.x
- }
- }
-
- override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
- state.snapToItemIndexInternal(index, scrollOffset)
- }
-
- override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
- val slotsPerLine = state.slotsPerLine
- val averageLineMainAxisSize =
- calculateLineAverageMainAxisSize(state.layoutInfo, state.isVertical)
- val before = index < firstVisibleItemIndex
- val linesDiff =
- (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
- slotsPerLine
-
- var coercedOffset = minOf(abs(targetScrollOffset), averageLineMainAxisSize)
- if (targetScrollOffset < 0) coercedOffset *= -1
- return (averageLineMainAxisSize * linesDiff).toFloat() + coercedOffset -
- firstVisibleItemScrollOffset
- }
-
- override val numOfItemsForTeleport: Int
- get() = 100 * state.slotsPerLine
-
- private fun calculateLineAverageMainAxisSize(
- layoutInfo: TvLazyGridLayoutInfo,
- isVertical: Boolean
- ): Int {
- val visibleItems = layoutInfo.visibleItemsInfo
- val lineOf: (Int) -> Int = {
- if (isVertical) visibleItems[it].row else visibleItems[it].column
- }
-
- var totalLinesMainAxisSize = 0
- var linesCount = 0
-
- var lineStartIndex = 0
- while (lineStartIndex < visibleItems.size) {
- val currentLine = lineOf(lineStartIndex)
- if (currentLine == -1) {
- // Filter out exiting items.
- ++lineStartIndex
- continue
- }
-
- var lineMainAxisSize = 0
- var lineEndIndex = lineStartIndex
- while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
- lineMainAxisSize =
- max(
- lineMainAxisSize,
- if (isVertical) {
- visibleItems[lineEndIndex].size.height
- } else {
- visibleItems[lineEndIndex].size.width
- }
- )
- ++lineEndIndex
- }
-
- totalLinesMainAxisSize += lineMainAxisSize
- ++linesCount
-
- lineStartIndex = lineEndIndex
- }
-
- return totalLinesMainAxisSize / linesCount + layoutInfo.mainAxisItemSpacing
- }
-
- override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
- state.scroll(block = block)
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt
deleted file mode 100644
index 71125e5..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt
+++ /dev/null
@@ -1,637 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.PivotOffsets
-
-/**
- * A lazy vertical grid layout. It composes only visible rows of the grid.
- *
- * @param columns describes the count and the size of the grid's columns, see [TvGridCells] doc for
- * more information
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
- * @param contentPadding specify a padding around the whole content
- * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items will be
- * laid out in the reverse order and [TvLazyGridState.firstVisibleItemIndex] == 0 means that grid
- * is scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
- * [verticalArrangement], e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321###
- * (bottom).
- * @param verticalArrangement The vertical arrangement of the layout's children
- * @param horizontalArrangement The horizontal arrangement of the layout's children
- * @param pivotOffsets offsets that are used when implementing Scrolling with Offset
- * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
- * allowed. You can still scroll programmatically using the state even when it is disabled.
- * @param pivotOffsets offsets of child element within the parent and starting edge of the child
- * from the pivot defined by the parentOffset.
- * @param content the [TvLazyGridScope] which describes the content
- */
-@Composable
-@Deprecated(
- "LazyVerticalGrid will, by default, set the position of focused item while " +
- "scrolling on a Tv. BringIntoViewSpec should be used to control the position.",
- replaceWith =
- ReplaceWith(
- "LazyVerticalGrid(" +
- "modifier = modifier, " +
- "contentPadding = contentPadding, " +
- "reverseLayout = reverseLayout, " +
- "verticalArrangement = verticalArrangement, " +
- "horizontalArrangement = horizontalArrangement, " +
- "userScrollEnabled = userScrollEnabled" +
- ") { content() }",
- imports = ["androidx.compose.foundation.lazy.grid.LazyVerticalGrid"],
- )
-)
-fun TvLazyVerticalGrid(
- columns: TvGridCells,
- modifier: Modifier = Modifier,
- state: TvLazyGridState = rememberTvLazyGridState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- verticalArrangement: Arrangement.Vertical =
- if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
- horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
- userScrollEnabled: Boolean = true,
- pivotOffsets: PivotOffsets = PivotOffsets(),
- content: TvLazyGridScope.() -> Unit
-) {
- LazyGrid(
- slots = rememberColumnWidthSums(columns, horizontalArrangement, contentPadding),
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- isVertical = true,
- horizontalArrangement = horizontalArrangement,
- verticalArrangement = verticalArrangement,
- userScrollEnabled = userScrollEnabled,
- content = content,
- pivotOffsets = pivotOffsets
- )
-}
-
-/**
- * A lazy horizontal grid layout. It composes only visible columns of the grid.
- *
- * @param rows a class describing how cells form rows, see [TvGridCells] doc for more information
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
- * @param contentPadding specify a padding around the whole content
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the end to the start and [TvLazyGridState.firstVisibleItemIndex] == 0 will mean
- * the first item is located at the end.
- * @param verticalArrangement The vertical arrangement of the layout's children
- * @param horizontalArrangement The horizontal arrangement of the layout's children
- * @param pivotOffsets offsets that are used when implementing Scrolling with Offset
- * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
- * allowed. You can still scroll programmatically using the state even when it is disabled.
- * @param pivotOffsets offsets of child element within the parent and starting edge of the child
- * from the pivot defined by the parentOffset.
- * @param content the [TvLazyGridScope] which describes the content
- */
-@Deprecated(
- "LazyHorizontalGrid will, by default, set the position of focused item while " +
- "scrolling on a Tv. BringIntoViewSpec should be used to control the position.",
- replaceWith =
- ReplaceWith(
- "LazyHorizontalGrid(" +
- "modifier = modifier, " +
- "contentPadding = contentPadding, " +
- "reverseLayout = reverseLayout, " +
- "horizontalArrangement = horizontalArrangement, " +
- "verticalArrangement = verticalArrangement, " +
- "userScrollEnabled = userScrollEnabled" +
- ") { content() }",
- imports = ["androidx.compose.foundation.lazy.grid.LazyHorizontalGrid"],
- )
-)
-@Composable
-fun TvLazyHorizontalGrid(
- rows: TvGridCells,
- modifier: Modifier = Modifier,
- state: TvLazyGridState = rememberTvLazyGridState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- horizontalArrangement: Arrangement.Horizontal =
- if (!reverseLayout) Arrangement.Start else Arrangement.End,
- verticalArrangement: Arrangement.Vertical = Arrangement.Top,
- userScrollEnabled: Boolean = true,
- pivotOffsets: PivotOffsets = PivotOffsets(),
- content: TvLazyGridScope.() -> Unit
-) {
- LazyGrid(
- slots = rememberRowHeightSums(rows, verticalArrangement, contentPadding),
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- reverseLayout = reverseLayout,
- isVertical = false,
- horizontalArrangement = horizontalArrangement,
- verticalArrangement = verticalArrangement,
- userScrollEnabled = userScrollEnabled,
- pivotOffsets = pivotOffsets,
- content = content
- )
-}
-
-/** Returns prefix sums of column widths. */
-@Composable
-private fun rememberColumnWidthSums(
- columns: TvGridCells,
- horizontalArrangement: Arrangement.Horizontal,
- contentPadding: PaddingValues
-) =
- remember<Density.(Constraints) -> LazyGridSlots>(
- columns,
- horizontalArrangement,
- contentPadding,
- ) {
- GridSlotCache { constraints ->
- require(constraints.maxWidth != Constraints.Infinity) {
- "LazyVerticalGrid's width should be bound by parent."
- }
- val horizontalPadding =
- contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
- contentPadding.calculateEndPadding(LayoutDirection.Ltr)
- val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
- with(columns) {
- calculateCrossAxisCellSizes(gridWidth, horizontalArrangement.spacing.roundToPx())
- .toIntArray()
- .let { sizes ->
- val positions = IntArray(sizes.size)
- with(horizontalArrangement) {
- arrange(gridWidth, sizes, LayoutDirection.Ltr, positions)
- }
- LazyGridSlots(sizes, positions)
- }
- }
- }
- }
-
-/** Returns prefix sums of row heights. */
-@Composable
-private fun rememberRowHeightSums(
- rows: TvGridCells,
- verticalArrangement: Arrangement.Vertical,
- contentPadding: PaddingValues
-) =
- remember<Density.(Constraints) -> LazyGridSlots>(
- rows,
- verticalArrangement,
- contentPadding,
- ) {
- GridSlotCache { constraints ->
- require(constraints.maxHeight != Constraints.Infinity) {
- "LazyHorizontalGrid's height should be bound by parent."
- }
- val verticalPadding =
- contentPadding.calculateTopPadding() + contentPadding.calculateBottomPadding()
- val gridHeight = constraints.maxHeight - verticalPadding.roundToPx()
- with(rows) {
- calculateCrossAxisCellSizes(gridHeight, verticalArrangement.spacing.roundToPx())
- .toIntArray()
- .let { sizes ->
- val positions = IntArray(sizes.size)
- with(verticalArrangement) { arrange(gridHeight, sizes, positions) }
- LazyGridSlots(sizes, positions)
- }
- }
- }
- }
-
-/** measurement cache to avoid recalculating row/column sizes on each scroll. */
-private class GridSlotCache(private val calculation: Density.(Constraints) -> LazyGridSlots) :
- (Density, Constraints) -> LazyGridSlots {
- private var cachedConstraints = Constraints()
- private var cachedDensity: Float = 0f
- private var cachedSizes: LazyGridSlots? = null
-
- override fun invoke(density: Density, constraints: Constraints): LazyGridSlots {
- with(density) {
- if (
- cachedSizes != null &&
- cachedConstraints == constraints &&
- cachedDensity == this.density
- ) {
- return cachedSizes!!
- }
-
- cachedConstraints = constraints
- cachedDensity = this.density
- return calculation(constraints).also { cachedSizes = it }
- }
- }
-}
-
-/**
- * This class describes the count and the sizes of columns in vertical grids, or rows in horizontal
- * grids.
- */
-@Deprecated(
- "Use `GridCells` instead.",
- replaceWith =
- ReplaceWith(
- "GridCells",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.GridCells")
- )
-)
-@Stable
-interface TvGridCells {
- /**
- * Calculates the number of cells and their cross axis size based on [availableSize] and
- * [spacing].
- *
- * For example, in vertical grids, [spacing] is passed from the grid's [Arrangement.Horizontal].
- * The [Arrangement.Horizontal] will also be used to arrange items in a row if the grid is wider
- * than the calculated sum of columns.
- *
- * Note that the calculated cross axis sizes will be considered in an RTL-aware manner -- if the
- * grid is vertical and the layout direction is RTL, the first width in the returned list will
- * correspond to the rightmost column.
- *
- * @param availableSize available size on cross axis, e.g. width of [TvLazyVerticalGrid].
- * @param spacing cross axis spacing, e.g. horizontal spacing for [TvLazyVerticalGrid]. The
- * spacing is passed from the corresponding [Arrangement] param of the lazy grid.
- */
- @Deprecated("Use GridCells from compose foundation.")
- fun Density.calculateCrossAxisCellSizes(availableSize: Int, spacing: Int): List<Int>
-
- /**
- * Defines a grid with fixed number of rows or columns.
- *
- * For example, for the vertical [TvLazyVerticalGrid] Fixed(3) would mean that there are 3
- * columns 1/3 of the parent width.
- */
- @Deprecated(
- "Use GridCells.Fixed from compose foundation.",
- replaceWith =
- ReplaceWith(
- "GridCells.Fixed(count)",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.GridCells")
- )
- )
- class Fixed(private val count: Int) : TvGridCells {
- init {
- require(count > 0) { "grid with no rows/columns" }
- }
-
- @Deprecated("Use GridCells from compose foundation.")
- override fun Density.calculateCrossAxisCellSizes(
- availableSize: Int,
- spacing: Int
- ): List<Int> {
- return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
- }
-
- override fun hashCode(): Int {
- return -count // Different sign from Adaptive.
- }
-
- override fun equals(other: Any?): Boolean {
- return other is Fixed && count == other.count
- }
- }
-
- /**
- * Defines a grid with as many rows or columns as possible on the condition that every cell has
- * at least [minSize] space and all extra space distributed evenly.
- *
- * For example, for the vertical [TvLazyVerticalGrid] Adaptive(20.dp) would mean that there will
- * be as many columns as possible and every column will be at least 20.dp and all the columns
- * will have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each.
- */
- @Deprecated(
- "Use GridCells from compose foundation.",
- replaceWith =
- ReplaceWith(
- "GridCells.Adaptive(minSize)",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.GridCells")
- )
- )
- class Adaptive(private val minSize: Dp) : TvGridCells {
- init {
- require(minSize > 0.dp) { "Grid requires a positive minSize" }
- }
-
- @Deprecated("Use GridCells from compose foundation.")
- override fun Density.calculateCrossAxisCellSizes(
- availableSize: Int,
- spacing: Int
- ): List<Int> {
- val count = maxOf((availableSize + spacing) / (minSize.roundToPx() + spacing), 1)
- return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
- }
-
- override fun hashCode(): Int {
- return minSize.hashCode()
- }
-
- override fun equals(other: Any?): Boolean {
- return other is Adaptive && minSize == other.minSize
- }
- }
-
- /**
- * Defines a grid with as many rows or columns as possible on the condition that every cell
- * takes exactly [size] space. The remaining space will be arranged through [TvLazyVerticalGrid]
- * arrangements on corresponding axis. If [size] is larger than container size, the cell will be
- * size to match the container.
- *
- * For example, for the vertical [TvLazyVerticalGrid] FixedSize(20.dp) would mean that there
- * will be as many columns as possible and every column will be exactly 20.dp. If the screen is
- * 88.dp wide tne there will be 4 columns 20.dp each with remaining 8.dp distributed through
- * [Arrangement.Horizontal].
- */
- @Deprecated(
- "Use GridCells from compose foundation.",
- replaceWith =
- ReplaceWith(
- "GridCells.FixedSize",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.GridCells")
- )
- )
- class FixedSize(private val size: Dp) : TvGridCells {
- init {
- require(size > 0.dp) { "Provided size $size should be larger than zero." }
- }
-
- @Deprecated("Use GridCells from compose foundation.")
- override fun Density.calculateCrossAxisCellSizes(
- availableSize: Int,
- spacing: Int
- ): List<Int> {
- val cellSize = size.roundToPx()
- return if (cellSize + spacing < availableSize + spacing) {
- val cellCount = (availableSize + spacing) / (cellSize + spacing)
- List(cellCount) { cellSize }
- } else {
- List(1) { availableSize }
- }
- }
-
- override fun hashCode(): Int {
- return size.hashCode()
- }
-
- override fun equals(other: Any?): Boolean {
- return other is FixedSize && size == other.size
- }
- }
-}
-
-private fun calculateCellsCrossAxisSizeImpl(
- gridSize: Int,
- slotCount: Int,
- spacing: Int
-): List<Int> {
- val gridSizeWithoutSpacing = gridSize - spacing * (slotCount - 1)
- val slotSize = gridSizeWithoutSpacing / slotCount
- val remainingPixels = gridSizeWithoutSpacing % slotCount
- return List(slotCount) { slotSize + if (it < remainingPixels) 1 else 0 }
-}
-
-/** Receiver scope which is used by [TvLazyVerticalGrid]. */
-@Deprecated(
- "Use LazyGridScope instead.",
- replaceWith =
- ReplaceWith(
- "LazyGridScope",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.LazyGridScope")
- )
-)
-@TvLazyGridScopeMarker
-sealed interface TvLazyGridScope {
- /**
- * Adds a single item to the scope.
- *
- * @param key a stable and unique key representing the item. Using the same key for multiple
- * items in the grid is not allowed. Type of the key should be saveable via Bundle on Android.
- * If null is passed the position in the grid will represent the key. When you specify the key
- * the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param span the span of the item. Default is 1x1. It is good practice to leave it `null` when
- * this matches the intended behavior, as providing a custom implementation impacts
- * performance
- * @param contentType the type of the content of this item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such
- * type will be considered compatible.
- * @param content the content of the item
- */
- @Deprecated("Use `LazyGridScope.item` instead")
- fun item(
- key: Any? = null,
- span: (TvLazyGridItemSpanScope.() -> TvGridItemSpan)? = null,
- contentType: Any? = null,
- content: @Composable TvLazyGridItemScope.() -> Unit
- )
-
- /**
- * Adds a [count] of items.
- *
- * @param count the items count
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the grid will represent the key. When you
- * specify the key the scroll position will be maintained based on the key, which means if you
- * add/remove items before the current visible item the item with the given key will be kept
- * as the first visible one.
- * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
- * it `null` when this matches the intended behavior, as providing a custom implementation
- * impacts performance
- * @param contentType a factory of the content types for the item. The item compositions of the
- * same type could be reused more efficiently. Note that null is a valid type and items of
- * such type will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
- @Deprecated("Use `LazyGridScope.items` instead")
- fun items(
- count: Int,
- key: ((index: Int) -> Any)? = null,
- span: (TvLazyGridItemSpanScope.(index: Int) -> TvGridItemSpan)? = null,
- contentType: (index: Int) -> Any? = { null },
- itemContent: @Composable TvLazyGridItemScope.(index: Int) -> Unit
- )
-}
-
-/**
- * Adds a list of items.
- *
- * @param items the data list
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the grid will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
- * `null` when this matches the intended behavior, as providing a custom implementation impacts
- * performance
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyGridScope.items` instead")
-inline fun <T> TvLazyGridScope.items(
- items: List<T>,
- noinline key: ((item: T) -> Any)? = null,
- noinline span: (TvLazyGridItemSpanScope.(item: T) -> TvGridItemSpan)? = null,
- noinline contentType: (item: T) -> Any? = { null },
- crossinline itemContent: @Composable TvLazyGridItemScope.(item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(items[index]) } else null,
- span =
- if (span != null) {
- { span(items[it]) }
- } else null,
- contentType = { index: Int -> contentType(items[index]) }
- ) {
- itemContent(items[it])
- }
-
-/**
- * Adds a list of items where the content of an item is aware of its index.
- *
- * @param items the data list
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the grid will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
- * `null` when this matches the intended behavior, as providing a custom implementation impacts
- * performance
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyGridScope.itemsIndexed` instead.")
-inline fun <T> TvLazyGridScope.itemsIndexed(
- items: List<T>,
- noinline key: ((index: Int, item: T) -> Any)? = null,
- noinline span: (TvLazyGridItemSpanScope.(index: Int, item: T) -> TvGridItemSpan)? = null,
- crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
- crossinline itemContent: @Composable TvLazyGridItemScope.(index: Int, item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(index, items[index]) } else null,
- span =
- if (span != null) {
- { span(it, items[it]) }
- } else null,
- contentType = { index -> contentType(index, items[index]) }
- ) {
- itemContent(it, items[it])
- }
-
-/**
- * Adds an array of items.
- *
- * @param items the data array
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the grid will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
- * `null` when this matches the intended behavior, as providing a custom implementation impacts
- * performance
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyGridScope.items` instead.")
-inline fun <T> TvLazyGridScope.items(
- items: Array<T>,
- noinline key: ((item: T) -> Any)? = null,
- noinline span: (TvLazyGridItemSpanScope.(item: T) -> TvGridItemSpan)? = null,
- noinline contentType: (item: T) -> Any? = { null },
- crossinline itemContent: @Composable TvLazyGridItemScope.(item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(items[index]) } else null,
- span =
- if (span != null) {
- { span(items[it]) }
- } else null,
- contentType = { index: Int -> contentType(items[index]) }
- ) {
- itemContent(items[it])
- }
-
-/**
- * Adds an array of items where the content of an item is aware of its index.
- *
- * @param items the data array
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the grid will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it
- * `null` when this matches the intended behavior, as providing a custom implementation impacts
- * performance
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyGridScope.itemsIndexed` instead.")
-inline fun <T> TvLazyGridScope.itemsIndexed(
- items: Array<T>,
- noinline key: ((index: Int, item: T) -> Any)? = null,
- noinline span: (TvLazyGridItemSpanScope.(index: Int, item: T) -> TvGridItemSpan)? = null,
- crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
- crossinline itemContent: @Composable TvLazyGridItemScope.(index: Int, item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(index, items[index]) } else null,
- span =
- if (span != null) {
- { span(it, items[it]) }
- } else null,
- contentType = { index -> contentType(index, items[index]) }
- ) {
- itemContent(it, items[it])
- }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
deleted file mode 100644
index ccd2065..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ /dev/null
@@ -1,320 +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.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
-
-/**
- * Handles the item placement animations when it is set via
- * [TvLazyGridItemScope.animateItemPlacement].
- *
- * This class is responsible for detecting when item position changed, figuring our start/end
- * offsets and starting the animations.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridItemPlacementAnimator {
- // state containing relevant info for active items.
- private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
-
- // snapshot of the key to index map used for the last measuring.
- private var keyIndexMap: LazyLayoutKeyIndexMap = LazyLayoutKeyIndexMap.Empty
-
- // keeps the index of the first visible index.
- private var firstVisibleIndex = 0
-
- // stored to not allocate it every pass.
- private val movingAwayKeys = LinkedHashSet<Any>()
- private val movingInFromStartBound = mutableListOf<LazyGridMeasuredItem>()
- private val movingInFromEndBound = mutableListOf<LazyGridMeasuredItem>()
- private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>()
- private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>()
-
- /**
- * Should be called after the measuring so we can detect position changes and start animations.
- *
- * Note that this method can compose new item and add it into the [positionedItems] list.
- */
- fun onMeasured(
- consumedScroll: Int,
- layoutWidth: Int,
- layoutHeight: Int,
- positionedItems: MutableList<LazyGridMeasuredItem>,
- itemProvider: LazyGridMeasuredItemProvider,
- spanLayoutProvider: LazyGridSpanLayoutProvider,
- isVertical: Boolean
- ) {
- if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
- // no animations specified - no work needed
- reset()
- return
- }
-
- val previousFirstVisibleIndex = firstVisibleIndex
- firstVisibleIndex = positionedItems.firstOrNull()?.index ?: 0
- val previousKeyToIndexMap = keyIndexMap
- keyIndexMap = itemProvider.keyIndexMap
-
- val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
-
- // the consumed scroll is considered as a delta we don't need to animate
- val scrollOffset =
- if (isVertical) {
- IntOffset(0, consumedScroll)
- } else {
- IntOffset(consumedScroll, 0)
- }
-
- // first add all items we had in the previous run
- movingAwayKeys.addAll(keyToItemInfoMap.keys)
- // iterate through the items which are visible (without animated offsets)
- positionedItems.fastForEach { item ->
- // remove items we have in the current one as they are still visible.
- movingAwayKeys.remove(item.key)
- if (item.hasAnimations) {
- val itemInfo = keyToItemInfoMap[item.key]
- // there is no state associated with this item yet
- if (itemInfo == null) {
- keyToItemInfoMap[item.key] = ItemInfo(item.crossAxisSize, item.crossAxisOffset)
- val previousIndex = previousKeyToIndexMap.getIndex(item.key)
- if (previousIndex != -1 && item.index != previousIndex) {
- if (previousIndex < previousFirstVisibleIndex) {
- // the larger index will be in the start of the list
- movingInFromStartBound.add(item)
- } else {
- movingInFromEndBound.add(item)
- }
- } else {
- initializeNode(
- item,
- item.offset.let { if (item.isVertical) it.y else it.x }
- )
- }
- } else {
- item.forEachNode {
- if (it.rawOffset != LazyLayoutAnimateItemModifierNode.NotInitialized) {
- it.rawOffset += scrollOffset
- }
- }
- itemInfo.crossAxisSize = item.crossAxisSize
- itemInfo.crossAxisOffset = item.crossAxisOffset
- startAnimationsIfNeeded(item)
- }
- } else {
- // no animation, clean up if needed
- keyToItemInfoMap.remove(item.key)
- }
- }
-
- var accumulatedOffset = 0
- var previousLine = -1
- var previousLineMainAxisSize = 0
- movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
- movingInFromStartBound.fastForEach { item ->
- val line = if (isVertical) item.row else item.column
- if (line != -1 && line == previousLine) {
- previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
- } else {
- accumulatedOffset += previousLineMainAxisSize
- previousLineMainAxisSize = item.mainAxisSize
- previousLine = line
- }
- val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize
- initializeNode(item, mainAxisOffset)
- startAnimationsIfNeeded(item)
- }
- accumulatedOffset = 0
- previousLine = -1
- previousLineMainAxisSize = 0
- movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
- movingInFromEndBound.fastForEach { item ->
- val line = if (isVertical) item.row else item.column
- if (line != -1 && line == previousLine) {
- previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
- } else {
- accumulatedOffset += previousLineMainAxisSize
- previousLineMainAxisSize = item.mainAxisSize
- previousLine = line
- }
- val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
- initializeNode(item, mainAxisOffset)
- startAnimationsIfNeeded(item)
- }
-
- movingAwayKeys.forEach { key ->
- // found an item which was in our map previously but is not a part of the
- // positionedItems now
- val itemInfo = keyToItemInfoMap.getValue(key)
- val newIndex = keyIndexMap.getIndex(key)
-
- if (newIndex == -1) {
- keyToItemInfoMap.remove(key)
- } else {
- val item =
- itemProvider.getAndMeasure(
- newIndex,
- constraints =
- if (isVertical) {
- Constraints.fixedWidth(itemInfo.crossAxisSize)
- } else {
- Constraints.fixedHeight(itemInfo.crossAxisSize)
- }
- )
- // check if we have any active placement animation on the item
- var inProgress = false
- repeat(item.placeablesCount) {
- if (item.getParentData(it).node?.isAnimationInProgress == true) {
- inProgress = true
- return@repeat
- }
- }
- if ((!inProgress && newIndex == previousKeyToIndexMap.getIndex(key))) {
- keyToItemInfoMap.remove(key)
- } else {
- if (newIndex < firstVisibleIndex) {
- movingAwayToStartBound.add(item)
- } else {
- movingAwayToEndBound.add(item)
- }
- }
- }
- }
-
- accumulatedOffset = 0
- previousLine = -1
- previousLineMainAxisSize = 0
- movingAwayToStartBound.sortByDescending { keyIndexMap.getIndex(it.key) }
- movingAwayToStartBound.fastForEach { item ->
- val line = spanLayoutProvider.getLineIndexOfItem(item.index)
- if (line != -1 && line == previousLine) {
- previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
- } else {
- accumulatedOffset += previousLineMainAxisSize
- previousLineMainAxisSize = item.mainAxisSize
- previousLine = line
- }
- val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize
-
- val itemInfo = keyToItemInfoMap.getValue(item.key)
-
- item.position(
- mainAxisOffset = mainAxisOffset,
- crossAxisOffset = itemInfo.crossAxisOffset,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight
- )
- positionedItems.add(item)
- startAnimationsIfNeeded(item)
- }
- accumulatedOffset = 0
- previousLine = -1
- previousLineMainAxisSize = 0
- movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) }
- movingAwayToEndBound.fastForEach { item ->
- val line = spanLayoutProvider.getLineIndexOfItem(item.index)
- if (line != -1 && line == previousLine) {
- previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
- } else {
- accumulatedOffset += previousLineMainAxisSize
- previousLineMainAxisSize = item.mainAxisSize
- previousLine = line
- }
- val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
-
- val itemInfo = keyToItemInfoMap.getValue(item.key)
- item.position(
- mainAxisOffset = mainAxisOffset,
- crossAxisOffset = itemInfo.crossAxisOffset,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- )
-
- positionedItems.add(item)
- startAnimationsIfNeeded(item)
- }
-
- movingInFromStartBound.clear()
- movingInFromEndBound.clear()
- movingAwayToStartBound.clear()
- movingAwayToEndBound.clear()
- movingAwayKeys.clear()
- }
-
- /**
- * Should be called when the animations are not needed for the next positions change, for
- * example when we snap to a new position.
- */
- fun reset() {
- keyToItemInfoMap.clear()
- keyIndexMap = LazyLayoutKeyIndexMap.Empty
- firstVisibleIndex = -1
- }
-
- private fun initializeNode(item: LazyGridMeasuredItem, mainAxisOffset: Int) {
- val firstPlaceableOffset = item.offset
-
- val targetFirstPlaceableOffset =
- if (item.isVertical) {
- firstPlaceableOffset.copy(y = mainAxisOffset)
- } else {
- firstPlaceableOffset.copy(x = mainAxisOffset)
- }
-
- // initialize offsets
- item.forEachNode { node ->
- val diffToFirstPlaceableOffset = item.offset - firstPlaceableOffset
- node.rawOffset = targetFirstPlaceableOffset + diffToFirstPlaceableOffset
- }
- }
-
- private fun startAnimationsIfNeeded(item: LazyGridMeasuredItem) {
- item.forEachNode { node ->
- val newTarget = item.offset
- val currentTarget = node.rawOffset
- if (
- currentTarget != LazyLayoutAnimateItemModifierNode.NotInitialized &&
- currentTarget != newTarget
- ) {
- node.animatePlacementDelta(newTarget - currentTarget)
- }
- node.rawOffset = newTarget
- }
- }
-
- private val Any?.node
- get() = this as? LazyLayoutAnimateItemModifierNode
-
- private val LazyGridMeasuredItem.hasAnimations: Boolean
- get() {
- forEachNode {
- return true
- }
- return false
- }
-
- private inline fun LazyGridMeasuredItem.forEachNode(
- block: (LazyLayoutAnimateItemModifierNode) -> Unit
- ) {
- repeat(placeablesCount) { index -> getParentData(index).node?.let(block) }
- }
-}
-
-private class ItemInfo(var crossAxisSize: Int, var crossAxisOffset: Int)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
deleted file mode 100644
index c799d79..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.referentialEqualityPolicy
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
-import androidx.tv.foundation.lazy.layout.NearestRangeKeyIndexMap
-
-@ExperimentalFoundationApi
-internal interface LazyGridItemProvider : LazyLayoutItemProvider {
- val keyIndexMap: LazyLayoutKeyIndexMap
- val spanLayoutProvider: LazyGridSpanLayoutProvider
-}
-
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberLazyGridItemProviderLambda(
- state: TvLazyGridState,
- content: TvLazyGridScope.() -> Unit,
-): () -> LazyGridItemProvider {
- val latestContent = rememberUpdatedState(content)
- return remember(state) {
- val intervalContentState =
- derivedStateOf(referentialEqualityPolicy()) {
- LazyGridIntervalContent(latestContent.value)
- }
- val itemProviderState =
- derivedStateOf(referentialEqualityPolicy()) {
- val intervalContent = intervalContentState.value
- val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent)
- LazyGridItemProviderImpl(
- state = state,
- intervalContent = intervalContent,
- keyIndexMap = map
- )
- }
- itemProviderState::value
- }
-}
-
-@ExperimentalFoundationApi
-private class LazyGridItemProviderImpl(
- private val state: TvLazyGridState,
- private val intervalContent: LazyGridIntervalContent,
- override val keyIndexMap: LazyLayoutKeyIndexMap,
-) : LazyGridItemProvider {
-
- override val itemCount: Int
- get() = intervalContent.itemCount
-
- override fun getKey(index: Int): Any =
- keyIndexMap.getKey(index) ?: intervalContent.getKey(index)
-
- override fun getContentType(index: Int): Any? = intervalContent.getContentType(index)
-
- @Composable
- override fun Item(index: Int, key: Any) {
- LazyLayoutPinnableItem(key, index, state.pinnedItems) {
- intervalContent.withInterval(index) { localIndex, content ->
- content.item(TvLazyGridItemScopeImpl, localIndex)
- }
- }
- }
-
- override val spanLayoutProvider: LazyGridSpanLayoutProvider
- get() = intervalContent.spanLayoutProvider
-
- override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key)
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is LazyGridItemProviderImpl) return false
-
- // the identity of this class is represented by intervalContent object.
- // having equals() allows us to skip items recomposition when intervalContent didn't change
- return intervalContent == other.intervalContent
- }
-
- override fun hashCode(): Int {
- return intervalContent.hashCode()
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
deleted file mode 100644
index 500b38a..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ /dev/null
@@ -1,554 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachReversed
-import androidx.compose.ui.util.fastSumBy
-import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
-import androidx.tv.foundation.lazy.list.fastFilter
-import kotlin.math.abs
-import kotlin.math.min
-import kotlin.math.roundToInt
-import kotlin.math.sign
-
-/**
- * Measures and calculates the positions for the currently visible items. The result is produced as
- * a [TvLazyGridMeasureResult] which contains all the calculations.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal fun measureLazyGrid(
- itemsCount: Int,
- measuredLineProvider: LazyGridMeasuredLineProvider,
- measuredItemProvider: LazyGridMeasuredItemProvider,
- mainAxisAvailableSize: Int,
- beforeContentPadding: Int,
- afterContentPadding: Int,
- spaceBetweenLines: Int,
- firstVisibleLineIndex: Int,
- firstVisibleLineScrollOffset: Int,
- scrollToBeConsumed: Float,
- constraints: Constraints,
- isVertical: Boolean,
- verticalArrangement: Arrangement.Vertical?,
- horizontalArrangement: Arrangement.Horizontal?,
- reverseLayout: Boolean,
- density: Density,
- placementAnimator: LazyGridItemPlacementAnimator,
- spanLayoutProvider: LazyGridSpanLayoutProvider,
- pinnedItems: List<Int>,
- layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
-): TvLazyGridMeasureResult {
- require(beforeContentPadding >= 0) { "negative beforeContentPadding" }
- require(afterContentPadding >= 0) { "negative afterContentPadding" }
- if (itemsCount <= 0) {
- // empty data set. reset the current scroll and report zero size
- return TvLazyGridMeasureResult(
- firstVisibleLine = null,
- firstVisibleLineScrollOffset = 0,
- canScrollForward = false,
- consumedScroll = 0f,
- measureResult = layout(constraints.minWidth, constraints.minHeight) {},
- visibleItemsInfo = emptyList(),
- viewportStartOffset = -beforeContentPadding,
- viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
- totalItemsCount = 0,
- reverseLayout = reverseLayout,
- orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
- afterContentPadding = afterContentPadding,
- mainAxisItemSpacing = spaceBetweenLines
- )
- } else {
- var currentFirstLineIndex = firstVisibleLineIndex
- var currentFirstLineScrollOffset = firstVisibleLineScrollOffset
-
- // represents the real amount of scroll we applied as a result of this measure pass.
- var scrollDelta = scrollToBeConsumed.roundToInt()
-
- // applying the whole requested scroll offset. we will figure out if we can't consume
- // all of it later
- currentFirstLineScrollOffset -= scrollDelta
-
- // if the current scroll offset is less than minimally possible
- if (currentFirstLineIndex == 0 && currentFirstLineScrollOffset < 0) {
- scrollDelta += currentFirstLineScrollOffset
- currentFirstLineScrollOffset = 0
- }
-
- // this will contain all the MeasuredItems representing the visible lines
- val visibleLines = ArrayDeque<LazyGridMeasuredLine>()
-
- // define min and max offsets
- val minOffset = -beforeContentPadding + if (spaceBetweenLines < 0) spaceBetweenLines else 0
- val maxOffset = mainAxisAvailableSize
-
- // include the start padding so we compose items in the padding area and neutralise item
- // spacing (if the spacing is negative this will make sure the previous item is composed)
- // before starting scrolling forward we will remove it back
- currentFirstLineScrollOffset += minOffset
-
- // we had scrolled backward or we compose items in the start padding area, which means
- // items before current firstLineScrollOffset should be visible. compose them and update
- // firstLineScrollOffset
- while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > 0) {
- val previous = currentFirstLineIndex - 1
- val measuredLine = measuredLineProvider.getAndMeasure(previous)
- visibleLines.add(0, measuredLine)
- currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
- currentFirstLineIndex = previous
- }
-
- // if we were scrolled backward, but there were not enough items before. this means
- // not the whole scroll was consumed
- if (currentFirstLineScrollOffset < minOffset) {
- scrollDelta += currentFirstLineScrollOffset
- currentFirstLineScrollOffset = minOffset
- }
-
- // neutralize previously added padding as we stopped filling the before content padding
- currentFirstLineScrollOffset -= minOffset
-
- var index = currentFirstLineIndex
- val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
- var currentMainAxisOffset = -currentFirstLineScrollOffset
-
- // first we need to skip lines we already composed while composing backward
- visibleLines.fastForEach {
- index++
- currentMainAxisOffset += it.mainAxisSizeWithSpacings
- }
-
- // then composing visible lines forward until we fill the whole viewport.
- // we want to have at least one line in visibleItems even if in fact all the items are
- // offscreen, this can happen if the content padding is larger than the available size.
- while (
- index < itemsCount &&
- (currentMainAxisOffset < maxMainAxis ||
- currentMainAxisOffset <= 0 || // filling beforeContentPadding area
- visibleLines.isEmpty())
- ) {
- val measuredLine = measuredLineProvider.getAndMeasure(index)
- if (measuredLine.isEmpty()) {
- break
- }
-
- currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
- if (
- currentMainAxisOffset <= minOffset &&
- measuredLine.items.last().index != itemsCount - 1
- ) {
- // this line is offscreen and will not be placed. advance firstVisibleLineIndex
- currentFirstLineIndex = index + 1
- currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
- } else {
- visibleLines.add(measuredLine)
- }
- index++
- }
-
- // we didn't fill the whole viewport with lines starting from firstVisibleLineIndex.
- // lets try to scroll back if we have enough lines before firstVisibleLineIndex.
- if (currentMainAxisOffset < maxOffset) {
- val toScrollBack = maxOffset - currentMainAxisOffset
- currentFirstLineScrollOffset -= toScrollBack
- currentMainAxisOffset += toScrollBack
- while (
- currentFirstLineScrollOffset < beforeContentPadding && currentFirstLineIndex > 0
- ) {
- val previousIndex = currentFirstLineIndex - 1
- val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
- visibleLines.add(0, measuredLine)
- currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
- currentFirstLineIndex = previousIndex
- }
- scrollDelta += toScrollBack
- if (currentFirstLineScrollOffset < 0) {
- scrollDelta += currentFirstLineScrollOffset
- currentMainAxisOffset += currentFirstLineScrollOffset
- currentFirstLineScrollOffset = 0
- }
- }
-
- // report the amount of pixels we consumed. scrollDelta can be smaller than
- // scrollToBeConsumed if there were not enough lines to fill the offered space or it
- // can be larger if lines were resized, or if, for example, we were previously
- // displaying the line 15, but now we have only 10 lines in total in the data set.
- val consumedScroll =
- if (
- scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
- abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
- ) {
- scrollDelta.toFloat()
- } else {
- scrollToBeConsumed
- }
-
- // the initial offset for lines from visibleLines list
- require(currentFirstLineScrollOffset >= 0) { "negative initial offset" }
- val visibleLinesScrollOffset = -currentFirstLineScrollOffset
- var firstLine = visibleLines.first()
-
- val firstItemIndex = firstLine.items.firstOrNull()?.index ?: 0
- val lastItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index ?: 0
- val extraItemsBefore =
- calculateExtraItems(
- pinnedItems,
- measuredItemProvider,
- itemConstraints = { measuredLineProvider.itemConstraints(it) },
- filter = { it in 0 until firstItemIndex }
- )
-
- val extraItemsAfter =
- calculateExtraItems(
- pinnedItems,
- measuredItemProvider,
- itemConstraints = { measuredLineProvider.itemConstraints(it) },
- filter = { it in (lastItemIndex + 1) until itemsCount }
- )
-
- // even if we compose lines to fill before content padding we should ignore lines fully
- // located there for the state's scroll position calculation (first line + first offset)
- if (beforeContentPadding > 0 || spaceBetweenLines < 0) {
- for (i in visibleLines.indices) {
- val size = visibleLines[i].mainAxisSizeWithSpacings
- if (
- currentFirstLineScrollOffset != 0 &&
- size <= currentFirstLineScrollOffset &&
- i != visibleLines.lastIndex
- ) {
- currentFirstLineScrollOffset -= size
- firstLine = visibleLines[i + 1]
- } else {
- break
- }
- }
- }
-
- val layoutWidth =
- if (isVertical) {
- constraints.maxWidth
- } else {
- constraints.constrainWidth(currentMainAxisOffset)
- }
- val layoutHeight =
- if (isVertical) {
- constraints.constrainHeight(currentMainAxisOffset)
- } else {
- constraints.maxHeight
- }
-
- val positionedItems =
- calculateItemsOffsets(
- lines = visibleLines,
- itemsBefore = extraItemsBefore,
- itemsAfter = extraItemsAfter,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- finalMainAxisOffset = currentMainAxisOffset,
- maxOffset = maxOffset,
- firstLineScrollOffset = visibleLinesScrollOffset,
- isVertical = isVertical,
- verticalArrangement = verticalArrangement,
- horizontalArrangement = horizontalArrangement,
- reverseLayout = reverseLayout,
- density = density
- )
-
- placementAnimator.onMeasured(
- consumedScroll = consumedScroll.toInt(),
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- positionedItems = positionedItems,
- itemProvider = measuredItemProvider,
- spanLayoutProvider = spanLayoutProvider,
- isVertical = isVertical
- )
-
- return TvLazyGridMeasureResult(
- firstVisibleLine = firstLine,
- firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
- canScrollForward = lastItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
- consumedScroll = consumedScroll,
- measureResult =
- layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { it.place(this) }
- },
- viewportStartOffset = -beforeContentPadding,
- viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
- visibleItemsInfo =
- if (extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) {
- positionedItems
- } else {
- positionedItems.fastFilter { it.index in firstItemIndex..lastItemIndex }
- },
- totalItemsCount = itemsCount,
- reverseLayout = reverseLayout,
- orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
- afterContentPadding = afterContentPadding,
- mainAxisItemSpacing = spaceBetweenLines
- )
- }
-}
-
-@ExperimentalFoundationApi
-private inline fun calculateExtraItems(
- pinnedItems: List<Int>,
- measuredItemProvider: LazyGridMeasuredItemProvider,
- itemConstraints: (Int) -> Constraints,
- filter: (Int) -> Boolean
-): List<LazyGridMeasuredItem> {
- var items: MutableList<LazyGridMeasuredItem>? = null
-
- pinnedItems.fastForEach { index ->
- if (filter(index)) {
- val constraints = itemConstraints(index)
- val measuredItem = measuredItemProvider.getAndMeasure(index, constraints = constraints)
- if (items == null) {
- items = mutableListOf()
- }
- items?.add(measuredItem)
- }
- }
-
- return items ?: emptyList()
-}
-
-/** Calculates [LazyGridMeasuredLine]s offsets. */
-private fun calculateItemsOffsets(
- lines: List<LazyGridMeasuredLine>,
- itemsBefore: List<LazyGridMeasuredItem>,
- itemsAfter: List<LazyGridMeasuredItem>,
- layoutWidth: Int,
- layoutHeight: Int,
- finalMainAxisOffset: Int,
- maxOffset: Int,
- firstLineScrollOffset: Int,
- isVertical: Boolean,
- verticalArrangement: Arrangement.Vertical?,
- horizontalArrangement: Arrangement.Horizontal?,
- reverseLayout: Boolean,
- density: Density,
-): MutableList<LazyGridMeasuredItem> {
- val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
- val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
- if (hasSpareSpace) {
- check(firstLineScrollOffset == 0) { "non-zero firstLineScrollOffset" }
- }
-
- val positionedItems = ArrayList<LazyGridMeasuredItem>(lines.fastSumBy { it.items.size })
-
- if (hasSpareSpace) {
- require(itemsBefore.isEmpty() && itemsAfter.isEmpty()) { "no items" }
- val linesCount = lines.size
- fun Int.reverseAware() = if (!reverseLayout) this else linesCount - this - 1
-
- val sizes = IntArray(linesCount) { index -> lines[index.reverseAware()].mainAxisSize }
- val offsets = IntArray(linesCount) { 0 }
- if (isVertical) {
- with(requireNotNull(verticalArrangement) { "null verticalArrangement" }) {
- density.arrange(mainAxisLayoutSize, sizes, offsets)
- }
- } else {
- with(requireNotNull(horizontalArrangement) { "null horizontalArrangement" }) {
- // Enforces Ltr layout direction as it is mirrored with placeRelative later.
- density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
- }
- }
-
- val reverseAwareOffsetIndices =
- if (reverseLayout) offsets.indices.reversed() else offsets.indices
-
- for (index in reverseAwareOffsetIndices) {
- val absoluteOffset = offsets[index]
- // when reverseLayout == true, offsets are stored in the reversed order to items
- val line = lines[index.reverseAware()]
- val relativeOffset =
- if (reverseLayout) {
- // inverse offset to align with scroll direction for positioning
- mainAxisLayoutSize - absoluteOffset - line.mainAxisSize
- } else {
- absoluteOffset
- }
- positionedItems.addAll(line.position(relativeOffset, layoutWidth, layoutHeight))
- }
- } else {
- var currentMainAxis = firstLineScrollOffset
-
- itemsBefore.fastForEachReversed {
- currentMainAxis -= it.mainAxisSizeWithSpacings
- it.position(currentMainAxis, 0, layoutWidth, layoutHeight)
- positionedItems.add(it)
- }
-
- currentMainAxis = firstLineScrollOffset
- lines.fastForEach {
- positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
- currentMainAxis += it.mainAxisSizeWithSpacings
- }
-
- itemsAfter.fastForEach {
- it.position(currentMainAxis, 0, layoutWidth, layoutHeight)
- positionedItems.add(it)
- currentMainAxis += it.mainAxisSizeWithSpacings
- }
- }
- return positionedItems
-}
-
-/** Abstracts away subcomposition and span calculation from the measuring logic of entire lines. */
-@OptIn(ExperimentalFoundationApi::class)
-internal abstract class LazyGridMeasuredLineProvider(
- private val isVertical: Boolean,
- private val slots: LazyGridSlots,
- private val gridItemsCount: Int,
- private val spaceBetweenLines: Int,
- private val measuredItemProvider: LazyGridMeasuredItemProvider,
- private val spanLayoutProvider: LazyGridSpanLayoutProvider
-) {
- // The constraints for cross axis size. The main axis is not restricted.
- internal fun childConstraints(startSlot: Int, span: Int): Constraints {
- val crossAxisSize =
- if (span == 1) {
- slots.sizes[startSlot]
- } else {
- val endSlot = startSlot + span - 1
- slots.positions[endSlot] + slots.sizes[endSlot] - slots.positions[startSlot]
- }
- .coerceAtLeast(0)
- return if (isVertical) {
- Constraints.fixedWidth(crossAxisSize)
- } else {
- Constraints.fixedHeight(crossAxisSize)
- }
- }
-
- fun itemConstraints(itemIndex: Int): Constraints {
- val span = spanLayoutProvider.spanOf(itemIndex, spanLayoutProvider.slotsPerLine)
- return childConstraints(0, span)
- }
-
- /**
- * Used to subcompose items on lines of lazy grids. Composed placeables will be measured with
- * the correct constraints and wrapped into [LazyGridMeasuredLine].
- */
- fun getAndMeasure(lineIndex: Int): LazyGridMeasuredLine {
- val lineConfiguration = spanLayoutProvider.getLineConfiguration(lineIndex)
- val lineItemsCount = lineConfiguration.spans.size
-
- // we add space between lines as an extra spacing for all lines apart from the last one
- // so the lazy grid measuring logic will take it into account.
- val mainAxisSpacing =
- if (
- lineItemsCount == 0 ||
- lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount
- ) {
- 0
- } else {
- spaceBetweenLines
- }
-
- var startSlot = 0
- val items =
- Array(lineItemsCount) {
- val span = lineConfiguration.spans[it].currentLineSpan
- val constraints = childConstraints(startSlot, span)
- measuredItemProvider
- .getAndMeasure(
- lineConfiguration.firstItemIndex + it,
- mainAxisSpacing,
- constraints
- )
- .also { startSlot += span }
- }
- return createLine(lineIndex, items, lineConfiguration.spans, mainAxisSpacing)
- }
-
- /**
- * Contains the mapping between the key and the index. It could contain not all the items of the
- * list as an optimization.
- */
- val keyIndexMap: LazyLayoutKeyIndexMap
- get() = measuredItemProvider.keyIndexMap
-
- abstract fun createLine(
- index: Int,
- items: Array<LazyGridMeasuredItem>,
- spans: List<TvGridItemSpan>,
- mainAxisSpacing: Int
- ): LazyGridMeasuredLine
-}
-
-/** Abstracts away the subcomposition from the measuring logic. */
-@OptIn(ExperimentalFoundationApi::class)
-internal abstract class LazyGridMeasuredItemProvider
-@ExperimentalFoundationApi
-constructor(
- private val itemProvider: LazyGridItemProvider,
- private val measureScope: LazyLayoutMeasureScope,
- private val defaultMainAxisSpacing: Int
-) {
- /**
- * Used to subcompose individual items of lazy grids. Composed placeables will be measured with
- * the provided [constraints] and wrapped into [LazyGridMeasuredItem].
- */
- fun getAndMeasure(
- index: Int,
- mainAxisSpacing: Int = defaultMainAxisSpacing,
- constraints: Constraints
- ): LazyGridMeasuredItem {
- val key = itemProvider.getKey(index)
- val contentType = itemProvider.getContentType(index)
- val placeables = measureScope.measure(index, constraints)
- val crossAxisSize =
- if (constraints.hasFixedWidth) {
- constraints.minWidth
- } else {
- require(constraints.hasFixedHeight) { "does not have fixed height" }
- constraints.minHeight
- }
- return createItem(index, key, contentType, crossAxisSize, mainAxisSpacing, placeables)
- }
-
- /**
- * Contains the mapping between the key and the index. It could contain not all the items of the
- * list as an optimization.
- */
- val keyIndexMap: LazyLayoutKeyIndexMap
- get() = itemProvider.keyIndexMap
-
- abstract fun createItem(
- index: Int,
- key: Any,
- contentType: Any?,
- crossAxisSize: Int,
- mainAxisSpacing: Int,
- placeables: List<Placeable>
- ): LazyGridMeasuredItem
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasuredItem.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasuredItem.kt
deleted file mode 100644
index dc0dd51..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasuredItem.kt
+++ /dev/null
@@ -1,180 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
-
-/**
- * Represents one measured item of the lazy grid. It can in fact consist of multiple placeables if
- * the user emit multiple layout nodes in the item callback.
- */
-internal class LazyGridMeasuredItem(
- override val index: Int,
- override val key: Any,
- val isVertical: Boolean,
- /**
- * Cross axis size is the same for all [placeables]. Take it as parameter for the case when
- * [placeables] is empty.
- */
- val crossAxisSize: Int,
- mainAxisSpacing: Int,
- private val reverseLayout: Boolean,
- private val layoutDirection: LayoutDirection,
- private val beforeContentPadding: Int,
- private val afterContentPadding: Int,
- private val placeables: List<Placeable>,
- /**
- * The offset which shouldn't affect any calculations but needs to be applied for the final
- * value passed into the place() call.
- */
- private val visualOffset: IntOffset,
- override val contentType: Any?
-) : TvLazyGridItemInfo {
- /** Main axis size of the item - the max main axis size of the placeables. */
- val mainAxisSize: Int
-
- /** The max main axis size of the placeables plus mainAxisSpacing. */
- val mainAxisSizeWithSpacings: Int
-
- val placeablesCount: Int
- get() = placeables.size
-
- private var mainAxisLayoutSize: Int = Unset
- private var minMainAxisOffset: Int = 0
- private var maxMainAxisOffset: Int = 0
-
- fun getParentData(index: Int) = placeables[index].parentData
-
- init {
- var maxMainAxis = 0
- placeables.fastForEach {
- maxMainAxis = maxOf(maxMainAxis, if (isVertical) it.height else it.width)
- }
- mainAxisSize = maxMainAxis
- mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
- }
-
- override val size: IntSize =
- if (isVertical) {
- IntSize(crossAxisSize, mainAxisSize)
- } else {
- IntSize(mainAxisSize, crossAxisSize)
- }
- override var offset: IntOffset = IntOffset.Zero
- private set
-
- val crossAxisOffset
- get() = if (isVertical) offset.x else offset.y
-
- override var row: Int = TvLazyGridItemInfo.UnknownRow
- private set
-
- override var column: Int = TvLazyGridItemInfo.UnknownColumn
- private set
-
- /**
- * Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset].
- * [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
- * up outside of the viewport (for example one item consist of 2 placeables, and the first one
- * is not going to be visible, so we don't place it as an optimization, but place the second
- * one). If [reverseOrder] is true the inner placeables would be placed in the inverted order.
- */
- fun position(
- mainAxisOffset: Int,
- crossAxisOffset: Int,
- layoutWidth: Int,
- layoutHeight: Int,
- row: Int = TvLazyGridItemInfo.UnknownRow,
- column: Int = TvLazyGridItemInfo.UnknownColumn
- ) {
- mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
- val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
- @Suppress("NAME_SHADOWING")
- val crossAxisOffset =
- if (isVertical && layoutDirection == LayoutDirection.Rtl) {
- crossAxisLayoutSize - crossAxisOffset - crossAxisSize
- } else {
- crossAxisOffset
- }
- offset =
- if (isVertical) {
- IntOffset(crossAxisOffset, mainAxisOffset)
- } else {
- IntOffset(mainAxisOffset, crossAxisOffset)
- }
- this.row = row
- this.column = column
- minMainAxisOffset = -beforeContentPadding
- maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
- }
-
- fun place(
- scope: Placeable.PlacementScope,
- ) =
- with(scope) {
- require(mainAxisLayoutSize != Unset) { "position() should be called first" }
- repeat(placeablesCount) { index ->
- val placeable = placeables[index]
- val minOffset = minMainAxisOffset - placeable.mainAxisSize
- val maxOffset = maxMainAxisOffset
-
- var offset = offset
- val animateNode = getParentData(index) as? LazyLayoutAnimateItemModifierNode
- if (animateNode != null) {
- val animatedOffset = offset + animateNode.placementDelta
- // cancel the animation if current and target offsets are both out of the
- // bounds.
- if (
- (offset.mainAxis <= minOffset && animatedOffset.mainAxis <= minOffset) ||
- (offset.mainAxis >= maxOffset && animatedOffset.mainAxis >= maxOffset)
- ) {
- animateNode.cancelAnimation()
- }
- offset = animatedOffset
- }
- if (reverseLayout) {
- offset =
- offset.copy { mainAxisOffset ->
- mainAxisLayoutSize - mainAxisOffset - placeable.mainAxisSize
- }
- }
- offset += visualOffset
- if (isVertical) {
- placeable.placeWithLayer(offset)
- } else {
- placeable.placeRelativeWithLayer(offset)
- }
- }
- }
-
- private val IntOffset.mainAxis
- get() = if (isVertical) y else x
-
- private val Placeable.mainAxisSize
- get() = if (isVertical) height else width
-
- private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
- IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
-}
-
-private const val Unset = Int.MIN_VALUE
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasuredLine.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasuredLine.kt
deleted file mode 100644
index 0561ebb..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasuredLine.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-
-/**
- * Represents one measured line of the lazy list. Each item on the line can in fact consist of
- * multiple placeables if the user emit multiple layout nodes in the item callback.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridMeasuredLine
-constructor(
- val index: Int,
- val items: Array<LazyGridMeasuredItem>,
- private val slots: LazyGridSlots,
- private val spans: List<TvGridItemSpan>,
- private val isVertical: Boolean,
- /** Spacing to be added after [mainAxisSize], in the main axis direction. */
- private val mainAxisSpacing: Int,
-) {
- /** Main axis size of the line - the max main axis size of the items on the line. */
- val mainAxisSize: Int
-
- /**
- * Sum of [mainAxisSpacing] and the max of the main axis sizes of the placeables on the line.
- */
- val mainAxisSizeWithSpacings: Int
-
- init {
- var maxMainAxis = 0
- items.forEach { item -> maxMainAxis = maxOf(maxMainAxis, item.mainAxisSize) }
- mainAxisSize = maxMainAxis
- mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
- }
-
- /** Whether this line contains any items. */
- fun isEmpty() = items.isEmpty()
-
- /**
- * Calculates positions for the [items] at [offset] main axis position. If [reverseOrder] is
- * true the [items] would be placed in the inverted order.
- */
- fun position(offset: Int, layoutWidth: Int, layoutHeight: Int): Array<LazyGridMeasuredItem> {
- var usedSpan = 0
- items.forEachIndexed { itemIndex, item ->
- val span = spans[itemIndex].currentLineSpan
- val startSlot = usedSpan
-
- item
- .position(
- mainAxisOffset = offset,
- crossAxisOffset = slots.positions[startSlot],
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- row = if (isVertical) index else startSlot,
- column = if (isVertical) startSlot else index
- )
- .also { usedSpan += span }
- }
- return items
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
deleted file mode 100644
index 42a3e30..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.setValue
-import androidx.tv.foundation.lazy.layout.LazyLayoutNearestRangeState
-
-/**
- * Contains the current scroll position represented by the first visible item index and the first
- * visible item scroll offset.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridScrollPosition(initialIndex: Int = 0, initialScrollOffset: Int = 0) {
- var index by mutableIntStateOf(initialIndex)
- private set
-
- var scrollOffset by mutableIntStateOf(initialScrollOffset)
- private set
-
- private var hadFirstNotEmptyLayout = false
-
- /** The last known key of the first item at [index] line. */
- private var lastKnownFirstItemKey: Any? = null
-
- val nearestRangeState =
- LazyLayoutNearestRangeState(
- initialIndex,
- NearestItemsSlidingWindowSize,
- NearestItemsExtraItemCount
- )
-
- /** Updates the current scroll position based on the results of the last measurement. */
- fun updateFromMeasureResult(measureResult: TvLazyGridMeasureResult) {
- lastKnownFirstItemKey = measureResult.firstVisibleLine?.items?.firstOrNull()?.key
- // we ignore the index and offset from measureResult until we get at least one
- // measurement with real items. otherwise the initial index and scroll passed to the
- // state would be lost and overridden with zeros.
- if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
- hadFirstNotEmptyLayout = true
- val scrollOffset = measureResult.firstVisibleLineScrollOffset
- check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
-
- val firstIndex = measureResult.firstVisibleLine?.items?.firstOrNull()?.index ?: 0
- update(firstIndex, scrollOffset)
- }
- }
-
- /**
- * Updates the scroll position - the passed values will be used as a start position for
- * composing the items during the next measure pass and will be updated by the real position
- * calculated during the measurement. This means that there is guarantee that exactly this index
- * and offset will be applied as it is possible that: a) there will be no item at this index in
- * reality b) item at this index will be smaller than the asked scrollOffset, which means we
- * would switch to the next item c) there will be not enough items to fill the viewport after
- * the requested index, so we would have to compose few elements before the asked index,
- * changing the first visible item.
- */
- fun requestPosition(index: Int, scrollOffset: Int) {
- update(index, scrollOffset)
- // clear the stored key as we have a direct request to scroll to [index] position and the
- // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
- lastKnownFirstItemKey = null
- }
-
- /**
- * In addition to keeping the first visible item index we also store the key of this item. When
- * the user provided custom keys for the items this mechanism allows us to detect when there
- * were items added or removed before our current first visible item and keep this item as the
- * first visible one even given that its index has been changed.
- */
- fun updateScrollPositionIfTheFirstItemWasMoved(
- itemProvider: LazyGridItemProvider,
- index: Int
- ): Int {
- val newIndex = itemProvider.findIndexByKey(lastKnownFirstItemKey, index)
- if (index != newIndex) {
- this.index = newIndex
- nearestRangeState.update(index)
- }
- return newIndex
- }
-
- private fun update(index: Int, scrollOffset: Int) {
- require(index >= 0f) { "Index should be non-negative ($index)" }
- this.index = index
- nearestRangeState.update(index)
- this.scrollOffset = scrollOffset
- }
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private const val NearestItemsSlidingWindowSize = 90
-
-/** The minimum amount of items near the current first visible item we want to have mapping for. */
-private const val NearestItemsExtraItemCount = 200
-
-/**
- * Finds a position of the item with the given key in the lists. This logic allows us to detect when
- * there were items added or removed before our current first item.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal fun LazyLayoutItemProvider.findIndexByKey(
- key: Any?,
- lastKnownIndex: Int,
-): Int {
- if (key == null || itemCount == 0) {
- // there were no real item during the previous measure
- return lastKnownIndex
- }
- if (lastKnownIndex < itemCount && key == getKey(lastKnownIndex)) {
- // this item is still at the same index
- return lastKnownIndex
- }
- val newIndex = getIndex(key)
- if (newIndex != -1) {
- return newIndex
- }
- // fallback to the previous index if we don't know the new index of the item
- return lastKnownIndex
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt
deleted file mode 100644
index 97d959b..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt
+++ /dev/null
@@ -1,74 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Immutable
-
-/** Represents the span of an item in a [TvLazyVerticalGrid]. */
-@Immutable
[email protected]
-@Deprecated("Use `GridItemSpan` instead.")
-value class TvGridItemSpan internal constructor(private val packedValue: Long) {
- /**
- * The span of the item on the current line. This will be the horizontal span for items of
- * [TvLazyVerticalGrid].
- */
- @ExperimentalFoundationApi
- val currentLineSpan: Int
- get() = packedValue.toInt()
-}
-
-/**
- * Creates a [TvGridItemSpan] with a specified [currentLineSpan]. This will be the horizontal span
- * for an item of a [TvLazyVerticalGrid].
- */
-@Deprecated(
- "Use `GridItemSpan` instead.",
- replaceWith =
- ReplaceWith(
- "GridItemSpan",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.GridItemSpan")
- )
-)
-fun TvGridItemSpan(currentLineSpan: Int) = TvGridItemSpan(currentLineSpan.toLong())
-
-/** Scope of lambdas used to calculate the spans of items in lazy grids. */
-@Deprecated("Use `LazyGridItemSpanScope` instead.")
-@TvLazyGridScopeMarker
-sealed interface TvLazyGridItemSpanScope {
- /**
- * The max current line (horizontal for vertical grids) the item can occupy, such that it will
- * be positioned on the current line.
- *
- * For example if [TvLazyVerticalGrid] has 3 columns this value will be 3 for the first cell in
- * the line, 2 for the second cell, and 1 for the last one. If you return a span count larger
- * than [maxCurrentLineSpan] this means we can't fit this cell into the current line, so the
- * cell will be positioned on the next line.
- */
- val maxCurrentLineSpan: Int
-
- /**
- * The max line span (horizontal for vertical grids) an item can occupy. This will be the number
- * of columns in vertical grids or the number of rows in horizontal grids.
- *
- * For example if [TvLazyVerticalGrid] has 3 columns this value will be 3 for each cell.
- */
- val maxLineSpan: Int
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
deleted file mode 100644
index ca1fe5d..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
-import androidx.compose.foundation.lazy.layout.MutableIntervalList
-import androidx.compose.runtime.Composable
-import kotlin.math.min
-import kotlin.math.sqrt
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val gridContent: LazyGridIntervalContent) {
- class LineConfiguration(val firstItemIndex: Int, val spans: List<TvGridItemSpan>)
-
- /** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
- private val buckets = ArrayList<Bucket>().apply { add(Bucket(0)) }
- /**
- * The interval at each we will store the starting element of lines. These will be then used to
- * calculate the layout of arbitrary lines, by starting from the closest known "bucket start".
- * The smaller the bucketSize, the smaller cost for calculating layout of arbitrary lines but
- * the higher memory usage for [buckets].
- */
- private val bucketSize
- get() = sqrt(1.0 * totalSize / slotsPerLine).toInt() + 1
-
- /** Caches the last calculated line index, useful when scrolling in main axis direction. */
- private var lastLineIndex = 0
- /** Caches the starting item index on [lastLineIndex]. */
- private var lastLineStartItemIndex = 0
- /** Caches the span of [lastLineStartItemIndex], if this was already calculated. */
- private var lastLineStartKnownSpan = 0
- /**
- * Caches a calculated bucket, this is useful when scrolling in reverse main axis direction. We
- * cannot only keep the last element, as we would not know previous max span.
- */
- private var cachedBucketIndex = -1
- /**
- * Caches layout of [cachedBucketIndex], this is useful when scrolling in reverse main axis
- * direction. We cannot only keep the last element, as we would not know previous max span.
- */
- private val cachedBucket = mutableListOf<Int>()
- /** List of 1x1 spans if we do not have custom spans. */
- private var previousDefaultSpans = emptyList<TvGridItemSpan>()
-
- private fun getDefaultSpans(currentSlotsPerLine: Int) =
- if (currentSlotsPerLine == previousDefaultSpans.size) {
- previousDefaultSpans
- } else {
- List(currentSlotsPerLine) { TvGridItemSpan(1) }.also { previousDefaultSpans = it }
- }
-
- val totalSize
- get() = gridContent.intervals.size
-
- /** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
- var slotsPerLine = 0
- set(value) {
- if (value != field) {
- field = value
- invalidateCache()
- }
- }
-
- fun getLineConfiguration(lineIndex: Int): LineConfiguration {
- if (!gridContent.hasCustomSpans) {
- // Quick return when all spans are 1x1 - in this case we can easily calculate positions.
- val firstItemIndex = lineIndex * slotsPerLine
- return LineConfiguration(
- firstItemIndex,
- getDefaultSpans(
- slotsPerLine.coerceAtMost(totalSize - firstItemIndex).coerceAtLeast(0)
- )
- )
- }
-
- val bucketIndex = min(lineIndex / bucketSize, buckets.size - 1)
- // We can calculate the items on the line from the closest cached bucket start item.
- var currentLine = bucketIndex * bucketSize
- var currentItemIndex = buckets[bucketIndex].firstItemIndex
- var knownCurrentItemSpan = buckets[bucketIndex].firstItemKnownSpan
- // ... but try using the more localised cached values.
- if (lastLineIndex in currentLine..lineIndex) {
- // The last calculated value is a better start point. Common when scrolling main axis.
- currentLine = lastLineIndex
- currentItemIndex = lastLineStartItemIndex
- knownCurrentItemSpan = lastLineStartKnownSpan
- } else if (
- bucketIndex == cachedBucketIndex && lineIndex - currentLine < cachedBucket.size
- ) {
- // It happens that the needed line start is fully cached. Common when scrolling in
- // reverse main axis, as we decided to cacheThisBucket previously.
- currentItemIndex = cachedBucket[lineIndex - currentLine]
- currentLine = lineIndex
- knownCurrentItemSpan = 0
- }
-
- val cacheThisBucket =
- currentLine % bucketSize == 0 && lineIndex - currentLine in 2 until bucketSize
- if (cacheThisBucket) {
- cachedBucketIndex = bucketIndex
- cachedBucket.clear()
- }
-
- check(currentLine <= lineIndex) { "currentLine > lineIndex" }
-
- while (currentLine < lineIndex && currentItemIndex < totalSize) {
- if (cacheThisBucket) {
- cachedBucket.add(currentItemIndex)
- }
-
- var spansUsed = 0
- while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
- val span =
- if (knownCurrentItemSpan == 0) {
- spanOf(currentItemIndex, slotsPerLine - spansUsed)
- } else {
- knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
- }
- if (spansUsed + span > slotsPerLine) {
- knownCurrentItemSpan = span
- break
- }
-
- currentItemIndex++
- spansUsed += span
- }
- ++currentLine
- if (currentLine % bucketSize == 0 && currentItemIndex < totalSize) {
- val currentLineBucket = currentLine / bucketSize
- // This should happen, as otherwise this should have been used as starting point.
- check(buckets.size == currentLineBucket) { "invalid starting point" }
- buckets.add(Bucket(currentItemIndex, knownCurrentItemSpan))
- }
- }
-
- lastLineIndex = lineIndex
- lastLineStartItemIndex = currentItemIndex
- lastLineStartKnownSpan = knownCurrentItemSpan
-
- val firstItemIndex = currentItemIndex
- val spans = mutableListOf<TvGridItemSpan>()
-
- var spansUsed = 0
- while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
- val span =
- if (knownCurrentItemSpan == 0) {
- spanOf(currentItemIndex, slotsPerLine - spansUsed)
- } else {
- knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
- }
- if (spansUsed + span > slotsPerLine) break
-
- currentItemIndex++
- spans.add(TvGridItemSpan(span))
- spansUsed += span
- }
- return LineConfiguration(firstItemIndex, spans)
- }
-
- /** Calculate the line of index [itemIndex]. */
- fun getLineIndexOfItem(itemIndex: Int): Int {
- if (totalSize <= 0) {
- return 0
- }
- require(itemIndex < totalSize) { "ItemIndex > total count" }
- if (!gridContent.hasCustomSpans) {
- return itemIndex / slotsPerLine
- }
-
- val lowerBoundBucket =
- buckets
- .binarySearch { it.firstItemIndex - itemIndex }
- .let { if (it >= 0) it else -it - 2 }
- var currentLine = lowerBoundBucket * bucketSize
- var currentItemIndex = buckets[lowerBoundBucket].firstItemIndex
-
- require(currentItemIndex <= itemIndex) { "currentItemIndex > itemIndex" }
- var spansUsed = 0
- while (currentItemIndex < itemIndex) {
- val span = spanOf(currentItemIndex++, slotsPerLine - spansUsed)
- if (spansUsed + span < slotsPerLine) {
- spansUsed += span
- } else if (spansUsed + span == slotsPerLine) {
- ++currentLine
- spansUsed = 0
- } else {
- // spansUsed + span > slotsPerLine
- ++currentLine
- spansUsed = span
- }
- if (currentLine % bucketSize == 0) {
- val currentLineBucket = currentLine / bucketSize
- if (currentLineBucket >= buckets.size) {
- buckets.add(Bucket(currentItemIndex - if (spansUsed > 0) 1 else 0))
- }
- }
- }
- if (spansUsed + spanOf(itemIndex, slotsPerLine - spansUsed) > slotsPerLine) {
- ++currentLine
- }
-
- return currentLine
- }
-
- fun spanOf(itemIndex: Int, maxSpan: Int): Int =
- with(TvLazyGridItemSpanScopeImpl) {
- maxCurrentLineSpan = maxSpan
- maxLineSpan = slotsPerLine
-
- val interval = gridContent.intervals[itemIndex]
- val localIntervalIndex = itemIndex - interval.startIndex
- val span = interval.value.span.invoke(this, localIntervalIndex)
- return span.currentLineSpan
- }
-
- private fun invalidateCache() {
- buckets.clear()
- buckets.add(Bucket(0))
- lastLineIndex = 0
- lastLineStartItemIndex = 0
- lastLineStartKnownSpan = 0
- cachedBucketIndex = -1
- cachedBucket.clear()
- }
-
- private class Bucket(
- /** Index of the first item in the bucket */
- val firstItemIndex: Int,
- /** Known span of the first item. Not zero only if this item caused "line break". */
- val firstItemKnownSpan: Int = 0
- )
-
- private object TvLazyGridItemSpanScopeImpl : TvLazyGridItemSpanScope {
- override var maxCurrentLineSpan = 0
- override var maxLineSpan = 0
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridIntervalContent(content: TvLazyGridScope.() -> Unit) :
- TvLazyGridScope, LazyLayoutIntervalContent<LazyGridInterval>() {
- internal val spanLayoutProvider: LazyGridSpanLayoutProvider = LazyGridSpanLayoutProvider(this)
-
- override val intervals = MutableIntervalList<LazyGridInterval>()
-
- internal var hasCustomSpans = false
-
- init {
- apply(content)
- }
-
- @Suppress("OVERRIDE_DEPRECATION")
- override fun item(
- key: Any?,
- span: (TvLazyGridItemSpanScope.() -> TvGridItemSpan)?,
- contentType: Any?,
- content: @Composable() (TvLazyGridItemScope.() -> Unit)
- ) {
- intervals.addInterval(
- 1,
- LazyGridInterval(
- key = key?.let { { key } },
- span = span?.let { { span() } } ?: DefaultSpan,
- type = { contentType },
- item = { content() }
- )
- )
- if (span != null) hasCustomSpans = true
- }
-
- @Suppress("OVERRIDE_DEPRECATION")
- override fun items(
- count: Int,
- key: ((index: Int) -> Any)?,
- span: (TvLazyGridItemSpanScope.(index: Int) -> TvGridItemSpan)?,
- contentType: (index: Int) -> Any?,
- itemContent: @Composable() (TvLazyGridItemScope.(index: Int) -> Unit)
- ) {
- intervals.addInterval(
- count,
- LazyGridInterval(
- key = key,
- span = span ?: DefaultSpan,
- type = contentType,
- item = itemContent
- )
- )
- if (span != null) hasCustomSpans = true
- }
-
- private companion object {
- val DefaultSpan: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan = { TvGridItemSpan(1) }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridInterval(
- override val key: ((index: Int) -> Any)?,
- val span: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan,
- override val type: ((index: Int) -> Any?),
- val item: @Composable TvLazyGridItemScope.(Int) -> Unit
-) : LazyLayoutIntervalContent.Interval
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyLayoutAnimateItemModifierNode.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyLayoutAnimateItemModifierNode.kt
deleted file mode 100644
index 6120c7a..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyLayoutAnimateItemModifierNode.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.VisibilityThreshold
-import androidx.compose.animation.core.spring
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.IntOffset
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.launch
-
-internal class LazyLayoutAnimateItemModifierNode(
- var placementAnimationSpec: FiniteAnimationSpec<IntOffset>
-) : Modifier.Node() {
-
- /**
- * Returns true when the placement animation is currently in progress so the parent should
- * continue composing this item.
- */
- var isAnimationInProgress by mutableStateOf(false)
- private set
-
- /**
- * This property is managed by the animation manager and is not directly used by this class. It
- * represents the last known offset of this item in the lazy layout coordinate space. It will be
- * updated on every scroll and is allowing the manager to track when the item position changes
- * not because of the scroll event in order to start the animation. When there is an active
- * animation it represents the final/target offset.
- */
- var rawOffset: IntOffset = NotInitialized
-
- private val placementDeltaAnimation = Animatable(IntOffset.Zero, IntOffset.VectorConverter)
-
- /**
- * Current delta to apply for a placement offset. Updates every animation frame. The settled
- * value is [IntOffset.Zero] so the animation is always targeting this value.
- */
- var placementDelta by mutableStateOf(IntOffset.Zero)
- private set
-
- /** Cancels the ongoing animation if there is one. */
- fun cancelAnimation() {
- if (isAnimationInProgress) {
- coroutineScope.launch {
- placementDeltaAnimation.snapTo(IntOffset.Zero)
- placementDelta = IntOffset.Zero
- isAnimationInProgress = false
- }
- }
- }
-
- /**
- * Tracks the offset of the item in the lookahead pass. When set, this is the animation target
- * that placementDelta should be applied to.
- */
- var lookaheadOffset: IntOffset = NotInitialized
-
- /** Animate the placement by the given [delta] offset. */
- fun animatePlacementDelta(delta: IntOffset) {
- val totalDelta = placementDelta - delta
- placementDelta = totalDelta
- isAnimationInProgress = true
- coroutineScope.launch {
- try {
- val spec =
- if (placementDeltaAnimation.isRunning) {
- // when interrupted, use the default spring, unless the spec is a spring.
- if (placementAnimationSpec is SpringSpec<IntOffset>) {
- placementAnimationSpec
- } else {
- InterruptionSpec
- }
- } else {
- placementAnimationSpec
- }
- if (!placementDeltaAnimation.isRunning) {
- // if not running we can snap to the initial value and animate to zero
- placementDeltaAnimation.snapTo(totalDelta)
- }
- // if animation is not currently running the target will be zero, otherwise
- // we have to continue the animation from the current value, but keep the needed
- // total delta for the new animation.
- val animationTarget = placementDeltaAnimation.value - totalDelta
- placementDeltaAnimation.animateTo(animationTarget, spec) {
- // placementDelta is calculated as if we always animate to target equal to zero
- placementDelta = value - animationTarget
- }
-
- isAnimationInProgress = false
- } catch (_: CancellationException) {
- // we don't reset inProgress in case of cancellation as it means
- // there is a new animation started which would reset it later
- }
- }
- }
-
- override fun onDetach() {
- placementDelta = IntOffset.Zero
- isAnimationInProgress = false
- rawOffset = NotInitialized
- // placementDeltaAnimation will be canceled because coroutineScope will be canceled.
- }
-
- companion object {
- val NotInitialized = IntOffset(Int.MAX_VALUE, Int.MAX_VALUE)
- }
-}
-
-/** We switch to this spec when a duration based animation is being interrupted. */
-private val InterruptionSpec =
- spring(
- stiffness = Spring.StiffnessMediumLow,
- visibilityThreshold = IntOffset.VisibilityThreshold
- )
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
deleted file mode 100644
index 4d9153c..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.semantics.CollectionInfo
-import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-internal fun rememberLazyGridSemanticState(
- state: TvLazyGridState,
- reverseScrolling: Boolean
-): LazyLayoutSemanticState =
- remember(state, reverseScrolling) {
- object : LazyLayoutSemanticState {
- override val currentPosition: Float
- get() = state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
-
- override val canScrollForward: Boolean
- get() = state.canScrollForward
-
- override suspend fun animateScrollBy(delta: Float) {
- state.animateScrollBy(delta)
- }
-
- override suspend fun scrollToItem(index: Int) {
- state.scrollToItem(index)
- }
-
- // TODO(popam): check if this is correct - it would be nice to provide correct columns
- override fun collectionInfo(): CollectionInfo =
- CollectionInfo(rowCount = -1, columnCount = -1)
- }
- }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt
deleted file mode 100644
index 6e850d5..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt
+++ /dev/null
@@ -1,75 +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.tv.foundation.lazy.grid
-
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-
-/**
- * Contains useful information about an individual item in lazy grids like [TvLazyVerticalGrid].
- *
- * @see TvLazyGridLayoutInfo
- */
-@Deprecated("Use `LazyGridItemInfo` instead.")
-sealed interface TvLazyGridItemInfo {
- /** The index of the item in the grid. */
- val index: Int
-
- /** The key of the item which was passed to the item() or items() function. */
- val key: Any
-
- /**
- * The offset of the item in pixels. It is relative to the top start of the lazy grid container.
- */
- val offset: IntOffset
-
- /**
- * The row occupied by the top start point of the item. If this is unknown, for example while
- * this item is animating to exit the viewport and is still visible, the value will be
- * [UnknownRow].
- */
- val row: Int
-
- /**
- * The column occupied by the top start point of the item. If this is unknown, for example while
- * this item is animating to exit the viewport and is still visible, the value will be
- * [UnknownColumn].
- */
- val column: Int
-
- /**
- * The pixel size of the item. Note that if you emit multiple layouts in the composable slot for
- * the item then this size will be calculated as the max of their sizes.
- */
- val size: IntSize
-
- /** The content type of the item which was passed to the item() or items() function. */
- val contentType: Any?
-
- companion object {
- /**
- * Possible value for [row], when they are unknown. This can happen when the item is visible
- * while animating to exit the viewport.
- */
- const val UnknownRow = -1
- /**
- * Possible value for [column], when they are unknown. This can happen when the item is
- * visible while animating to exit the viewport.
- */
- const val UnknownColumn = -1
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt
deleted file mode 100644
index 90c4ae6..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt
+++ /dev/null
@@ -1,53 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.VisibilityThreshold
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.IntOffset
-
-/** Receiver scope being used by the item content parameter of [TvLazyVerticalGrid]. */
-@Deprecated("Use `LazyGridItemScope` instead.")
-@Stable
-@TvLazyGridScopeMarker
-sealed interface TvLazyGridItemScope {
- /**
- * This modifier animates the item placement within the Lazy grid.
- *
- * When you provide a key via [TvLazyGridScope.item]/[TvLazyGridScope.items] this modifier will
- * enable item reordering animations. Aside from item reordering all other position changes
- * caused by events like arrangement or alignment changes will also be animated.
- *
- * @param animationSpec a finite animation that will be used to animate the item placement.
- */
- @Deprecated("Use `LazyGridItemScope.animateItemPlacement` instead.")
- @ExperimentalFoundationApi
- fun Modifier.animateItemPlacement(
- animationSpec: FiniteAnimationSpec<IntOffset> =
- spring(
- stiffness = Spring.StiffnessMediumLow,
- visibilityThreshold = IntOffset.VisibilityThreshold
- )
- ): Modifier
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt
deleted file mode 100644
index 393aef5..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.ParentDataModifierNode
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-
-@Suppress("OVERRIDE_DEPRECATION")
-@OptIn(ExperimentalFoundationApi::class)
-internal object TvLazyGridItemScopeImpl : TvLazyGridItemScope {
- @ExperimentalFoundationApi
- override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
- this then AnimateItemPlacementElement(animationSpec)
-}
-
-private class AnimateItemPlacementElement(val animationSpec: FiniteAnimationSpec<IntOffset>) :
- ModifierNodeElement<AnimateItemPlacementNode>() {
-
- override fun create(): AnimateItemPlacementNode = AnimateItemPlacementNode(animationSpec)
-
- override fun update(node: AnimateItemPlacementNode) {
- node.delegatingNode.placementAnimationSpec = animationSpec
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is AnimateItemPlacementElement) return false
- return animationSpec != other.animationSpec
- }
-
- override fun hashCode(): Int {
- return animationSpec.hashCode()
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "animateItemPlacement"
- value = animationSpec
- }
-}
-
-private class AnimateItemPlacementNode(animationSpec: FiniteAnimationSpec<IntOffset>) :
- DelegatingNode(), ParentDataModifierNode {
-
- val delegatingNode = delegate(LazyLayoutAnimateItemModifierNode(animationSpec))
-
- override fun Density.modifyParentData(parentData: Any?): Any = delegatingNode
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt
deleted file mode 100644
index b4e4a19..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt
+++ /dev/null
@@ -1,84 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
-
-/**
- * Contains useful information about the currently displayed layout state of lazy grids like
- * [TvLazyVerticalGrid]. For example you can get the list of currently displayed items.
- *
- * Use [TvLazyGridState.layoutInfo] to retrieve this
- */
-@Deprecated("Use `LazyGridLayoutInfo` instead.")
-sealed interface TvLazyGridLayoutInfo {
- /** The list of [TvLazyGridItemInfo] representing all the currently visible items. */
- val visibleItemsInfo: List<TvLazyGridItemInfo>
-
- /**
- * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
- * which would be visible. Usually it is 0, but it can be negative if non-zero
- * [beforeContentPadding] was applied as the content displayed in the content padding area is
- * still visible.
- *
- * You can use it to understand what items from [visibleItemsInfo] are fully visible.
- */
- val viewportStartOffset: Int
-
- /**
- * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
- * which would be visible. It is the size of the lazy grid layout minus [beforeContentPadding].
- *
- * You can use it to understand what items from [visibleItemsInfo] are fully visible.
- */
- val viewportEndOffset: Int
-
- /** The total count of items passed to [TvLazyVerticalGrid]. */
- val totalItemsCount: Int
-
- /**
- * The size of the viewport in pixels. It is the lazy grid layout size including all the content
- * paddings.
- */
- val viewportSize: IntSize
-
- /** The orientation of the lazy grid. */
- val orientation: Orientation
-
- /** True if the direction of scrolling and layout is reversed. */
- val reverseLayout: Boolean
-
- /**
- * The content padding in pixels applied before the first row/column in the direction of
- * scrolling. For example it is a top content padding for LazyVerticalGrid with reverseLayout
- * set to false.
- */
- val beforeContentPadding: Int
-
- /**
- * The content padding in pixels applied after the last row/column in the direction of
- * scrolling. For example it is a bottom content padding for LazyVerticalGrid with reverseLayout
- * set to false.
- */
- val afterContentPadding: Int
-
- /** The spacing between lines in the direction of scrolling. */
- val mainAxisItemSpacing: Int
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt
deleted file mode 100644
index 47e316f..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.unit.IntSize
-
-/** The result of the measure pass for lazy list layout. */
-@OptIn(ExperimentalFoundationApi::class)
-internal class TvLazyGridMeasureResult(
- // properties defining the scroll position:
- /** The new first visible line of items. */
- val firstVisibleLine: LazyGridMeasuredLine?,
- /** The new value for [TvLazyGridState.firstVisibleItemScrollOffset]. */
- val firstVisibleLineScrollOffset: Int,
- /** True if there is some space available to continue scrolling in the forward direction. */
- val canScrollForward: Boolean,
- /** The amount of scroll consumed during the measure pass. */
- val consumedScroll: Float,
- /** MeasureResult defining the layout. */
- measureResult: MeasureResult,
- // properties representing the info needed for LazyListLayoutInfo:
- /** see [TvLazyGridLayoutInfo.visibleItemsInfo] */
- override val visibleItemsInfo: List<TvLazyGridItemInfo>,
- /** see [TvLazyGridLayoutInfo.viewportStartOffset] */
- override val viewportStartOffset: Int,
- /** see [TvLazyGridLayoutInfo.viewportEndOffset] */
- override val viewportEndOffset: Int,
- /** see [TvLazyGridLayoutInfo.totalItemsCount] */
- override val totalItemsCount: Int,
- /** see [TvLazyGridLayoutInfo.reverseLayout] */
- override val reverseLayout: Boolean,
- /** see [TvLazyGridLayoutInfo.orientation] */
- override val orientation: Orientation,
- /** see [TvLazyGridLayoutInfo.afterContentPadding] */
- override val afterContentPadding: Int,
- /** see [TvLazyGridLayoutInfo.mainAxisItemSpacing] */
- override val mainAxisItemSpacing: Int
-) : TvLazyGridLayoutInfo, MeasureResult by measureResult {
- override val viewportSize: IntSize
- get() = IntSize(width, height)
-
- override val beforeContentPadding: Int
- get() = -viewportStartOffset
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
deleted file mode 100644
index 9f1d7c8..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.grid
-
-/** DSL marker used to distinguish between lazy grid dsl scope and the item content scope. */
-@Deprecated("No longer needed as TvLazyHorizontalGrid and TvLazyVerticalGrid are deprecated.")
-@DslMarker
-annotation class TvLazyGridScopeMarker
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
deleted file mode 100644
index 810c1ce..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
+++ /dev/null
@@ -1,449 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
-import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.layout.Remeasurement
-import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastForEach
-import androidx.tv.foundation.lazy.layout.AwaitFirstLayoutModifier
-import androidx.tv.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
-import androidx.tv.foundation.lazy.layout.animateScrollToItem
-import kotlin.math.abs
-
-/**
- * Creates a [TvLazyGridState] that is remembered across compositions.
- *
- * Changes to the provided initial values will **not** result in the state being recreated or
- * changed in any way if it has already been created.
- *
- * @param initialFirstVisibleItemIndex the initial value for [TvLazyGridState.firstVisibleItemIndex]
- * @param initialFirstVisibleItemScrollOffset the initial value for
- * [TvLazyGridState.firstVisibleItemScrollOffset]
- */
-@Deprecated(
- "Use `rememberLazyGridState` instead.",
- replaceWith =
- ReplaceWith(
- "rememberLazyGridState(" +
- "initialFirstVisibleItemIndex = initialFirstVisibleItemIndex, " +
- "initialFirstVisibleItemScrollOffset = initialFirstVisibleItemScrollOffset" +
- ")",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.rememberLazyGridState")
- )
-)
-@Composable
-fun rememberTvLazyGridState(
- initialFirstVisibleItemIndex: Int = 0,
- initialFirstVisibleItemScrollOffset: Int = 0
-): TvLazyGridState {
- return rememberSaveable(saver = TvLazyGridState.Saver) {
- TvLazyGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset)
- }
-}
-
-/**
- * A state object that can be hoisted to control and observe scrolling.
- *
- * In most cases, this will be created via [rememberTvLazyGridState].
- *
- * @param firstVisibleItemIndex the initial value for [TvLazyGridState.firstVisibleItemIndex]
- * @param firstVisibleItemScrollOffset the initial value for
- * [TvLazyGridState.firstVisibleItemScrollOffset]
- */
-@Deprecated(
- "Use `LazyGridState` instead.",
- replaceWith =
- ReplaceWith(
- "LazyGridState(" +
- "firstVisibleItemIndex = firstVisibleItemIndex, " +
- "firstVisibleItemScrollOffset = firstVisibleItemScrollOffset" +
- ")",
- imports = arrayOf("androidx.compose.foundation.lazy.grid.LazyGridState")
- )
-)
-@OptIn(ExperimentalFoundationApi::class)
-@Stable
-class TvLazyGridState
-constructor(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0) :
- ScrollableState {
- /** The holder class for the current scroll position. */
- private val scrollPosition =
- LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
-
- /**
- * The index of the first item that is visible.
- *
- * Note that this property is observable and if you use it in the composable function it will be
- * recomposed on every change causing potential performance issues.
- */
- val firstVisibleItemIndex: Int
- get() = scrollPosition.index
-
- /**
- * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount
- * that the item is offset backwards
- */
- val firstVisibleItemScrollOffset: Int
- get() = scrollPosition.scrollOffset
-
- /** Backing state for [layoutInfo] */
- private val layoutInfoState = mutableStateOf<TvLazyGridLayoutInfo>(EmptyTvLazyGridLayoutInfo)
-
- /**
- * The object of [TvLazyGridLayoutInfo] calculated during the last layout pass. For example, you
- * can use it to calculate what items are currently visible.
- *
- * Note that this property is observable and is updated after every scroll or remeasure. If you
- * use it in the composable function it will be recomposed on every change causing potential
- * performance issues including infinity recomposition loop. Therefore, avoid using it in the
- * composition.
- */
- val layoutInfo: TvLazyGridLayoutInfo
- get() = layoutInfoState.value
-
- /**
- * [InteractionSource] that will be used to dispatch drag events when this grid is being
- * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
- * [isScrollInProgress].
- */
- val interactionSource: InteractionSource
- get() = internalInteractionSource
-
- internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
-
- /**
- * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
- * - that is, it is the amount that the items are offset in y
- */
- internal var scrollToBeConsumed = 0f
- private set
-
- /** Needed for [animateScrollToItem]. Updated on every measure. */
- internal var slotsPerLine: Int by mutableIntStateOf(0)
-
- /** Needed for [animateScrollToItem]. Updated on every measure. */
- internal var density: Density = Density(1f, 1f)
-
- /** Needed for [notifyPrefetch]. */
- internal var isVertical: Boolean = true
-
- /**
- * The ScrollableController instance. We keep it as we need to call stopAnimation on it once we
- * reached the end of the grid.
- */
- private val scrollableState = ScrollableState { -onScroll(-it) }
-
- /** Only used for testing to confirm that we're not making too many measure passes */
- /*@VisibleForTesting*/
- internal var numMeasurePasses: Int = 0
- private set
-
- /** Only used for testing to disable prefetching when needed to test the main logic. */
- /*@VisibleForTesting*/
- internal var prefetchingEnabled: Boolean = true
-
- /**
- * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
- */
- private var lineToPrefetch = -1
-
- /** The list of handles associated with the items from the [lineToPrefetch] line. */
- private val currentLinePrefetchHandles =
- mutableVectorOf<LazyLayoutPrefetchState.PrefetchHandle>()
-
- /**
- * Keeps the scrolling direction during the previous calculation in order to be able to detect
- * the scrolling direction change.
- */
- private var wasScrollingForward = false
-
- /**
- * The [Remeasurement] object associated with our layout. It allows us to remeasure
- * synchronously during scroll.
- */
- internal var remeasurement: Remeasurement? = null
-
- /** The modifier which provides [remeasurement]. */
- internal val remeasurementModifier =
- object : RemeasurementModifier {
- override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
- [email protected] = remeasurement
- }
- }
-
- /**
- * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is
- * ready.
- */
- internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
-
- /** Finds items on a line and their measurement constraints. Used for prefetching. */
- internal var prefetchInfoRetriever: (line: Int) -> List<Pair<Int, Constraints>> by
- mutableStateOf({ emptyList() })
-
- internal val placementAnimator = LazyGridItemPlacementAnimator()
-
- internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
-
- private val animateScrollScope = LazyGridAnimateScrollScope(this)
-
- /** Stores currently pinned items which are always composed. */
- internal val pinnedItems = LazyLayoutPinnedItemList()
-
- internal val nearestRange: IntRange by scrollPosition.nearestRangeState
-
- /**
- * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
- * pixels.
- *
- * @param index the index to which to scroll. Must be non-negative.
- * @param scrollOffset the offset that the item should end up after the scroll. Note that
- * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
- * scroll the item further upward (taking it partly offscreen).
- */
- suspend fun scrollToItem(
- /*@IntRange(from = 0)*/
- index: Int,
- scrollOffset: Int = 0
- ) {
- scroll { snapToItemIndexInternal(index, scrollOffset) }
- }
-
- internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
- scrollPosition.requestPosition(index, scrollOffset)
- // placement animation is not needed because we snap into a new position.
- placementAnimator.reset()
- remeasurement?.forceRemeasure()
- }
-
- /**
- * Call this function to take control of scrolling and gain the ability to send scroll events
- * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
- * performed within a [scroll] block (even if they don't call any other methods on this object)
- * in order to guarantee that mutual exclusion is enforced.
- *
- * If [scroll] is called from elsewhere, this will be canceled.
- */
- override suspend fun scroll(
- scrollPriority: MutatePriority,
- block: suspend ScrollScope.() -> Unit
- ) {
- awaitLayoutModifier.waitForFirstLayout()
- scrollableState.scroll(scrollPriority, block)
- }
-
- override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
-
- override val isScrollInProgress: Boolean
- get() = scrollableState.isScrollInProgress
-
- override var canScrollForward: Boolean by mutableStateOf(false)
- private set
-
- override var canScrollBackward: Boolean by mutableStateOf(false)
- private set
-
- // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
- // fine-grained control over scrolling
- /*@VisibleForTesting*/
- internal fun onScroll(distance: Float): Float {
- if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
- return 0f
- }
- check(abs(scrollToBeConsumed) <= 0.5f) {
- "entered drag with non-zero pending scroll: $scrollToBeConsumed"
- }
- scrollToBeConsumed += distance
-
- // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
- // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
- // we have less than 0.5 pixels
- if (abs(scrollToBeConsumed) > 0.5f) {
- val preScrollToBeConsumed = scrollToBeConsumed
- remeasurement?.forceRemeasure()
- if (prefetchingEnabled) {
- notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
- }
- }
-
- // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
- if (abs(scrollToBeConsumed) <= 0.5f) {
- // We consumed all of it - we'll hold onto the fractional scroll for later, so report
- // that we consumed the whole thing
- return distance
- } else {
- val scrollConsumed = distance - scrollToBeConsumed
- // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
- // nested scrolling)
- scrollToBeConsumed = 0f // We're not consuming the rest, give it back
- return scrollConsumed
- }
- }
-
- private fun notifyPrefetch(delta: Float) {
- val prefetchState = prefetchState
- if (!prefetchingEnabled) {
- return
- }
- val info = layoutInfo
- if (info.visibleItemsInfo.isNotEmpty()) {
- val scrollingForward = delta < 0
- val lineToPrefetch: Int
- val closestNextItemToPrefetch: Int
- if (scrollingForward) {
- lineToPrefetch =
- 1 + info.visibleItemsInfo.last().let { if (isVertical) it.row else it.column }
- closestNextItemToPrefetch = info.visibleItemsInfo.last().index + 1
- } else {
- lineToPrefetch =
- -1 + info.visibleItemsInfo.first().let { if (isVertical) it.row else it.column }
- closestNextItemToPrefetch = info.visibleItemsInfo.first().index - 1
- }
- if (
- lineToPrefetch != this.lineToPrefetch &&
- closestNextItemToPrefetch in 0 until info.totalItemsCount
- ) {
- if (wasScrollingForward != scrollingForward) {
- // the scrolling direction has been changed which means the last prefetched
- // is not going to be reached anytime soon so it is safer to dispose it.
- // if this line is already visible it is safe to call the method anyway
- // as it will be no-op
- currentLinePrefetchHandles.forEach { it.cancel() }
- }
- this.wasScrollingForward = scrollingForward
- this.lineToPrefetch = lineToPrefetch
- currentLinePrefetchHandles.clear()
- prefetchInfoRetriever(lineToPrefetch).fastForEach {
- currentLinePrefetchHandles.add(
- prefetchState.schedulePrefetch(it.first, it.second)
- )
- }
- }
- }
- }
-
- private fun cancelPrefetchIfVisibleItemsChanged(info: TvLazyGridLayoutInfo) {
- if (lineToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
- val expectedLineToPrefetch =
- if (wasScrollingForward) {
- info.visibleItemsInfo.last().let { if (isVertical) it.row else it.column } + 1
- } else {
- info.visibleItemsInfo.first().let { if (isVertical) it.row else it.column } - 1
- }
- if (lineToPrefetch != expectedLineToPrefetch) {
- lineToPrefetch = -1
- currentLinePrefetchHandles.forEach { it.cancel() }
- currentLinePrefetchHandles.clear()
- }
- }
- }
-
- internal val prefetchState = LazyLayoutPrefetchState()
-
- /**
- * Animate (smooth scroll) to the given item.
- *
- * @param index the index to which to scroll. Must be non-negative.
- * @param scrollOffset the offset that the item should end up after the scroll. Note that
- * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
- * scroll the item further upward (taking it partly offscreen).
- */
- suspend fun animateScrollToItem(
- /*@IntRange(from = 0)*/
- index: Int,
- scrollOffset: Int = 0
- ) {
- animateScrollScope.animateScrollToItem(index, scrollOffset)
- }
-
- /** Updates the state with the new calculated scroll position and consumed scroll. */
- internal fun applyMeasureResult(result: TvLazyGridMeasureResult) {
- scrollPosition.updateFromMeasureResult(result)
- scrollToBeConsumed -= result.consumedScroll
- layoutInfoState.value = result
-
- canScrollForward = result.canScrollForward
- canScrollBackward =
- (result.firstVisibleLine?.index ?: 0) != 0 || result.firstVisibleLineScrollOffset != 0
-
- numMeasurePasses++
-
- cancelPrefetchIfVisibleItemsChanged(result)
- }
-
- /**
- * When the user provided custom keys for the items we can try to detect when there were items
- * added or removed before our current first visible item and keep this item as the first
- * visible one even given that its index has been changed.
- */
- internal fun updateScrollPositionIfTheFirstItemWasMoved(
- itemProvider: LazyGridItemProvider,
- firstItemIndex: Int = Snapshot.withoutReadObservation { scrollPosition.index }
- ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
-
- companion object {
- /** The default [Saver] implementation for [TvLazyGridState]. */
- val Saver: Saver<TvLazyGridState, *> =
- listSaver(
- save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
- restore = {
- TvLazyGridState(
- firstVisibleItemIndex = it[0],
- firstVisibleItemScrollOffset = it[1]
- )
- }
- )
- }
-}
-
-private object EmptyTvLazyGridLayoutInfo : TvLazyGridLayoutInfo {
- override val visibleItemsInfo = emptyList<TvLazyGridItemInfo>()
- override val viewportStartOffset = 0
- override val viewportEndOffset = 0
- override val totalItemsCount = 0
- override val viewportSize = IntSize.Zero
- override val orientation = Orientation.Vertical
- override val reverseLayout = false
- override val beforeContentPadding: Int = 0
- override val afterContentPadding: Int = 0
- override val mainAxisItemSpacing: Int = 0
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier.kt
deleted file mode 100644
index acf6806..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/AwaitFirstLayoutModifier.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.layout
-
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnGloballyPositionedModifier
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-/**
- * Internal modifier which allows to delay some interactions (e.g. scroll) until layout is ready.
- */
-internal class AwaitFirstLayoutModifier : OnGloballyPositionedModifier {
- private var wasPositioned = false
- private var continuation: Continuation<Unit>? = null
-
- suspend fun waitForFirstLayout() {
- if (!wasPositioned) {
- val oldContinuation = continuation
- suspendCoroutine { continuation = it }
- oldContinuation?.resume(Unit)
- }
- }
-
- override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
- if (!wasPositioned) {
- wasPositioned = true
- continuation?.resume(Unit)
- continuation = null
- }
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
deleted file mode 100644
index a218e82..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
+++ /dev/null
@@ -1,266 +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.tv.foundation.lazy.layout
-
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.copy
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
-import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.abs
-
-private class ItemFoundInScroll(
- val itemOffset: Int,
- val previousAnimation: AnimationState<Float, AnimationVector1D>
-) : CancellationException()
-
-private val TargetDistance = 2500.dp
-private val BoundDistance = 1500.dp
-private val MinimumDistance = 50.dp
-
-private const val DEBUG = false
-
-private inline fun debugLog(generateMsg: () -> String) {
- if (DEBUG) {
- println("LazyScrolling: ${generateMsg()}")
- }
-}
-
-/**
- * Abstraction over animated scroll for using [animateScrollToItem] in different layouts.
- * todo(b/243786897): revisit this API and make it public
- */
-internal interface LazyAnimateScrollScope {
- val density: Density
-
- val firstVisibleItemIndex: Int
-
- val firstVisibleItemScrollOffset: Int
-
- val lastVisibleItemIndex: Int
-
- val itemCount: Int
-
- fun getTargetItemOffset(index: Int): Int?
-
- fun ScrollScope.snapToItem(index: Int, scrollOffset: Int)
-
- fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float
-
- /** defines min number of items that forces scroll to snap if animation did not reach it */
- val numOfItemsForTeleport: Int
-
- suspend fun scroll(block: suspend ScrollScope.() -> Unit)
-}
-
-internal suspend fun LazyAnimateScrollScope.animateScrollToItem(
- index: Int,
- scrollOffset: Int,
-) {
- scroll {
- require(index >= 0f) { "Index should be non-negative ($index)" }
- try {
- val targetDistancePx = with(density) { TargetDistance.toPx() }
- val boundDistancePx = with(density) { BoundDistance.toPx() }
- val minDistancePx = with(density) { MinimumDistance.toPx() }
- var loop = true
- var anim = AnimationState(0f)
- val targetItemInitialOffset = getTargetItemOffset(index)
- if (targetItemInitialOffset != null) {
- // It's already visible, just animate directly
- throw ItemFoundInScroll(targetItemInitialOffset, anim)
- }
- val forward = index > firstVisibleItemIndex
-
- fun isOvershot(): Boolean {
- // Did we scroll past the item?
- @Suppress("RedundantIf") // It's way easier to understand the logic this way
- return if (forward) {
- if (firstVisibleItemIndex > index) {
- true
- } else if (
- firstVisibleItemIndex == index &&
- firstVisibleItemScrollOffset > scrollOffset
- ) {
- true
- } else {
- false
- }
- } else { // backward
- if (firstVisibleItemIndex < index) {
- true
- } else if (
- firstVisibleItemIndex == index &&
- firstVisibleItemScrollOffset < scrollOffset
- ) {
- true
- } else {
- false
- }
- }
- }
-
- var loops = 1
- while (loop && itemCount > 0) {
- val expectedDistance = expectedDistanceTo(index, scrollOffset)
- val target =
- if (abs(expectedDistance) < targetDistancePx) {
- val absTargetPx = maxOf(abs(expectedDistance), minDistancePx)
- if (forward) absTargetPx else -absTargetPx
- } else {
- if (forward) targetDistancePx else -targetDistancePx
- }
-
- debugLog {
- "Scrolling to index=$index offset=$scrollOffset from " +
- "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
- " calculated target=$target"
- }
-
- anim = anim.copy(value = 0f)
- var prevValue = 0f
- anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
- // If we haven't found the item yet, check if it's visible.
- var targetItemOffset = getTargetItemOffset(index)
-
- if (targetItemOffset == null) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue =
- if (target > 0) {
- value.coerceAtMost(target)
- } else {
- value.coerceAtLeast(target)
- }
- val delta = coercedValue - prevValue
- debugLog {
- "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
- }
-
- val consumed = scrollBy(delta)
- targetItemOffset = getTargetItemOffset(index)
- if (targetItemOffset != null) {
- debugLog { "Found the item after performing scrollBy()" }
- } else if (!isOvershot()) {
- if (delta != consumed) {
- debugLog { "Hit end without finding the item" }
- cancelAnimation()
- loop = false
- return@animateTo
- }
- prevValue += delta
- if (forward) {
- if (value > boundDistancePx) {
- debugLog { "Struck bound going forward" }
- cancelAnimation()
- }
- } else {
- if (value < -boundDistancePx) {
- debugLog { "Struck bound going backward" }
- cancelAnimation()
- }
- }
-
- if (forward) {
- if (
- loops >= 2 &&
- index - lastVisibleItemIndex > numOfItemsForTeleport
- ) {
- // Teleport
- debugLog { "Teleport forward" }
- snapToItem(
- index = index - numOfItemsForTeleport,
- scrollOffset = 0
- )
- }
- } else {
- if (
- loops >= 2 &&
- firstVisibleItemIndex - index > numOfItemsForTeleport
- ) {
- // Teleport
- debugLog { "Teleport backward" }
- snapToItem(
- index = index + numOfItemsForTeleport,
- scrollOffset = 0
- )
- }
- }
- }
- }
-
- // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
- // the final position, there's no need to animate to it.
- if (isOvershot()) {
- debugLog { "Overshot" }
- snapToItem(index = index, scrollOffset = scrollOffset)
- loop = false
- cancelAnimation()
- return@animateTo
- } else if (targetItemOffset != null) {
- debugLog { "Found item" }
- throw ItemFoundInScroll(targetItemOffset, anim)
- }
- }
-
- loops++
- }
- } catch (itemFound: ItemFoundInScroll) {
- // We found it, animate to it
- // Bring to the requested position - will be automatically stopped if not possible
- val anim = itemFound.previousAnimation.copy(value = 0f)
- val target = (itemFound.itemOffset + scrollOffset).toFloat()
- var prevValue = 0f
- debugLog { "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}" }
- anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue =
- when {
- target > 0 -> {
- value.coerceAtMost(target)
- }
- target < 0 -> {
- value.coerceAtLeast(target)
- }
- else -> {
- debugLog {
- "WARNING: somehow ended up seeking 0px, this shouldn't happen"
- }
- 0f
- }
- }
- val delta = coercedValue - prevValue
- debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
- val consumed = scrollBy(delta)
- if (
- delta != consumed /* hit the end, stop */ ||
- coercedValue != value /* would have overshot, stop */
- ) {
- cancelAnimation()
- }
- prevValue += delta
- }
- // Once we're finished the animation, snap to the exact position to account for
- // rounding error (otherwise we tend to end up with the previous item scrolled the
- // tiniest bit onscreen)
- // TODO: prevent temporarily scrolling *past* the item
- snapToItem(index = index, scrollOffset = scrollOffset)
- }
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo.kt
deleted file mode 100644
index 6474ec5..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutBeyondBoundsInfo.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.layout
-
-import androidx.compose.runtime.collection.mutableVectorOf
-
-/**
- * This data structure is used to save information about the number of "beyond bounds items" that we
- * want to compose. These items are not within the visible bounds of the lazy layout, but we compose
- * them because they are explicitly requested through the
- * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout].
- *
- * When the lazy layout receives a [layout][androidx.compose.ui.layout.BeyondBoundsLayout.layout]
- * request to layout items beyond visible bounds, it creates an instance of
- * [LazyLayoutBeyondBoundsInfo.Interval] by using the [addInterval] function. This returns the
- * interval of items that are currently composed, and we can request other intervals to control the
- * number of beyond bounds items.
- *
- * There can be multiple intervals created at the same time, and [LazyLayoutBeyondBoundsInfo] merges
- * all the intervals to calculate the effective beyond bounds items.
- *
- * The [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout] is designed to be
- * synchronous, so once you are done using the items, call [removeInterval] to remove the extra
- * items you had requested.
- *
- * Note that when you clear an interval, the items in that interval might not be cleared right away
- * if another interval was created that has the same items. This is done to support two use cases:
- * 1. To allow items to be pinned while they are being scrolled into view.
- * 2. To allow users to call [layout][androidx.compose.ui.layout.BeyondBoundsLayout.layout] from
- * within the completion block of another layout call.
- */
-internal class LazyLayoutBeyondBoundsInfo {
- private val beyondBoundsItems = mutableVectorOf<Interval>()
-
- /**
- * Create a beyond bounds interval. This can be used to specify which composed items we want to
- * retain. For instance, it can be used to force the measuring of items that are beyond the
- * visible bounds of a lazy list.
- *
- * @param start The starting index (inclusive) for this interval.
- * @param end The ending index (inclusive) for this interval.
- * @return An interval that specifies which items we want to retain.
- */
- fun addInterval(start: Int, end: Int): Interval {
- return Interval(start, end).apply { beyondBoundsItems.add(this) }
- }
-
- /** Clears the specified interval. Use this to remove the interval created by [addInterval]. */
- fun removeInterval(interval: Interval) {
- beyondBoundsItems.remove(interval)
- }
-
- /** Returns true if there are beyond bounds intervals. */
- fun hasIntervals(): Boolean = beyondBoundsItems.isNotEmpty()
-
- /** The effective start index after merging all the current intervals. */
- val start: Int
- get() {
- var minIndex = beyondBoundsItems.first().start
- beyondBoundsItems.forEach {
- if (it.start < minIndex) {
- minIndex = it.start
- }
- }
- require(minIndex >= 0) { "negative minIndex" }
- return minIndex
- }
-
- /** The effective end index after merging all the current intervals. */
- val end: Int
- get() {
- var maxIndex = beyondBoundsItems.first().end
- beyondBoundsItems.forEach {
- if (it.end > maxIndex) {
- maxIndex = it.end
- }
- }
- return maxIndex
- }
-
- /** The Interval used to implement [LazyLayoutBeyondBoundsInfo]. */
- internal data class Interval(
- /** The start index for the interval. */
- val start: Int,
-
- /** The end index for the interval. */
- val end: Int
- ) {
- init {
- require(start >= 0) { "negative start index" }
- require(end >= start) { "end index greater than start" }
- }
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt
deleted file mode 100644
index 3e81463..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt
+++ /dev/null
@@ -1,112 +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.tv.foundation.lazy.layout
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-
-/**
- * A key-index mapping used inside the [LazyLayoutItemProvider]. It might not contain all items in
- * the lazy layout as optimization, but it must cover items the provider is requesting during layout
- * pass. See [NearestRangeKeyIndexMap] as sample implementation that samples items near current
- * viewport.
- */
-internal interface LazyLayoutKeyIndexMap {
- /** @return current index for given [key] or `-1` if not found. */
- fun getIndex(key: Any): Int
-
- /** @return key for a given [index] if it is known, or null otherwise. */
- fun getKey(index: Int): Any?
-
- /** Empty map implementation, always returning `-1` for any key. */
- companion object Empty : LazyLayoutKeyIndexMap {
- @Suppress("AutoBoxing") override fun getIndex(key: Any): Int = -1
-
- override fun getKey(index: Int) = null
- }
-}
-
-/**
- * Implementation of [LazyLayoutKeyIndexMap] indexing over given [IntRange] of items. Items outside
- * of given range are considered unknown, with null returned as the index.
- */
-@ExperimentalFoundationApi
-internal class NearestRangeKeyIndexMap(
- nearestRange: IntRange,
- intervalContent: LazyLayoutIntervalContent<*>
-) : LazyLayoutKeyIndexMap {
- private val map: Map<Any, Int>
- private val keys: Array<Any?>
- private val keysStartIndex: Int
-
- init {
- // Traverses the interval [list] in order to create a mapping from the key to the index for
- // all the indexes in the passed [range].
- val list = intervalContent.intervals
- val first = nearestRange.first
- check(first >= 0) { "negative nearestRange.first" }
- val last = minOf(nearestRange.last, list.size - 1)
- if (last < first) {
- map = emptyMap()
- keys = emptyArray()
- keysStartIndex = 0
- } else {
- keys = arrayOfNulls<Any?>(last - first + 1)
- keysStartIndex = first
- map =
- hashMapOf<Any, Int>().also { map ->
- list.forEach(
- fromIndex = first,
- toIndex = last,
- ) {
- val keyFactory = it.value.key
- val start = maxOf(first, it.startIndex)
- val end = minOf(last, it.startIndex + it.size - 1)
- for (i in start..end) {
- val key =
- keyFactory?.invoke(i - it.startIndex) ?: getDefaultLazyLayoutKey(i)
- map[key] = i
- keys[i - keysStartIndex] = key
- }
- }
- }
- }
- }
-
- override fun getIndex(key: Any): Int = map.getOrElse(key) { -1 }
-
- override fun getKey(index: Int) = keys.getOrElse(index - keysStartIndex) { null }
-}
-
-/**
- * Returns a range of indexes which contains at least [extraItemCount] items near the first visible
- * item. It is optimized to return the same range for small changes in the firstVisibleItem value so
- * we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(
- firstVisibleItem: Int,
- slidingWindowSize: Int,
- extraItemCount: Int
-): IntRange {
- val slidingWindowStart = slidingWindowSize * (firstVisibleItem / slidingWindowSize)
-
- val start = maxOf(slidingWindowStart - extraItemCount, 0)
- val end = slidingWindowStart + slidingWindowSize + extraItemCount
- return start until end
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState.kt
deleted file mode 100644
index f4469db..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutNearestRangeState.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.layout
-
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.structuralEqualityPolicy
-
-internal class LazyLayoutNearestRangeState(
- firstVisibleItem: Int,
- private val slidingWindowSize: Int,
- private val extraItemCount: Int
-) : State<IntRange> {
-
- override var value: IntRange by
- mutableStateOf(
- calculateNearestItemsRange(firstVisibleItem, slidingWindowSize, extraItemCount),
- structuralEqualityPolicy()
- )
- private set
-
- private var lastFirstVisibleItem = firstVisibleItem
-
- fun update(firstVisibleItem: Int) {
- if (firstVisibleItem != lastFirstVisibleItem) {
- lastFirstVisibleItem = firstVisibleItem
- value = calculateNearestItemsRange(firstVisibleItem, slidingWindowSize, extraItemCount)
- }
- }
-
- private companion object {
- /**
- * Returns a range of indexes which contains at least [extraItemCount] items near the first
- * visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
- private fun calculateNearestItemsRange(
- firstVisibleItem: Int,
- slidingWindowSize: Int,
- extraItemCount: Int
- ): IntRange {
- val slidingWindowStart = slidingWindowSize * (firstVisibleItem / slidingWindowSize)
-
- val start = maxOf(slidingWindowStart - extraItemCount, 0)
- val end = slidingWindowStart + slidingWindowSize + extraItemCount
- return start until end
- }
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
deleted file mode 100644
index 6261e9a..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ /dev/null
@@ -1,187 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.layout
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.semantics.CollectionInfo
-import androidx.compose.ui.semantics.ScrollAxisRange
-import androidx.compose.ui.semantics.collectionInfo
-import androidx.compose.ui.semantics.horizontalScrollAxisRange
-import androidx.compose.ui.semantics.indexForKey
-import androidx.compose.ui.semantics.isTraversalGroup
-import androidx.compose.ui.semantics.scrollBy
-import androidx.compose.ui.semantics.scrollToIndex
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.verticalScrollAxisRange
-import androidx.tv.foundation.lazy.list.TvLazyListState
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalFoundationApi::class)
-@Suppress("ComposableModifierFactory")
-@Composable
-internal fun Modifier.lazyLayoutSemantics(
- itemProviderLambda: () -> LazyLayoutItemProvider,
- state: LazyLayoutSemanticState,
- orientation: Orientation,
- userScrollEnabled: Boolean,
- reverseScrolling: Boolean
-): Modifier {
- val coroutineScope = rememberCoroutineScope()
- return this.then(
- remember(itemProviderLambda, state, orientation, userScrollEnabled) {
- val isVertical = orientation == Orientation.Vertical
- val indexForKeyMapping: (Any) -> Int = { needle ->
- val itemProvider = itemProviderLambda()
- var result = -1
- for (index in 0 until itemProvider.itemCount) {
- if (itemProvider.getKey(index) == needle) {
- result = index
- break
- }
- }
- result
- }
-
- val accessibilityScrollState =
- ScrollAxisRange(
- value = {
- // This is a simple way of representing the current position without
- // needing any lazy items to be measured. It's good enough so far, because
- // screen-readers care mostly about whether scroll position changed or not
- // rather than the actual offset in pixels.
- state.currentPosition
- },
- maxValue = {
- val itemProvider = itemProviderLambda()
- if (state.canScrollForward) {
- // If we can scroll further, we don't know the end yet,
- // but it's upper bounded by #items + 1
- itemProvider.itemCount + 1f
- } else {
- // If we can't scroll further, the current value is the max
- state.currentPosition
- }
- },
- reverseScrolling = reverseScrolling
- )
-
- val scrollByAction: ((x: Float, y: Float) -> Boolean)? =
- if (userScrollEnabled) {
- { x, y ->
- val delta =
- if (isVertical) {
- y
- } else {
- x
- }
- coroutineScope.launch { state.animateScrollBy(delta) }
- // TODO(aelias): is it important to return false if we know in advance we
- // cannot scroll?
- true
- }
- } else {
- null
- }
-
- val scrollToIndexAction: ((Int) -> Boolean)? =
- if (userScrollEnabled) {
- { index ->
- val itemProvider = itemProviderLambda()
- require(index >= 0 && index < itemProvider.itemCount) {
- "Can't scroll to index $index, it is out of " +
- "bounds [0, ${itemProvider.itemCount})"
- }
- coroutineScope.launch { state.scrollToItem(index) }
- true
- }
- } else {
- null
- }
-
- val collectionInfo = state.collectionInfo()
-
- Modifier.semantics {
- isTraversalGroup = true
- indexForKey(indexForKeyMapping)
-
- if (isVertical) {
- verticalScrollAxisRange = accessibilityScrollState
- } else {
- horizontalScrollAxisRange = accessibilityScrollState
- }
-
- if (scrollByAction != null) {
- scrollBy(action = scrollByAction)
- }
-
- if (scrollToIndexAction != null) {
- scrollToIndex(action = scrollToIndexAction)
- }
-
- this.collectionInfo = collectionInfo
- }
- }
- )
-}
-
-internal interface LazyLayoutSemanticState {
- val currentPosition: Float
- val canScrollForward: Boolean
-
- fun collectionInfo(): CollectionInfo
-
- suspend fun animateScrollBy(delta: Float)
-
- suspend fun scrollToItem(index: Int)
-}
-
-internal fun LazyLayoutSemanticState(
- state: TvLazyListState,
- isVertical: Boolean
-): LazyLayoutSemanticState =
- object : LazyLayoutSemanticState {
-
- override val currentPosition: Float
- get() = state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
-
- override val canScrollForward: Boolean
- get() = state.canScrollForward
-
- override suspend fun animateScrollBy(delta: Float) {
- state.animateScrollBy(delta)
- }
-
- override suspend fun scrollToItem(index: Int) {
- state.scrollToItem(index)
- }
-
- override fun collectionInfo(): CollectionInfo =
- if (isVertical) {
- CollectionInfo(rowCount = -1, columnCount = 1)
- } else {
- CollectionInfo(rowCount = 1, columnCount = -1)
- }
- }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt
deleted file mode 100644
index 505e616..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt
+++ /dev/null
@@ -1,262 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
-import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
-import androidx.compose.ui.util.fastForEach
-import androidx.tv.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
-import kotlin.math.min
-
-/**
- * This modifier is used to measure and place additional items when the lazyList receives a request
- * to layout items beyond the visible bounds.
- */
-@Suppress("ComposableModifierFactory")
-@Composable
-internal fun Modifier.lazyListBeyondBoundsModifier(
- state: TvLazyListState,
- beyondBoundsItemCount: Int,
- reverseLayout: Boolean,
- orientation: Orientation
-): Modifier {
- val layoutDirection = LocalLayoutDirection.current
- val beyondBoundsState =
- remember(state, beyondBoundsItemCount) {
- LazyListBeyondBoundsState(state, beyondBoundsItemCount)
- }
- val beyondBoundsInfo = state.beyondBoundsInfo
- return this then
- remember(beyondBoundsState, beyondBoundsInfo, reverseLayout, layoutDirection, orientation) {
- LazyLayoutBeyondBoundsModifierLocal(
- beyondBoundsState,
- beyondBoundsInfo,
- reverseLayout,
- layoutDirection,
- orientation
- )
- }
-}
-
-internal class LazyListBeyondBoundsState(
- val state: TvLazyListState,
- val beyondBoundsItemCount: Int
-) : LazyLayoutBeyondBoundsState {
-
- override fun remeasure() {
- state.remeasurement?.forceRemeasure()
- }
-
- override val itemCount: Int
- get() = state.layoutInfo.totalItemsCount
-
- override val hasVisibleItems: Boolean
- get() = state.layoutInfo.visibleItemsInfo.isNotEmpty()
-
- override val firstPlacedIndex: Int
- get() = maxOf(0, state.firstVisibleItemIndex - beyondBoundsItemCount)
-
- override val lastPlacedIndex: Int
- get() =
- minOf(
- itemCount - 1,
- state.layoutInfo.visibleItemsInfo.last().index + beyondBoundsItemCount
- )
-}
-
-internal interface LazyLayoutBeyondBoundsState {
-
- fun remeasure()
-
- val itemCount: Int
-
- val hasVisibleItems: Boolean
-
- val firstPlacedIndex: Int
-
- val lastPlacedIndex: Int
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal fun LazyLayoutItemProvider.calculateLazyLayoutPinnedIndices(
- pinnedItemList: LazyLayoutPinnedItemList,
- beyondBoundsInfo: LazyLayoutBeyondBoundsInfo,
-): List<Int> {
- if (!beyondBoundsInfo.hasIntervals() && pinnedItemList.isEmpty()) {
- return emptyList()
- } else {
- val pinnedItems = mutableListOf<Int>()
- val beyondBoundsRange =
- if (beyondBoundsInfo.hasIntervals()) {
- beyondBoundsInfo.start..min(beyondBoundsInfo.end, itemCount - 1)
- } else {
- IntRange.EMPTY
- }
- pinnedItemList.fastForEach {
- val index = findIndexByKey(it.key, it.index)
- if (index in beyondBoundsRange) return@fastForEach
- if (index !in 0 until itemCount) return@fastForEach
- pinnedItems.add(index)
- }
- for (i in beyondBoundsRange) {
- pinnedItems.add(i)
- }
- return pinnedItems
- }
-}
-
-internal class LazyLayoutBeyondBoundsModifierLocal(
- private val state: LazyLayoutBeyondBoundsState,
- private val beyondBoundsInfo: LazyLayoutBeyondBoundsInfo,
- private val reverseLayout: Boolean,
- private val layoutDirection: LayoutDirection,
- private val orientation: Orientation
-) : ModifierLocalProvider<BeyondBoundsLayout?>, BeyondBoundsLayout {
- override val key: ProvidableModifierLocal<BeyondBoundsLayout?>
- get() = ModifierLocalBeyondBoundsLayout
-
- override val value: BeyondBoundsLayout
- get() = this
-
- companion object {
- private val emptyBeyondBoundsScope =
- object : BeyondBoundsScope {
- override val hasMoreContent = false
- }
- }
-
- override fun <T> layout(
- direction: BeyondBoundsLayout.LayoutDirection,
- block: BeyondBoundsScope.() -> T?
- ): T? {
- // If the lazy list is empty, or if it does not have any visible items (Which implies
- // that there isn't space to add a single item), we don't attempt to layout any more items.
- if (state.itemCount <= 0 || !state.hasVisibleItems) {
- return block.invoke(emptyBeyondBoundsScope)
- }
-
- // We use a new interval each time because this function is re-entrant.
- val startIndex =
- if (direction.isForward()) {
- state.lastPlacedIndex
- } else {
- state.firstPlacedIndex
- }
- var interval = beyondBoundsInfo.addInterval(startIndex, startIndex)
- var found: T? = null
- while (found == null && interval.hasMoreContent(direction)) {
-
- // Add one extra beyond bounds item.
- interval =
- addNextInterval(interval, direction).also {
- beyondBoundsInfo.removeInterval(interval)
- }
- state.remeasure()
-
- // When we invoke this block, the beyond bounds items are present.
- found =
- block.invoke(
- object : BeyondBoundsScope {
- override val hasMoreContent: Boolean
- get() = interval.hasMoreContent(direction)
- }
- )
- }
-
- // Dispose the items that are beyond the visible bounds.
- beyondBoundsInfo.removeInterval(interval)
- state.remeasure()
- return found
- }
-
- private fun BeyondBoundsLayout.LayoutDirection.isForward(): Boolean =
- when (this) {
- Before -> false
- After -> true
- Above -> reverseLayout
- Below -> !reverseLayout
- Left ->
- when (layoutDirection) {
- Ltr -> reverseLayout
- Rtl -> !reverseLayout
- }
- Right ->
- when (layoutDirection) {
- Ltr -> !reverseLayout
- Rtl -> reverseLayout
- }
- else -> unsupportedDirection()
- }
-
- private fun addNextInterval(
- currentInterval: LazyLayoutBeyondBoundsInfo.Interval,
- direction: BeyondBoundsLayout.LayoutDirection
- ): LazyLayoutBeyondBoundsInfo.Interval {
- var start = currentInterval.start
- var end = currentInterval.end
- if (direction.isForward()) {
- end++
- } else {
- start--
- }
- return beyondBoundsInfo.addInterval(start, end)
- }
-
- private fun LazyLayoutBeyondBoundsInfo.Interval.hasMoreContent(
- direction: BeyondBoundsLayout.LayoutDirection
- ): Boolean {
- if (direction.isOppositeToOrientation()) return false
- return if (direction.isForward()) end < state.itemCount - 1 else start > 0
- }
-
- private fun BeyondBoundsLayout.LayoutDirection.isOppositeToOrientation(): Boolean {
- return when (this) {
- Above,
- Below -> orientation == Orientation.Horizontal
- Left,
- Right -> orientation == Orientation.Vertical
- Before,
- After -> false
- else -> unsupportedDirection()
- }
- }
-}
-
-private fun unsupportedDirection(): Nothing =
- error("Lazy list does not support beyond bounds layout for the specified direction")
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
deleted file mode 100644
index 8051823..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
+++ /dev/null
@@ -1,362 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.PivotOffsets
-
-/* Copied from
-compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
- and modified */
-
-/** Receiver scope which is used by [TvLazyColumn] and [TvLazyRow]. */
-@Deprecated("Use `LazyListScope` instead.")
-@TvLazyListScopeMarker
-sealed interface TvLazyListScope {
- /**
- * Adds a single item.
- *
- * @param key a stable and unique key representing the item. Using the same key for multiple
- * items in the list is not allowed. Type of the key should be saveable via Bundle on Android.
- * If null is passed the position in the list will represent the key. When you specify the key
- * the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param contentType the type of the content of this item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such
- * type will be considered compatible.
- * @param content the content of the item
- */
- fun item(
- key: Any? = null,
- contentType: Any? = null,
- content: @Composable TvLazyListItemScope.() -> Unit
- )
-
- /**
- * Adds a [count] of items.
- *
- * @param count the items count
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the list will represent the key. When you
- * specify the key the scroll position will be maintained based on the key, which means if you
- * add/remove items before the current visible item the item with the given key will be kept
- * as the first visible one.
- * @param contentType a factory of the content types for the item. The item compositions of the
- * same type could be reused more efficiently. Note that null is a valid type and items of
- * such type will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
- fun items(
- count: Int,
- key: ((index: Int) -> Any)? = null,
- contentType: (index: Int) -> Any? = { null },
- itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
- )
-
- /**
- * Adds a sticky header item, which will remain pinned even when scrolling after it. The header
- * will remain pinned until the next header will take its place.
- *
- * @param key a stable and unique key representing the item. Using the same key for multiple
- * items in the list is not allowed. Type of the key should be saveable via Bundle on Android.
- * If null is passed the position in the list will represent the key. When you specify the key
- * the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param contentType the type of the content of this item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such
- * type will be considered compatible.
- * @param content the content of the header
- */
- @ExperimentalTvFoundationApi
- fun stickyHeader(
- key: Any? = null,
- contentType: Any? = null,
- content: @Composable TvLazyListItemScope.() -> Unit
- )
-}
-
-/**
- * Adds a list of items.
- *
- * @param items the data list
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the list will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyListScope.items` instead.")
-inline fun <T> TvLazyListScope.items(
- items: List<T>,
- noinline key: ((item: T) -> Any)? = null,
- noinline contentType: (item: T) -> Any? = { null },
- crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(items[index]) } else null,
- contentType = { index: Int -> contentType(items[index]) }
- ) {
- itemContent(items[it])
- }
-
-/**
- * Adds a list of items where the content of an item is aware of its index.
- *
- * @param items the data list
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the list will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyListScope.itemsIndexed` instead.")
-inline fun <T> TvLazyListScope.itemsIndexed(
- items: List<T>,
- noinline key: ((index: Int, item: T) -> Any)? = null,
- crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
- crossinline itemContent: @Composable TvLazyListItemScope.(index: Int, item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(index, items[index]) } else null,
- contentType = { index -> contentType(index, items[index]) }
- ) {
- itemContent(it, items[it])
- }
-
-/**
- * Adds an array of items.
- *
- * @param items the data array
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the list will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyListScope.items` instead.")
-inline fun <T> TvLazyListScope.items(
- items: Array<T>,
- noinline key: ((item: T) -> Any)? = null,
- noinline contentType: (item: T) -> Any? = { null },
- crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(items[index]) } else null,
- contentType = { index: Int -> contentType(items[index]) }
- ) {
- itemContent(items[it])
- }
-
-/**
- * Adds an array of items where the content of an item is aware of its index.
- *
- * @param items the data array
- * @param key a factory of stable and unique keys representing the item. Using the same key for
- * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on
- * Android. If null is passed the position in the list will represent the key. When you specify
- * the key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param contentType a factory of the content types for the item. The item compositions of the same
- * type could be reused more efficiently. Note that null is a valid type and items of such type
- * will be considered compatible.
- * @param itemContent the content displayed by a single item
- */
-@Deprecated("Use `LazyListScope.itemsIndexed` instead.")
-inline fun <T> TvLazyListScope.itemsIndexed(
- items: Array<T>,
- noinline key: ((index: Int, item: T) -> Any)? = null,
- crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
- crossinline itemContent: @Composable TvLazyListItemScope.(index: Int, item: T) -> Unit
-) =
- items(
- count = items.size,
- key = if (key != null) { index: Int -> key(index, items[index]) } else null,
- contentType = { index -> contentType(index, items[index]) }
- ) {
- itemContent(it, items[it])
- }
-
-/**
- * The horizontally scrolling list that only composes and lays out the currently visible items. The
- * [content] block defines a DSL which allows you to emit items of different types. For example you
- * can use [TvLazyListScope.item] to add a single item and [TvLazyListScope.items] to add a list of
- * items.
- *
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the content
- * after it has been clipped, which is not possible via [modifier] param. You can use it to add a
- * padding before the first item or after the last one. If you want to add a spacing between each
- * item use [horizontalArrangement].
- * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are laid
- * out in the reverse order and [TvLazyListState.firstVisibleItemIndex] == 0 means that row is
- * scrolled to the end. Note that [reverseLayout] does not change the behavior of
- * [horizontalArrangement], e.g. with [Arrangement.Start] [123###] becomes [321###].
- * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows to
- * add a spacing between items and specify the arrangement of the items when we have not enough of
- * them to fill the whole minimum size.
- * @param verticalAlignment the vertical alignment applied to the items
- * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
- * allowed. You can still scroll programmatically using the state even when it is disabled.
- * @param pivotOffsets offsets of child element within the parent and starting edge of the child
- * from the pivot defined by the parentOffset.
- * @param content a block which describes the content. Inside this block you can use methods like
- * [TvLazyListScope.item] to add a single item or [TvLazyListScope.items] to add a list of items.
- */
-@Deprecated(
- "LazyRow will, by default, set the position of focused item while scrolling on " +
- "Tv. BringIntoViewSpec should be used to control the position.",
- replaceWith =
- ReplaceWith(
- "LazyRow(" +
- "modifier = modifier, " +
- "contentPadding = contentPadding, " +
- "reverseLayout = reverseLayout, " +
- "horizontalArrangement = horizontalArrangement, " +
- "verticalAlignment = verticalAlignment, " +
- "userScrollEnabled = userScrollEnabled" +
- ") { content() }",
- imports = ["androidx.compose.foundation.lazy.LazyRow"],
- )
-)
-@Composable
-fun TvLazyRow(
- modifier: Modifier = Modifier,
- state: TvLazyListState = rememberTvLazyListState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- horizontalArrangement: Arrangement.Horizontal =
- if (!reverseLayout) Arrangement.Start else Arrangement.End,
- verticalAlignment: Alignment.Vertical = Alignment.Top,
- userScrollEnabled: Boolean = true,
- pivotOffsets: PivotOffsets = PivotOffsets(),
- content: TvLazyListScope.() -> Unit
-) {
- LazyList(
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- verticalAlignment = verticalAlignment,
- horizontalArrangement = horizontalArrangement,
- isVertical = false,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- content = content,
- pivotOffsets = pivotOffsets
- )
-}
-
-/**
- * The vertically scrolling list that only composes and lays out the currently visible items. The
- * [content] block defines a DSL which allows you to emit items of different types. For example you
- * can use [TvLazyListScope.item] to add a single item and [TvLazyListScope.items] to add a list of
- * items.
- *
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the list's state.
- * @param contentPadding a padding around the whole content. This will add padding for the. content
- * after it has been clipped, which is not possible via [modifier] param. You can use it to add a
- * padding before the first item or after the last one. If you want to add a spacing between each
- * item use [verticalArrangement].
- * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are laid
- * out in the reverse order and [TvLazyListState.firstVisibleItemIndex] == 0 means that column is
- * scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
- * [verticalArrangement], e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321###
- * (bottom).
- * @param verticalArrangement The vertical arrangement of the layout's children. This allows to add
- * a spacing between items and specify the arrangement of the items when we have not enough of
- * them to fill the whole minimum size.
- * @param horizontalAlignment the horizontal alignment applied to the items.
- * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
- * allowed. You can still scroll programmatically using the state even when it is disabled.
- * @param content a block which describes the content. Inside this block you can use methods like
- * @param pivotOffsets offsets of child element within the parent and starting edge of the child
- * from the pivot defined by the parentOffset. [TvLazyListScope.item] to add a single item or
- * [TvLazyListScope.items] to add a list of items.
- */
-@Deprecated(
- "LazyColumn will, by default, set the position of focused item while scrolling on " +
- "Tv. BringIntoViewSpec should be used to control the position.",
- replaceWith =
- ReplaceWith(
- "LazyColumn(" +
- "modifier = modifier, " +
- "contentPadding = contentPadding, " +
- "reverseLayout = reverseLayout, " +
- "verticalArrangement = verticalArrangement, " +
- "horizontalAlignment = horizontalAlignment, " +
- "userScrollEnabled = userScrollEnabled" +
- ") { content() }",
- imports = ["androidx.compose.foundation.lazy.LazyColumn"],
- )
-)
-@Composable
-fun TvLazyColumn(
- modifier: Modifier = Modifier,
- state: TvLazyListState = rememberTvLazyListState(),
- contentPadding: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- verticalArrangement: Arrangement.Vertical =
- if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
- horizontalAlignment: Alignment.Horizontal = Alignment.Start,
- userScrollEnabled: Boolean = true,
- pivotOffsets: PivotOffsets = PivotOffsets(),
- content: TvLazyListScope.() -> Unit
-) {
- LazyList(
- modifier = modifier,
- state = state,
- contentPadding = contentPadding,
- horizontalAlignment = horizontalAlignment,
- verticalArrangement = verticalArrangement,
- isVertical = true,
- reverseLayout = reverseLayout,
- userScrollEnabled = userScrollEnabled,
- content = content,
- pivotOffsets = pivotOffsets
- )
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
deleted file mode 100644
index 600ea05..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.checkScrollableContainerConstraints
-import androidx.compose.foundation.clipScrollableContainer
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.foundation.lazy.layout.LazyLayout
-import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
-import androidx.compose.foundation.overscroll
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.offset
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.PivotOffsets
-import androidx.tv.foundation.lazy.layout.lazyLayoutSemantics
-import androidx.tv.foundation.scrollableWithPivot
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTvFoundationApi::class)
-@Composable
-internal fun LazyList(
- /** Modifier to be applied for the inner layout */
- modifier: Modifier,
- /** State controlling the scroll position */
- state: TvLazyListState,
- /** The inner padding to be added for the whole content(not for each individual item) */
- contentPadding: PaddingValues,
- /** reverse the direction of scrolling and layout */
- reverseLayout: Boolean,
- /** The layout orientation of the list */
- isVertical: Boolean,
- /** Whether scrolling via the user gestures is allowed. */
- userScrollEnabled: Boolean,
- /** Number of items to layout before and after the visible items */
- beyondBoundsItemCount: Int = 0,
- /**
- * offsets of child element within the parent and starting edge of the child from the pivot
- * defined by the parentOffset.
- */
- pivotOffsets: PivotOffsets,
- /** The alignment to align items horizontally. Required when isVertical is true */
- horizontalAlignment: Alignment.Horizontal? = null,
- /** The vertical arrangement for items. Required when isVertical is true */
- verticalArrangement: Arrangement.Vertical? = null,
- /** The alignment to align items vertically. Required when isVertical is false */
- verticalAlignment: Alignment.Vertical? = null,
- /** The horizontal arrangement for items. Required when isVertical is false */
- horizontalArrangement: Arrangement.Horizontal? = null,
- /** The content of the list */
- content: TvLazyListScope.() -> Unit
-) {
- val itemProviderLambda = rememberLazyListItemProviderLambda(state, content)
- val semanticState = rememberLazyListSemanticState(state, isVertical)
- val scope = rememberCoroutineScope()
- state.coroutineScope = scope
-
- val measurePolicy =
- rememberLazyListMeasurePolicy(
- itemProviderLambda,
- state,
- contentPadding,
- reverseLayout,
- isVertical,
- beyondBoundsItemCount,
- horizontalAlignment,
- verticalAlignment,
- horizontalArrangement,
- verticalArrangement
- )
-
- ScrollPositionUpdater(itemProviderLambda, state)
-
- val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
-
- LazyLayout(
- modifier =
- modifier
- .then(state.remeasurementModifier)
- .then(state.awaitLayoutModifier)
- .lazyLayoutSemantics(
- itemProviderLambda = itemProviderLambda,
- state = semanticState,
- orientation = orientation,
- userScrollEnabled = userScrollEnabled,
- reverseScrolling = reverseLayout
- )
- .clipScrollableContainer(orientation)
- .lazyListBeyondBoundsModifier(
- state,
- beyondBoundsItemCount,
- reverseLayout,
- orientation
- )
- .overscroll(overscrollEffect)
- .scrollableWithPivot(
- orientation = orientation,
- reverseDirection =
- ScrollableDefaults.reverseDirection(
- LocalLayoutDirection.current,
- orientation,
- reverseLayout
- ),
- state = state,
- enabled = userScrollEnabled,
- pivotOffsets = pivotOffsets
- ),
- prefetchState = state.prefetchState,
- measurePolicy = measurePolicy,
- itemProvider = itemProviderLambda
- )
-}
-
-/** Extracted to minimize the recomposition scope */
-@ExperimentalFoundationApi
-@Composable
-private fun ScrollPositionUpdater(
- itemProviderLambda: () -> LazyListItemProvider,
- state: TvLazyListState
-) {
- val itemProvider = itemProviderLambda()
- if (itemProvider.itemCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
- }
-}
-
-@OptIn(ExperimentalTvFoundationApi::class)
-@ExperimentalFoundationApi
-@Composable
-private fun rememberLazyListMeasurePolicy(
- /** Items provider of the list. */
- itemProviderLambda: () -> LazyListItemProvider,
- /** The state of the list. */
- state: TvLazyListState,
- /** The inner padding to be added for the whole content(nor for each individual item) */
- contentPadding: PaddingValues,
- /** reverse the direction of scrolling and layout */
- reverseLayout: Boolean,
- /** The layout orientation of the list */
- isVertical: Boolean,
- /** Number of items to layout before and after the visible items */
- beyondBoundsItemCount: Int,
- /** The alignment to align items horizontally. Required when isVertical is true */
- horizontalAlignment: Alignment.Horizontal? = null,
- /** The alignment to align items vertically. Required when isVertical is false */
- verticalAlignment: Alignment.Vertical? = null,
- /** The horizontal arrangement for items. Required when isVertical is false */
- horizontalArrangement: Arrangement.Horizontal? = null,
- /** The vertical arrangement for items. Required when isVertical is true */
- verticalArrangement: Arrangement.Vertical? = null
-) =
- remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
- state,
- contentPadding,
- reverseLayout,
- isVertical,
- horizontalAlignment,
- verticalAlignment,
- horizontalArrangement,
- verticalArrangement
- ) {
- { containerConstraints ->
- // Tracks if the lookahead pass has occurred
- val hasLookaheadPassOccurred = state.hasLookaheadPassOccurred || isLookingAhead
- checkScrollableContainerConstraints(
- containerConstraints,
- if (isVertical) Orientation.Vertical else Orientation.Horizontal
- )
-
- // resolve content paddings
- val startPadding =
- if (isVertical) {
- contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
- } else {
- // in horizontal configuration, padding is reversed by placeRelative
- contentPadding.calculateStartPadding(layoutDirection).roundToPx()
- }
-
- val endPadding =
- if (isVertical) {
- contentPadding.calculateRightPadding(layoutDirection).roundToPx()
- } else {
- // in horizontal configuration, padding is reversed by placeRelative
- contentPadding.calculateEndPadding(layoutDirection).roundToPx()
- }
- val topPadding = contentPadding.calculateTopPadding().roundToPx()
- val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
- val totalVerticalPadding = topPadding + bottomPadding
- val totalHorizontalPadding = startPadding + endPadding
- val totalMainAxisPadding =
- if (isVertical) totalVerticalPadding else totalHorizontalPadding
- val beforeContentPadding =
- when {
- isVertical && !reverseLayout -> topPadding
- isVertical && reverseLayout -> bottomPadding
- !isVertical && !reverseLayout -> startPadding
- else -> endPadding // !isVertical && reverseLayout
- }
- val afterContentPadding = totalMainAxisPadding - beforeContentPadding
- val contentConstraints =
- containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
-
- // Update the state's cached Density
- state.density = this
-
- val itemProvider = itemProviderLambda()
- // this will update the scope used by the item composables
- itemProvider.itemScope.setMaxSize(
- width = contentConstraints.maxWidth,
- height = contentConstraints.maxHeight
- )
-
- val spaceBetweenItemsDp =
- if (isVertical) {
- requireNotNull(verticalArrangement) {
- "null verticalArrangement when isVertical == true"
- }
- .spacing
- } else {
- requireNotNull(horizontalArrangement) {
- "null horizontalArrangement when isVertical == false"
- }
- .spacing
- }
- val spaceBetweenItems = spaceBetweenItemsDp.roundToPx()
-
- val itemsCount = itemProvider.itemCount
-
- // can be negative if the content padding is larger than the max size from constraints
- val mainAxisAvailableSize =
- if (isVertical) {
- containerConstraints.maxHeight - totalVerticalPadding
- } else {
- containerConstraints.maxWidth - totalHorizontalPadding
- }
- val visualItemOffset =
- if (!reverseLayout || mainAxisAvailableSize > 0) {
- IntOffset(startPadding, topPadding)
- } else {
- // When layout is reversed and paddings together take >100% of the available
- // space,
- // layout size is coerced to 0 when positioning. To take that space into
- // account,
- // we offset start padding by negative space between paddings.
- IntOffset(
- if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
- if (isVertical) topPadding + mainAxisAvailableSize else topPadding
- )
- }
-
- val measuredItemProvider =
- object :
- LazyListMeasuredItemProvider(
- contentConstraints,
- isVertical,
- itemProvider,
- this
- ) {
- override fun createItem(
- index: Int,
- key: Any,
- contentType: Any?,
- placeables: List<Placeable>
- ): LazyListMeasuredItem {
- // we add spaceBetweenItems as an extra spacing for all items apart from the
- // last one so
- // the lazy list measuring logic will take it into account.
- val spacing = if (index == itemsCount - 1) 0 else spaceBetweenItems
- return LazyListMeasuredItem(
- index = index,
- placeables = placeables,
- isVertical = isVertical,
- horizontalAlignment = horizontalAlignment,
- verticalAlignment = verticalAlignment,
- layoutDirection = layoutDirection,
- reverseLayout = reverseLayout,
- beforeContentPadding = beforeContentPadding,
- afterContentPadding = afterContentPadding,
- spacing = spacing,
- visualOffset = visualItemOffset,
- key = key,
- contentType = contentType
- )
- }
- }
- state.premeasureConstraints = measuredItemProvider.childConstraints
-
- val firstVisibleItemIndex: Int
- val firstVisibleScrollOffset: Int
- Snapshot.withoutReadObservation {
- firstVisibleItemIndex =
- state.updateScrollPositionIfTheFirstItemWasMoved(
- itemProvider,
- state.firstVisibleItemIndex
- )
- firstVisibleScrollOffset = state.firstVisibleItemScrollOffset
- }
-
- val pinnedItems =
- itemProvider.calculateLazyLayoutPinnedIndices(
- pinnedItemList = state.pinnedItems,
- beyondBoundsInfo = state.beyondBoundsInfo
- )
-
- val scrollToBeConsumed =
- if (isLookingAhead || !hasLookaheadPassOccurred) {
- state.scrollToBeConsumed
- } else {
- state.scrollDeltaBetweenPasses
- }
-
- measureLazyList(
- itemsCount = itemsCount,
- measuredItemProvider = measuredItemProvider,
- mainAxisAvailableSize = mainAxisAvailableSize,
- beforeContentPadding = beforeContentPadding,
- afterContentPadding = afterContentPadding,
- spaceBetweenItems = spaceBetweenItems,
- firstVisibleItemIndex = firstVisibleItemIndex,
- firstVisibleItemScrollOffset = firstVisibleScrollOffset,
- scrollToBeConsumed = scrollToBeConsumed,
- constraints = contentConstraints,
- isVertical = isVertical,
- headerIndexes = itemProvider.headerIndexes,
- verticalArrangement = verticalArrangement,
- horizontalArrangement = horizontalArrangement,
- reverseLayout = reverseLayout,
- density = this,
- placementAnimator = state.placementAnimator,
- beyondBoundsItemCount = beyondBoundsItemCount,
- pinnedItems = pinnedItems,
- hasLookaheadPassOccurred = hasLookaheadPassOccurred,
- isLookingAhead = isLookingAhead,
- postLookaheadLayoutInfo = state.postLookaheadLayoutInfo,
- layout = { width, height, placement ->
- layout(
- containerConstraints.constrainWidth(width + totalHorizontalPadding),
- containerConstraints.constrainHeight(height + totalVerticalPadding),
- emptyMap(),
- placement
- )
- }
- )
- .also { state.applyMeasureResult(it, isLookingAhead) }
- }
- }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
deleted file mode 100644
index 8d1697c..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastSumBy
-import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
-import kotlin.math.abs
-
-internal class LazyListAnimateScrollScope(private val state: TvLazyListState) :
- LazyAnimateScrollScope {
- override val density: Density
- get() = state.density
-
- override val firstVisibleItemIndex: Int
- get() = state.firstVisibleItemIndex
-
- override val firstVisibleItemScrollOffset: Int
- get() = state.firstVisibleItemScrollOffset
-
- override val lastVisibleItemIndex: Int
- get() = state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
-
- override val itemCount: Int
- get() = state.layoutInfo.totalItemsCount
-
- override val numOfItemsForTeleport: Int = 100
-
- override fun getTargetItemOffset(index: Int): Int? =
- state.layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == index }?.offset
-
- override fun ScrollScope.snapToItem(index: Int, scrollOffset: Int) {
- state.snapToItemIndexInternal(index, scrollOffset)
- }
-
- override fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
- val layoutInfo = state.layoutInfo
- val visibleItems = layoutInfo.visibleItemsInfo
- val averageSize =
- visibleItems.fastSumBy { it.size } / visibleItems.size + layoutInfo.mainAxisItemSpacing
- val indexesDiff = index - firstVisibleItemIndex
- var coercedOffset = minOf(abs(targetScrollOffset), averageSize)
- if (targetScrollOffset < 0) coercedOffset *= -1
- return (averageSize * indexesDiff).toFloat() + coercedOffset - firstVisibleItemScrollOffset
- }
-
- override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
- state.scroll(block = block)
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsInfo.kt
deleted file mode 100644
index 9377a5a3..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsInfo.kt
+++ /dev/null
@@ -1,109 +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.tv.foundation.lazy.list
-
-import androidx.compose.runtime.collection.mutableVectorOf
-
-/**
- * This data structure is used to save information about the number of "beyond bounds items" that we
- * want to compose. These items are not within the visible bounds of the lazylist, but we compose
- * them because they are explicitly requested through the
- * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout].
- *
- * When the LazyList receives a
- * [searchBeyondBounds][androidx.compose.ui.layout.BeyondBoundsLayout.searchBeyondBounds] request to
- * layout items beyond visible bounds, it creates an instance of [LazyListBeyondBoundsInfo] by using
- * the [addInterval] function. This returns the interval of items that are currently composed, and
- * we can edit this interval to control the number of beyond bounds items.
- *
- * There can be multiple intervals created at the same time, and LazyList merges all the intervals
- * to calculate the effective beyond bounds items.
- *
- * The [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout] is designed to be
- * synchronous, so once you are done using the items, call [removeInterval] to remove the extra
- * items you had requested.
- *
- * Note that when you clear an interval, the items in that interval might not be cleared right away
- * if another interval was created that has the same items. This is done to support two use cases:
- * 1. To allow items to be pinned while they are being scrolled into view.
- * 2. To allow users to call
- * [searchBeyondBounds][androidx.compose.ui.layout.BeyondBoundsLayout.searchBeyondBounds] from
- * within the completion block of another searchBeyondBounds call.
- */
-internal class LazyListBeyondBoundsInfo {
- private val beyondBoundsItems = mutableVectorOf<Interval>()
-
- /**
- * Create a beyond bounds interval. This can be used to specify which composed items we want to
- * retain. For instance, it can be used to force the measuring of items that are beyond the
- * visible bounds of a lazy list.
- *
- * @param start The starting index (inclusive) for this interval.
- * @param end The ending index (inclusive) for this interval.
- * @return An interval that specifies which items we want to retain.
- */
- fun addInterval(start: Int, end: Int): Interval {
- return Interval(start, end).apply { beyondBoundsItems.add(this) }
- }
-
- /** Clears the specified interval. Use this to remove the interval created by [addInterval]. */
- fun removeInterval(interval: Interval) {
- beyondBoundsItems.remove(interval)
- }
-
- /** Returns true if there are beyond bounds intervals. */
- fun hasIntervals(): Boolean = beyondBoundsItems.isNotEmpty()
-
- /** The effective start index after merging all the current intervals. */
- val start: Int
- get() {
- var minIndex = beyondBoundsItems.first().start
- beyondBoundsItems.forEach {
- if (it.start < minIndex) {
- minIndex = it.start
- }
- }
- require(minIndex >= 0) { "negative minIndex" }
- return minIndex
- }
-
- /** The effective end index after merging all the current intervals. */
- val end: Int
- get() {
- var maxIndex = beyondBoundsItems.first().end
- beyondBoundsItems.forEach {
- if (it.end > maxIndex) {
- maxIndex = it.end
- }
- }
- return maxIndex
- }
-
- /** The Interval used to implement [LazyListBeyondBoundsInfo]. */
- internal data class Interval(
- /** The start index for the interval. */
- val start: Int,
-
- /** The end index for the interval. */
- val end: Int
- ) {
- init {
- require(start >= 0) { "negative start index" }
- require(end >= start) { "end < start" }
- }
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt
deleted file mode 100644
index 3c080d9..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt
+++ /dev/null
@@ -1,95 +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.tv.foundation.lazy.list
-
-import androidx.compose.ui.util.fastForEachIndexed
-
-/**
- * This method finds the sticky header in composedItems list or composes the header item if needed.
- *
- * @param composedVisibleItems list of items already composed and expected to be visible. if the
- * header wasn't in this list but is needed the header will be added as the first item in this
- * list.
- * @param itemProvider the provider so we can compose a header if it wasn't composed already
- * @param headerIndexes list of indexes of headers. Must be sorted.
- * @param beforeContentPadding the padding before the first item in the list
- */
-internal fun findOrComposeLazyListHeader(
- composedVisibleItems: MutableList<LazyListMeasuredItem>,
- itemProvider: LazyListMeasuredItemProvider,
- headerIndexes: List<Int>,
- beforeContentPadding: Int,
- layoutWidth: Int,
- layoutHeight: Int,
-): LazyListMeasuredItem? {
- var currentHeaderOffset: Int = Int.MIN_VALUE
- var nextHeaderOffset: Int = Int.MIN_VALUE
-
- var currentHeaderListPosition = -1
- var nextHeaderListPosition = -1
- // we use visibleItemsInfo and not firstVisibleItemIndex as visibleItemsInfo list also
- // contains all the items which are visible in the start content padding area
- val firstVisible = composedVisibleItems.first().index
- // find the header which can be displayed
- for (index in headerIndexes.indices) {
- if (headerIndexes[index] <= firstVisible) {
- currentHeaderListPosition = headerIndexes[index]
- nextHeaderListPosition = headerIndexes.getOrElse(index + 1) { -1 }
- } else {
- break
- }
- }
-
- var indexInComposedVisibleItems = -1
- composedVisibleItems.fastForEachIndexed { index, item ->
- if (item.index == currentHeaderListPosition) {
- indexInComposedVisibleItems = index
- currentHeaderOffset = item.offset
- } else {
- if (item.index == nextHeaderListPosition) {
- nextHeaderOffset = item.offset
- }
- }
- }
-
- if (currentHeaderListPosition == -1) {
- // we have no headers needing special handling
- return null
- }
-
- val measuredHeaderItem = itemProvider.getAndMeasure(currentHeaderListPosition)
-
- var headerOffset =
- if (currentHeaderOffset != Int.MIN_VALUE) {
- maxOf(-beforeContentPadding, currentHeaderOffset)
- } else {
- -beforeContentPadding
- }
- // if we have a next header overlapping with the current header, the next one will be
- // pushing the current one away from the viewport.
- if (nextHeaderOffset != Int.MIN_VALUE) {
- headerOffset = minOf(headerOffset, nextHeaderOffset - measuredHeaderItem.size)
- }
-
- measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight)
- if (indexInComposedVisibleItems != -1) {
- composedVisibleItems[indexInComposedVisibleItems] = measuredHeaderItem
- } else {
- composedVisibleItems.add(0, measuredHeaderItem)
- }
- return measuredHeaderItem
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
deleted file mode 100644
index d820c49..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
+++ /dev/null
@@ -1,272 +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.tv.foundation.lazy.list
-
-import androidx.collection.mutableScatterSetOf
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-import androidx.tv.foundation.lazy.grid.LazyLayoutAnimateItemModifierNode
-import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
-
-/**
- * Handles the item placement animations when it is set via
- * [TvLazyListItemScope.animateItemPlacement].
- *
- * This class is responsible for detecting when item position changed, figuring our start/end
- * offsets and starting the animations.
- */
-internal class LazyListItemPlacementAnimator {
- // contains the keys of the active items with animation node.
- private val activeKeys = mutableSetOf<Any>()
-
- // snapshot of the key to index map used for the last measuring.
- private var keyIndexMap: LazyLayoutKeyIndexMap = LazyLayoutKeyIndexMap.Empty
-
- // keeps the index of the first visible item index.
- private var firstVisibleIndex = 0
-
- // stored to not allocate it every pass.
- private val movingAwayKeys = mutableScatterSetOf<Any>()
- private val movingInFromStartBound = mutableListOf<LazyListMeasuredItem>()
- private val movingInFromEndBound = mutableListOf<LazyListMeasuredItem>()
- private val movingAwayToStartBound = mutableListOf<LazyListMeasuredItem>()
- private val movingAwayToEndBound = mutableListOf<LazyListMeasuredItem>()
-
- /**
- * Should be called after the measuring so we can detect position changes and start animations.
- *
- * Note that this method can compose new item and add it into the [positionedItems] list.
- */
- fun onMeasured(
- consumedScroll: Int,
- layoutWidth: Int,
- layoutHeight: Int,
- positionedItems: MutableList<LazyListMeasuredItem>,
- itemProvider: LazyListMeasuredItemProvider,
- isVertical: Boolean,
- isLookingAhead: Boolean,
- hasLookaheadOccurred: Boolean
- ) {
- if (!positionedItems.fastAny { it.hasAnimations } && activeKeys.isEmpty()) {
- // no animations specified - no work needed
- reset()
- return
- }
-
- val previousFirstVisibleIndex = firstVisibleIndex
- firstVisibleIndex = positionedItems.firstOrNull()?.index ?: 0
-
- val previousKeyToIndexMap = keyIndexMap
- keyIndexMap = itemProvider.keyIndexMap
-
- val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
-
- // the consumed scroll is considered as a delta we don't need to animate
- val scrollOffset =
- if (isVertical) {
- IntOffset(0, consumedScroll)
- } else {
- IntOffset(consumedScroll, 0)
- }
-
- // Only setup animations when we have access to target value in the current pass, which
- // means lookahead pass, or regular pass when not in a lookahead scope.
- val shouldSetupAnimation = isLookingAhead || !hasLookaheadOccurred
- // first add all items we had in the previous run
- movingAwayKeys.addAll(activeKeys)
- // iterate through the items which are visible (without animated offsets)
- positionedItems.fastForEach { item ->
- // remove items we have in the current one as they are still visible.
- movingAwayKeys.remove(item.key)
- if (item.hasAnimations) {
- if (!activeKeys.contains(item.key)) {
- activeKeys += item.key
- val previousIndex = previousKeyToIndexMap.getIndex(item.key)
- if (previousIndex != -1 && item.index != previousIndex) {
- if (previousIndex < previousFirstVisibleIndex) {
- // the larger index will be in the start of the list
- movingInFromStartBound.add(item)
- } else {
- movingInFromEndBound.add(item)
- }
- } else {
- initializeNode(
- item,
- item.getOffset(0).let { if (item.isVertical) it.y else it.x }
- )
- }
- } else {
- if (shouldSetupAnimation) {
- item.forEachNode { _, node ->
- if (
- node.rawOffset != LazyLayoutAnimateItemModifierNode.NotInitialized
- ) {
- node.rawOffset += scrollOffset
- }
- }
- startAnimationsIfNeeded(item)
- }
- }
- } else {
- // no animation, clean up if needed
- activeKeys.remove(item.key)
- }
- }
-
- var accumulatedOffset = 0
- if (shouldSetupAnimation) {
- movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
- movingInFromStartBound.fastForEach { item ->
- accumulatedOffset += item.size
- val mainAxisOffset = 0 - accumulatedOffset
- initializeNode(item, mainAxisOffset)
- startAnimationsIfNeeded(item)
- }
- accumulatedOffset = 0
- movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
- movingInFromEndBound.fastForEach { item ->
- val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
- accumulatedOffset += item.size
- initializeNode(item, mainAxisOffset)
- startAnimationsIfNeeded(item)
- }
- }
-
- movingAwayKeys.forEach { key ->
- // found an item which was in our map previously but is not a part of the
- // positionedItems now
- val newIndex = keyIndexMap.getIndex(key)
-
- if (newIndex == -1) {
- activeKeys.remove(key)
- } else {
- val item = itemProvider.getAndMeasure(newIndex)
- // check if we have any active placement animation on the item
- var inProgress = false
- repeat(item.placeablesCount) {
- if (item.getParentData(it).node?.isAnimationInProgress == true) {
- inProgress = true
- return@repeat
- }
- }
- if ((!inProgress && newIndex == previousKeyToIndexMap.getIndex(key))) {
- activeKeys.remove(key)
- } else {
- if (newIndex < firstVisibleIndex) {
- movingAwayToStartBound.add(item)
- } else {
- movingAwayToEndBound.add(item)
- }
- }
- }
- }
-
- accumulatedOffset = 0
- movingAwayToStartBound.sortByDescending { keyIndexMap.getIndex(it.key) }
- movingAwayToStartBound.fastForEach { item ->
- accumulatedOffset += item.size
- val mainAxisOffset = 0 - accumulatedOffset
-
- item.position(mainAxisOffset, layoutWidth, layoutHeight)
- if (shouldSetupAnimation) {
- startAnimationsIfNeeded(item)
- }
- }
-
- accumulatedOffset = 0
- movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) }
- movingAwayToEndBound.fastForEach { item ->
- val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
- accumulatedOffset += item.size
-
- item.position(mainAxisOffset, layoutWidth, layoutHeight)
- if (shouldSetupAnimation) {
- startAnimationsIfNeeded(item)
- }
- }
-
- // This adds the new items to the list of positioned items while keeping the index of
- // the positioned items sorted in ascending order.
- positionedItems.addAll(0, movingAwayToStartBound.apply { reverse() })
- positionedItems.addAll(movingAwayToEndBound)
-
- movingInFromStartBound.clear()
- movingInFromEndBound.clear()
- movingAwayToStartBound.clear()
- movingAwayToEndBound.clear()
- movingAwayKeys.clear()
- }
-
- /**
- * Should be called when the animations are not needed for the next positions change, for
- * example when we snap to a new position.
- */
- fun reset() {
- activeKeys.clear()
- keyIndexMap = LazyLayoutKeyIndexMap.Empty
- firstVisibleIndex = -1
- }
-
- private fun initializeNode(item: LazyListMeasuredItem, mainAxisOffset: Int) {
- val firstPlaceableOffset = item.getOffset(0)
-
- val targetFirstPlaceableOffset =
- if (item.isVertical) {
- firstPlaceableOffset.copy(y = mainAxisOffset)
- } else {
- firstPlaceableOffset.copy(x = mainAxisOffset)
- }
-
- // initialize offsets
- item.forEachNode { placeableIndex, node ->
- val diffToFirstPlaceableOffset = item.getOffset(placeableIndex) - firstPlaceableOffset
- node.rawOffset = targetFirstPlaceableOffset + diffToFirstPlaceableOffset
- }
- }
-
- private fun startAnimationsIfNeeded(item: LazyListMeasuredItem) {
- item.forEachNode { placeableIndex, node ->
- val newTarget = item.getOffset(placeableIndex)
- val currentTarget = node.rawOffset
- if (
- currentTarget != LazyLayoutAnimateItemModifierNode.NotInitialized &&
- currentTarget != newTarget
- ) {
- node.animatePlacementDelta(newTarget - currentTarget)
- }
- node.rawOffset = newTarget
- }
- }
-
- private val Any?.node
- get() = this as? LazyLayoutAnimateItemModifierNode
-
- private val LazyListMeasuredItem.hasAnimations: Boolean
- get() {
- forEachNode { _, _ ->
- return true
- }
- return false
- }
-
- private inline fun LazyListMeasuredItem.forEachNode(
- block: (placeableIndex: Int, node: LazyLayoutAnimateItemModifierNode) -> Unit
- ) {
- repeat(placeablesCount) { index -> getParentData(index).node?.let { block(index, it) } }
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
deleted file mode 100644
index 8def104..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.referentialEqualityPolicy
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
-import androidx.tv.foundation.lazy.layout.NearestRangeKeyIndexMap
-
-@ExperimentalFoundationApi
-internal interface LazyListItemProvider : LazyLayoutItemProvider {
- val keyIndexMap: LazyLayoutKeyIndexMap
- /** The list of indexes of the sticky header items */
- val headerIndexes: List<Int>
- /** The scope used by the item content lambdas */
- val itemScope: TvLazyListItemScopeImpl
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-internal fun rememberLazyListItemProviderLambda(
- state: TvLazyListState,
- content: TvLazyListScope.() -> Unit
-): () -> LazyListItemProvider {
- val latestContent = rememberUpdatedState(content)
- return remember(state) {
- val scope = TvLazyListItemScopeImpl()
- val intervalContentState =
- derivedStateOf(referentialEqualityPolicy()) {
- TvLazyListIntervalContent(latestContent.value)
- }
- val itemProviderState =
- derivedStateOf(referentialEqualityPolicy()) {
- val intervalContent = intervalContentState.value
- val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent)
- LazyListItemProviderImpl(
- state = state,
- intervalContent = intervalContent,
- itemScope = scope,
- keyIndexMap = map
- )
- }
- itemProviderState::value
- }
-}
-
-@ExperimentalFoundationApi
-private class LazyListItemProviderImpl
-constructor(
- private val state: TvLazyListState,
- private val intervalContent: TvLazyListIntervalContent,
- override val itemScope: TvLazyListItemScopeImpl,
- override val keyIndexMap: LazyLayoutKeyIndexMap,
-) : LazyListItemProvider {
-
- override val itemCount: Int
- get() = intervalContent.itemCount
-
- @Composable
- override fun Item(index: Int, key: Any) {
- LazyLayoutPinnableItem(key, index, state.pinnedItems) {
- intervalContent.withInterval(index) { localIndex, content ->
- content.item(itemScope, localIndex)
- }
- }
- }
-
- override fun getKey(index: Int): Any =
- keyIndexMap.getKey(index) ?: intervalContent.getKey(index)
-
- override fun getContentType(index: Int): Any? = intervalContent.getContentType(index)
-
- override val headerIndexes: List<Int>
- get() = intervalContent.headerIndexes
-
- override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key)
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is LazyListItemProviderImpl) return false
-
- // the identity of this class is represented by intervalContent object.
- // having equals() allows us to skip items recomposition when intervalContent didn't change
- return intervalContent == other.intervalContent
- }
-
- override fun hashCode(): Int {
- return intervalContent.hashCode()
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
deleted file mode 100644
index 278ab4c..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
+++ /dev/null
@@ -1,595 +0,0 @@
-/*
- * Copyright 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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachReversed
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.contract
-import kotlin.math.abs
-import kotlin.math.roundToInt
-import kotlin.math.sign
-
-/**
- * Measures and calculates the positions for the requested items. The result is produced as a
- * [LazyListMeasureResult] which contains all the calculations.
- */
-@OptIn(ExperimentalFoundationApi::class)
-internal fun measureLazyList(
- itemsCount: Int,
- measuredItemProvider: LazyListMeasuredItemProvider,
- mainAxisAvailableSize: Int,
- beforeContentPadding: Int,
- afterContentPadding: Int,
- spaceBetweenItems: Int,
- firstVisibleItemIndex: Int,
- firstVisibleItemScrollOffset: Int,
- scrollToBeConsumed: Float,
- constraints: Constraints,
- isVertical: Boolean,
- headerIndexes: List<Int>,
- verticalArrangement: Arrangement.Vertical?,
- horizontalArrangement: Arrangement.Horizontal?,
- reverseLayout: Boolean,
- density: Density,
- placementAnimator: LazyListItemPlacementAnimator,
- beyondBoundsItemCount: Int,
- pinnedItems: List<Int>,
- hasLookaheadPassOccurred: Boolean,
- isLookingAhead: Boolean,
- postLookaheadLayoutInfo: TvLazyListLayoutInfo?,
- layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
-): LazyListMeasureResult {
- require(beforeContentPadding >= 0) { "invalid beforeContentPadding" }
- require(afterContentPadding >= 0) { "invalid afterContentPadding" }
- if (itemsCount <= 0) {
- // empty data set. reset the current scroll and report zero size
- return LazyListMeasureResult(
- firstVisibleItem = null,
- firstVisibleItemScrollOffset = 0,
- canScrollForward = false,
- consumedScroll = 0f,
- measureResult = layout(constraints.minWidth, constraints.minHeight) {},
- scrollBackAmount = 0f,
- visibleItemsInfo = emptyList(),
- viewportStartOffset = -beforeContentPadding,
- viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
- totalItemsCount = 0,
- reverseLayout = reverseLayout,
- orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
- afterContentPadding = afterContentPadding,
- mainAxisItemSpacing = spaceBetweenItems
- )
- } else {
- var currentFirstItemIndex = firstVisibleItemIndex
- var currentFirstItemScrollOffset = firstVisibleItemScrollOffset
- if (currentFirstItemIndex >= itemsCount) {
- // the data set has been updated and now we have less items that we were
- // scrolled to before
- currentFirstItemIndex = itemsCount - 1
- currentFirstItemScrollOffset = 0
- }
-
- // represents the real amount of scroll we applied as a result of this measure pass.
- var scrollDelta = scrollToBeConsumed.roundToInt()
-
- // applying the whole requested scroll offset. we will figure out if we can't consume
- // all of it later
- currentFirstItemScrollOffset -= scrollDelta
-
- // if the current scroll offset is less than minimally possible
- if (currentFirstItemIndex == 0 && currentFirstItemScrollOffset < 0) {
- scrollDelta += currentFirstItemScrollOffset
- currentFirstItemScrollOffset = 0
- }
-
- // this will contain all the MeasuredItems representing the visible items
- val visibleItems = ArrayDeque<LazyListMeasuredItem>()
-
- // define min and max offsets
- val minOffset = -beforeContentPadding + if (spaceBetweenItems < 0) spaceBetweenItems else 0
- val maxOffset = mainAxisAvailableSize
-
- // include the start padding so we compose items in the padding area and neutralise item
- // spacing (if the spacing is negative this will make sure the previous item is composed)
- // before starting scrolling forward we will remove it back
- currentFirstItemScrollOffset += minOffset
-
- // max of cross axis sizes of all visible items
- var maxCrossAxis = 0
-
- // we had scrolled backward or we compose items in the start padding area, which means
- // items before current firstItemScrollOffset should be visible. compose them and update
- // firstItemScrollOffset
- while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > 0) {
- val previous = currentFirstItemIndex - 1
- val measuredItem = measuredItemProvider.getAndMeasure(previous)
- visibleItems.add(0, measuredItem)
- maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
- currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
- currentFirstItemIndex = previous
- }
-
- // if we were scrolled backward, but there were not enough items before. this means
- // not the whole scroll was consumed
- if (currentFirstItemScrollOffset < minOffset) {
- scrollDelta += currentFirstItemScrollOffset
- currentFirstItemScrollOffset = minOffset
- }
-
- // neutralize previously added padding as we stopped filling the before content padding
- currentFirstItemScrollOffset -= minOffset
-
- var index = currentFirstItemIndex
- val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
- var currentMainAxisOffset = -currentFirstItemScrollOffset
-
- // first we need to skip items we already composed while composing backward
- visibleItems.fastForEach {
- index++
- currentMainAxisOffset += it.sizeWithSpacings
- }
-
- // then composing visible items forward until we fill the whole viewport.
- // we want to have at least one item in visibleItems even if in fact all the items are
- // offscreen, this can happen if the content padding is larger than the available size.
- while (
- index < itemsCount &&
- (currentMainAxisOffset < maxMainAxis ||
- currentMainAxisOffset <= 0 || // filling beforeContentPadding area
- visibleItems.isEmpty())
- ) {
- val measuredItem = measuredItemProvider.getAndMeasure(index)
- currentMainAxisOffset += measuredItem.sizeWithSpacings
-
- if (currentMainAxisOffset <= minOffset && index != itemsCount - 1) {
- // this item is offscreen and will not be placed. advance firstVisibleItemIndex
- currentFirstItemIndex = index + 1
- currentFirstItemScrollOffset -= measuredItem.sizeWithSpacings
- } else {
- maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
- visibleItems.add(measuredItem)
- }
-
- index++
- }
-
- val preScrollBackScrollDelta = scrollDelta
- // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
- // lets try to scroll back if we have enough items before firstVisibleItemIndex.
- if (currentMainAxisOffset < maxOffset) {
- val toScrollBack = maxOffset - currentMainAxisOffset
- currentFirstItemScrollOffset -= toScrollBack
- currentMainAxisOffset += toScrollBack
- while (
- currentFirstItemScrollOffset < beforeContentPadding && currentFirstItemIndex > 0
- ) {
- val previousIndex = currentFirstItemIndex - 1
- val measuredItem = measuredItemProvider.getAndMeasure(previousIndex)
- visibleItems.add(0, measuredItem)
- maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
- currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
- currentFirstItemIndex = previousIndex
- }
- scrollDelta += toScrollBack
- if (currentFirstItemScrollOffset < 0) {
- scrollDelta += currentFirstItemScrollOffset
- currentMainAxisOffset += currentFirstItemScrollOffset
- currentFirstItemScrollOffset = 0
- }
- }
-
- // report the amount of pixels we consumed. scrollDelta can be smaller than
- // scrollToBeConsumed if there were not enough items to fill the offered space or it
- // can be larger if items were resized, or if, for example, we were previously
- // displaying the item 15, but now we have only 10 items in total in the data set.
- val consumedScroll =
- if (
- scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
- abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
- ) {
- scrollDelta.toFloat()
- } else {
- scrollToBeConsumed
- }
-
- val unconsumedScroll = scrollToBeConsumed - consumedScroll
- // When scrolling to the bottom via gesture, there could be scrollback due to
- // not being able to consume the whole scroll. In that case, the amount of
- // scrollBack is the inverse of unconsumed scroll.
- val scrollBackAmount: Float =
- if (isLookingAhead && scrollDelta > preScrollBackScrollDelta && unconsumedScroll <= 0) {
- scrollDelta - preScrollBackScrollDelta + unconsumedScroll
- } else 0f
-
- // the initial offset for items from visibleItems list
- require(currentFirstItemScrollOffset >= 0) { "negative currentFirstItemScrollOffset" }
- val visibleItemsScrollOffset = -currentFirstItemScrollOffset
- var firstItem = visibleItems.first()
-
- // even if we compose items to fill before content padding we should ignore items fully
- // located there for the state's scroll position calculation (first item + first offset)
- if (beforeContentPadding > 0 || spaceBetweenItems < 0) {
- for (i in visibleItems.indices) {
- val size = visibleItems[i].sizeWithSpacings
- if (
- currentFirstItemScrollOffset != 0 &&
- size <= currentFirstItemScrollOffset &&
- i != visibleItems.lastIndex
- ) {
- currentFirstItemScrollOffset -= size
- firstItem = visibleItems[i + 1]
- } else {
- break
- }
- }
- }
-
- // Compose extra items before
- val extraItemsBefore =
- createItemsBeforeList(
- currentFirstItemIndex = currentFirstItemIndex,
- measuredItemProvider = measuredItemProvider,
- beyondBoundsItemCount = beyondBoundsItemCount,
- pinnedItems = pinnedItems
- )
-
- // Update maxCrossAxis with extra items
- extraItemsBefore.fastForEach { maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize) }
-
- // Compose items after last item
- val extraItemsAfter =
- createItemsAfterList(
- visibleItems = visibleItems,
- measuredItemProvider = measuredItemProvider,
- itemsCount = itemsCount,
- beyondBoundsItemCount = beyondBoundsItemCount,
- pinnedItems = pinnedItems,
- consumedScroll = consumedScroll,
- isLookingAhead = isLookingAhead,
- lastPostLookaheadLayoutInfo = postLookaheadLayoutInfo
- )
-
- // Update maxCrossAxis with extra items
- extraItemsAfter.fastForEach { maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize) }
-
- val noExtraItems =
- firstItem == visibleItems.first() &&
- extraItemsBefore.isEmpty() &&
- extraItemsAfter.isEmpty()
-
- val layoutWidth =
- constraints.constrainWidth(if (isVertical) maxCrossAxis else currentMainAxisOffset)
- val layoutHeight =
- constraints.constrainHeight(if (isVertical) currentMainAxisOffset else maxCrossAxis)
-
- val positionedItems =
- calculateItemsOffsets(
- items = visibleItems,
- extraItemsBefore = extraItemsBefore,
- extraItemsAfter = extraItemsAfter,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- finalMainAxisOffset = currentMainAxisOffset,
- maxOffset = maxOffset,
- itemsScrollOffset = visibleItemsScrollOffset,
- isVertical = isVertical,
- verticalArrangement = verticalArrangement,
- horizontalArrangement = horizontalArrangement,
- reverseLayout = reverseLayout,
- density = density,
- )
-
- placementAnimator.onMeasured(
- consumedScroll = consumedScroll.toInt(),
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- positionedItems = positionedItems,
- itemProvider = measuredItemProvider,
- isVertical = isVertical,
- isLookingAhead = isLookingAhead,
- hasLookaheadOccurred = hasLookaheadPassOccurred
- )
-
- val headerItem =
- if (headerIndexes.isNotEmpty()) {
- findOrComposeLazyListHeader(
- composedVisibleItems = positionedItems,
- itemProvider = measuredItemProvider,
- headerIndexes = headerIndexes,
- beforeContentPadding = beforeContentPadding,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight
- )
- } else {
- null
- }
-
- return LazyListMeasureResult(
- firstVisibleItem = firstItem,
- firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
- canScrollForward = index < itemsCount || currentMainAxisOffset > maxOffset,
- consumedScroll = consumedScroll,
- measureResult =
- layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach {
- if (it !== headerItem) {
- it.place(this, isLookingAhead)
- }
- }
- // the header item should be placed (drawn) after all other items
- headerItem?.place(this, isLookingAhead)
- },
- scrollBackAmount = scrollBackAmount,
- visibleItemsInfo =
- if (noExtraItems) positionedItems
- else
- positionedItems.fastFilter {
- (it.index >= visibleItems.first().index &&
- it.index <= visibleItems.last().index) || it === headerItem
- },
- viewportStartOffset = -beforeContentPadding,
- viewportEndOffset = maxOffset + afterContentPadding,
- totalItemsCount = itemsCount,
- reverseLayout = reverseLayout,
- orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
- afterContentPadding = afterContentPadding,
- mainAxisItemSpacing = spaceBetweenItems
- )
- }
-}
-
-private fun createItemsAfterList(
- visibleItems: MutableList<LazyListMeasuredItem>,
- measuredItemProvider: LazyListMeasuredItemProvider,
- itemsCount: Int,
- beyondBoundsItemCount: Int,
- pinnedItems: List<Int>,
- consumedScroll: Float,
- isLookingAhead: Boolean,
- lastPostLookaheadLayoutInfo: TvLazyListLayoutInfo?
-): List<LazyListMeasuredItem> {
- var list: MutableList<LazyListMeasuredItem>? = null
-
- var end = visibleItems.last().index
-
- end = minOf(end + beyondBoundsItemCount, itemsCount - 1)
-
- for (i in visibleItems.last().index + 1..end) {
- if (list == null) list = mutableListOf()
- list.add(measuredItemProvider.getAndMeasure(i))
- }
-
- pinnedItems.fastForEach { index ->
- if (index > end) {
- if (list == null) list = mutableListOf()
- list?.add(measuredItemProvider.getAndMeasure(index))
- }
- }
-
- if (isLookingAhead) {
- // Check if there's any item that needs to be composed based on last postLookaheadLayoutInfo
- if (
- lastPostLookaheadLayoutInfo != null &&
- lastPostLookaheadLayoutInfo.visibleItemsInfo.isNotEmpty()
- ) {
- // Find first item with index > end. Note that `visibleItemsInfo.last()` may not have
- // the largest index as the last few items could be added to animate item placement.
- val firstItem =
- lastPostLookaheadLayoutInfo.visibleItemsInfo.run {
- var found: TvLazyListItemInfo? = null
- for (i in size - 1 downTo 0) {
- if (this[i].index > end && (i == 0 || this[i - 1].index <= end)) {
- found = this[i]
- break
- }
- }
- found
- }
- val lastVisibleItem = lastPostLookaheadLayoutInfo.visibleItemsInfo.last()
- if (firstItem != null) {
- for (i in firstItem.index..lastVisibleItem.index) {
- if (list?.fastAny { it.index == i } != null) {
- if (list == null) list = mutableListOf()
- list?.add(measuredItemProvider.getAndMeasure(i))
- }
- }
- }
-
- // Calculate the additional offset to subcompose based on what was shown in the
- // previous post-loookahead pass and the scroll consumed.
- val additionalOffset =
- lastPostLookaheadLayoutInfo.viewportEndOffset -
- lastVisibleItem.offset -
- lastVisibleItem.size -
- consumedScroll
- if (additionalOffset > 0) {
- var index = lastVisibleItem.index + 1
- var totalOffset = 0
- while (index < itemsCount && totalOffset < additionalOffset) {
- val item =
- if (index <= end) {
- visibleItems.fastFirstOrNull { it.index == index }
- } else null ?: list?.fastFirstOrNull { it.index == index }
- if (item != null) {
- index++
- totalOffset += item.sizeWithSpacings
- } else {
- if (list == null) list = mutableListOf()
- list?.add(measuredItemProvider.getAndMeasure(index))
- index++
- totalOffset += list!!.last().sizeWithSpacings
- }
- }
- }
- }
- }
-
- return list ?: emptyList()
-}
-
-private fun createItemsBeforeList(
- currentFirstItemIndex: Int,
- measuredItemProvider: LazyListMeasuredItemProvider,
- beyondBoundsItemCount: Int,
- pinnedItems: List<Int>
-): List<LazyListMeasuredItem> {
- var list: MutableList<LazyListMeasuredItem>? = null
-
- var start = currentFirstItemIndex
-
- start = maxOf(0, start - beyondBoundsItemCount)
-
- for (i in currentFirstItemIndex - 1 downTo start) {
- if (list == null) list = mutableListOf()
- list.add(measuredItemProvider.getAndMeasure(i))
- }
-
- pinnedItems.fastForEachReversed { index ->
- if (index < start) {
- if (list == null) list = mutableListOf()
- list?.add(measuredItemProvider.getAndMeasure(index))
- }
- }
-
- return list ?: emptyList()
-}
-
-/** Calculates [LazyListMeasuredItem]s offsets. */
-private fun calculateItemsOffsets(
- items: List<LazyListMeasuredItem>,
- extraItemsBefore: List<LazyListMeasuredItem>,
- extraItemsAfter: List<LazyListMeasuredItem>,
- layoutWidth: Int,
- layoutHeight: Int,
- finalMainAxisOffset: Int,
- maxOffset: Int,
- itemsScrollOffset: Int,
- isVertical: Boolean,
- verticalArrangement: Arrangement.Vertical?,
- horizontalArrangement: Arrangement.Horizontal?,
- reverseLayout: Boolean,
- density: Density,
-): MutableList<LazyListMeasuredItem> {
- val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
- val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
- if (hasSpareSpace) {
- check(itemsScrollOffset == 0) { "non-zero itemsScrollOffset" }
- }
-
- val positionedItems =
- ArrayList<LazyListMeasuredItem>(items.size + extraItemsBefore.size + extraItemsAfter.size)
-
- if (hasSpareSpace) {
- require(extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) { "no extra items" }
-
- val itemsCount = items.size
- fun Int.reverseAware() = if (!reverseLayout) this else itemsCount - this - 1
-
- val sizes = IntArray(itemsCount) { index -> items[index.reverseAware()].size }
- val offsets = IntArray(itemsCount) { 0 }
- if (isVertical) {
- with(
- requireNotNull(verticalArrangement) {
- "null verticalArrangement when isVertical == true"
- }
- ) {
- density.arrange(mainAxisLayoutSize, sizes, offsets)
- }
- } else {
- with(
- requireNotNull(horizontalArrangement) {
- "null horizontalArrangement when isVertical == false"
- }
- ) {
- // Enforces Ltr layout direction as it is mirrored with placeRelative later.
- density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
- }
- }
-
- val reverseAwareOffsetIndices =
- if (!reverseLayout) offsets.indices else offsets.indices.reversed()
- for (index in reverseAwareOffsetIndices) {
- val absoluteOffset = offsets[index]
- // when reverseLayout == true, offsets are stored in the reversed order to items
- val item = items[index.reverseAware()]
- val relativeOffset =
- if (reverseLayout) {
- // inverse offset to align with scroll direction for positioning
- mainAxisLayoutSize - absoluteOffset - item.size
- } else {
- absoluteOffset
- }
- item.position(relativeOffset, layoutWidth, layoutHeight)
- positionedItems.add(item)
- }
- } else {
- var currentMainAxis = itemsScrollOffset
- extraItemsBefore.fastForEach {
- currentMainAxis -= it.sizeWithSpacings
- it.position(currentMainAxis, layoutWidth, layoutHeight)
- positionedItems.add(it)
- }
-
- currentMainAxis = itemsScrollOffset
- items.fastForEach {
- it.position(currentMainAxis, layoutWidth, layoutHeight)
- positionedItems.add(it)
- currentMainAxis += it.sizeWithSpacings
- }
-
- extraItemsAfter.fastForEach {
- it.position(currentMainAxis, layoutWidth, layoutHeight)
- positionedItems.add(it)
- currentMainAxis += it.sizeWithSpacings
- }
- }
- return positionedItems
-}
-
-/**
- * Returns a list containing only elements matching the given [predicate].
- *
- * **Do not use for collections that come from public APIs**, since they may not support random
- * access in an efficient way, and this method may actually be a lot slower. Only use for
- * collections that are created by code we control and are known to support random access.
- */
-@OptIn(ExperimentalContracts::class)
-internal fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
- contract { callsInPlace(predicate) }
- val target = ArrayList<T>(size)
- fastForEach { if (predicate(it)) target += (it) }
- return target
-}
-
-private val EmptyRange = Int.MIN_VALUE to Int.MIN_VALUE
-private val Int.notInEmptyRange
- get() = this != Int.MIN_VALUE
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt
deleted file mode 100644
index cec0874..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt
+++ /dev/null
@@ -1,63 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.unit.IntSize
-
-/** The result of the measure pass for lazy list layout. */
-internal class LazyListMeasureResult(
- // properties defining the scroll position:
- /** The new first visible item. */
- val firstVisibleItem: LazyListMeasuredItem?,
- /** The new value for [TvLazyListState.firstVisibleItemScrollOffset]. */
- val firstVisibleItemScrollOffset: Int,
- /** True if there is some space available to continue scrolling in the forward direction. */
- val canScrollForward: Boolean,
- /** The amount of scroll consumed during the measure pass. */
- val consumedScroll: Float,
- /** MeasureResult defining the layout. */
- measureResult: MeasureResult,
- /** The amount of scroll-back that happened due to reaching the end of the list. */
- val scrollBackAmount: Float,
- // properties representing the info needed for LazyListLayoutInfo:
- /** see [TvLazyListLayoutInfo.visibleItemsInfo] */
- override val visibleItemsInfo: List<TvLazyListItemInfo>,
- /** see [TvLazyListLayoutInfo.viewportStartOffset] */
- override val viewportStartOffset: Int,
- /** see [TvLazyListLayoutInfo.viewportEndOffset] */
- override val viewportEndOffset: Int,
- /** see [TvLazyListLayoutInfo.totalItemsCount] */
- override val totalItemsCount: Int,
- /** see [TvLazyListLayoutInfo.reverseLayout] */
- override val reverseLayout: Boolean,
- /** see [TvLazyListLayoutInfo.orientation] */
- override val orientation: Orientation,
- /** see [TvLazyListLayoutInfo.afterContentPadding] */
- override val afterContentPadding: Int,
- /** see [TvLazyListLayoutInfo.mainAxisItemSpacing] */
- override val mainAxisItemSpacing: Int
-) : TvLazyListLayoutInfo, MeasureResult by measureResult {
- override val viewportSize: IntSize
- get() = IntSize(width, height)
-
- override val beforeContentPadding: Int
- get() = -viewportStartOffset
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasuredItem.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasuredItem.kt
deleted file mode 100644
index f481255..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasuredItem.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachIndexed
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.lazy.grid.LazyLayoutAnimateItemModifierNode
-
-/**
- * Represents one measured item of the lazy list. It can in fact consist of multiple placeables if
- * the user emit multiple layout nodes in the item callback.
- */
-internal class LazyListMeasuredItem
-@ExperimentalTvFoundationApi
-constructor(
- override val index: Int,
- private val placeables: List<Placeable>,
- val isVertical: Boolean,
- private val horizontalAlignment: Alignment.Horizontal?,
- private val verticalAlignment: Alignment.Vertical?,
- private val layoutDirection: LayoutDirection,
- private val reverseLayout: Boolean,
- private val beforeContentPadding: Int,
- private val afterContentPadding: Int,
- /**
- * Extra spacing to be added to [size] aside from the sum of the [placeables] size. It is
- * usually representing the spacing after the item.
- */
- private val spacing: Int,
- /**
- * The offset which shouldn't affect any calculations but needs to be applied for the final
- * value passed into the place() call.
- */
- private val visualOffset: IntOffset,
- override val key: Any,
- override val contentType: Any?
-) : TvLazyListItemInfo {
- override var offset: Int = 0
- private set
-
- /** Sum of the main axis sizes of all the inner placeables. */
- override val size: Int
-
- /** Sum of the main axis sizes of all the inner placeables and [spacing]. */
- val sizeWithSpacings: Int
-
- /** Max of the cross axis sizes of all the inner placeables. */
- val crossAxisSize: Int
-
- private var mainAxisLayoutSize: Int = Unset
- private var minMainAxisOffset: Int = 0
- private var maxMainAxisOffset: Int = 0
-
- // optimized for storing x and y offsets for each placeable one by one.
- // array's size == placeables.size * 2, first we store x, then y.
- private val placeableOffsets: IntArray
-
- init {
- var mainAxisSize = 0
- var maxCrossAxis = 0
- placeables.fastForEach {
- mainAxisSize += if (isVertical) it.height else it.width
- maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
- }
- size = mainAxisSize
- sizeWithSpacings = (size + spacing).coerceAtLeast(0)
- crossAxisSize = maxCrossAxis
- placeableOffsets = IntArray(placeables.size * 2)
- }
-
- val placeablesCount: Int
- get() = placeables.size
-
- fun getParentData(index: Int) = placeables[index].parentData
-
- /**
- * Calculates positions for the inner placeables at [offset] main axis position. If
- * [reverseOrder] is true the inner placeables would be placed in the inverted order.
- */
- fun position(offset: Int, layoutWidth: Int, layoutHeight: Int) {
- this.offset = offset
- mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
- var mainAxisOffset = offset
- placeables.fastForEachIndexed { index, placeable ->
- val indexInArray = index * 2
- if (isVertical) {
- placeableOffsets[indexInArray] =
- requireNotNull(horizontalAlignment) {
- "null horizontalAlignment when isVertical == true"
- }
- .align(placeable.width, layoutWidth, layoutDirection)
- placeableOffsets[indexInArray + 1] = mainAxisOffset
- mainAxisOffset += placeable.height
- } else {
- placeableOffsets[indexInArray] = mainAxisOffset
- placeableOffsets[indexInArray + 1] =
- requireNotNull(verticalAlignment) {
- "null verticalAlignment when isVertical == false"
- }
- .align(placeable.height, layoutHeight)
- mainAxisOffset += placeable.width
- }
- }
- minMainAxisOffset = -beforeContentPadding
- maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
- }
-
- fun getOffset(index: Int) =
- IntOffset(placeableOffsets[index * 2], placeableOffsets[index * 2 + 1])
-
- fun place(scope: Placeable.PlacementScope, isLookingAhead: Boolean) =
- with(scope) {
- require(mainAxisLayoutSize != Unset) { "position() should be called first" }
- repeat(placeablesCount) { index ->
- val placeable = placeables[index]
- val minOffset = minMainAxisOffset - placeable.mainAxisSize
- val maxOffset = maxMainAxisOffset
- var offset = getOffset(index)
- val animateNode = getParentData(index) as? LazyLayoutAnimateItemModifierNode
- if (animateNode != null) {
- if (isLookingAhead) {
- // Skip animation in lookahead pass
- animateNode.lookaheadOffset = offset
- } else {
- val targetOffset =
- if (
- animateNode.lookaheadOffset !=
- LazyLayoutAnimateItemModifierNode.NotInitialized
- ) {
- animateNode.lookaheadOffset
- } else {
- offset
- }
- val animatedOffset = targetOffset + animateNode.placementDelta
- // cancel the animation if current and target offsets are both out of the
- // bounds
- if (
- (targetOffset.mainAxis <= minOffset &&
- animatedOffset.mainAxis <= minOffset) ||
- (targetOffset.mainAxis >= maxOffset &&
- animatedOffset.mainAxis >= maxOffset)
- ) {
- animateNode.cancelAnimation()
- }
- offset = animatedOffset
- }
- }
- if (reverseLayout) {
- offset =
- offset.copy { mainAxisOffset ->
- mainAxisLayoutSize - mainAxisOffset - placeable.mainAxisSize
- }
- }
- offset += visualOffset
- if (isVertical) {
- placeable.placeWithLayer(offset)
- } else {
- placeable.placeRelativeWithLayer(offset)
- }
- }
- }
-
- private val IntOffset.mainAxis
- get() = if (isVertical) y else x
-
- private val Placeable.mainAxisSize
- get() = if (isVertical) height else width
-
- private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
- IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
-}
-
-private const val Unset = Int.MIN_VALUE
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasuredItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasuredItemProvider.kt
deleted file mode 100644
index 4af87b7..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasuredItemProvider.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.Constraints
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.lazy.layout.LazyLayoutKeyIndexMap
-
-/** Abstracts away the subcomposition from the measuring logic. */
-@OptIn(ExperimentalFoundationApi::class)
-internal abstract class LazyListMeasuredItemProvider
-@ExperimentalTvFoundationApi
-constructor(
- constraints: Constraints,
- isVertical: Boolean,
- private val itemProvider: LazyListItemProvider,
- private val measureScope: LazyLayoutMeasureScope
-) {
- // the constraints we will measure child with. the main axis is not restricted
- val childConstraints =
- Constraints(
- maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
- maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
- )
-
- /**
- * Used to subcompose items of lazy lists. Composed placeables will be measured with the correct
- * constraints and wrapped into [LazyListMeasuredItem].
- */
- fun getAndMeasure(index: Int): LazyListMeasuredItem {
- val key = itemProvider.getKey(index)
- val contentType = itemProvider.getContentType(index)
- val placeables = measureScope.measure(index, childConstraints)
- return createItem(index, key, contentType, placeables)
- }
-
- /**
- * Contains the mapping between the key and the index. It could contain not all the items of the
- * list as an optimization.
- */
- val keyIndexMap: LazyLayoutKeyIndexMap
- get() = itemProvider.keyIndexMap
-
- abstract fun createItem(
- index: Int,
- key: Any,
- contentType: Any?,
- placeables: List<Placeable>
- ): LazyListMeasuredItem
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
deleted file mode 100644
index 913b45f..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.setValue
-import androidx.tv.foundation.lazy.layout.LazyLayoutNearestRangeState
-
-/**
- * Contains the current scroll position represented by the first visible item index and the first
- * visible item scroll offset.
- */
-internal class LazyListScrollPosition(initialIndex: Int = 0, initialScrollOffset: Int = 0) {
- var index by mutableIntStateOf(initialIndex)
-
- var scrollOffset by mutableIntStateOf(initialScrollOffset)
- private set
-
- private var hadFirstNotEmptyLayout = false
-
- /** The last know key of the item at [index] position. */
- private var lastKnownFirstItemKey: Any? = null
-
- val nearestRangeState =
- LazyLayoutNearestRangeState(
- initialIndex,
- NearestItemsSlidingWindowSize,
- NearestItemsExtraItemCount
- )
-
- /** Updates the current scroll position based on the results of the last measurement. */
- fun updateFromMeasureResult(measureResult: LazyListMeasureResult) {
- lastKnownFirstItemKey = measureResult.firstVisibleItem?.key
- // we ignore the index and offset from measureResult until we get at least one
- // measurement with real items. otherwise the initial index and scroll passed to the
- // state would be lost and overridden with zeros.
- if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
- hadFirstNotEmptyLayout = true
- val scrollOffset = measureResult.firstVisibleItemScrollOffset
- check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
- val firstIndex = measureResult.firstVisibleItem?.index ?: 0
- update(firstIndex, scrollOffset)
- }
- }
-
- /**
- * Updates the scroll position - the passed values will be used as a start position for
- * composing the items during the next measure pass and will be updated by the real position
- * calculated during the measurement. This means that there is no guarantee that exactly this
- * index and offset will be applied as it is possible that: a) there will be no item at this
- * index in reality b) item at this index will be smaller than the asked scrollOffset, which
- * means we would switch to the next item c) there will be not enough items to fill the viewport
- * after the requested index, so we would have to compose few elements before the asked index,
- * changing the first visible item.
- */
- fun requestPosition(index: Int, scrollOffset: Int) {
- update(index, scrollOffset)
- // clear the stored key as we have a direct request to scroll to [index] position and the
- // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
- lastKnownFirstItemKey = null
- }
-
- /**
- * In addition to keeping the first visible item index we also store the key of this item. When
- * the user provided custom keys for the items this mechanism allows us to detect when there
- * were items added or removed before our current first visible item and keep this item as the
- * first visible one even given that its index has been changed.
- */
- @ExperimentalFoundationApi
- fun updateScrollPositionIfTheFirstItemWasMoved(
- itemProvider: LazyListItemProvider,
- index: Int
- ): Int {
- val newIndex = itemProvider.findIndexByKey(lastKnownFirstItemKey, index)
- if (index != newIndex) {
- this.index = newIndex
- nearestRangeState.update(index)
- }
- return newIndex
- }
-
- private fun update(index: Int, scrollOffset: Int) {
- require(index >= 0f) { "Index should be non-negative ($index)" }
- this.index = index
- nearestRangeState.update(index)
- this.scrollOffset = scrollOffset
- }
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-internal const val NearestItemsSlidingWindowSize = 30
-
-/** The minimum amount of items near the current first visible item we want to have mapping for. */
-internal const val NearestItemsExtraItemCount = 100
-
-/**
- * Finds a position of the item with the given key in the lists. This logic allows us to detect when
- * there were items added or removed before our current first item.
- */
-@ExperimentalFoundationApi
-internal fun LazyLayoutItemProvider.findIndexByKey(
- key: Any?,
- lastKnownIndex: Int,
-): Int {
- if (key == null) {
- // there were no real item during the previous measure
- return lastKnownIndex
- }
- if (lastKnownIndex < itemCount && key == getKey(lastKnownIndex)) {
- // this item is still at the same index
- return lastKnownIndex
- }
- val newIndex = getIndex(key)
- if (newIndex != -1) {
- return newIndex
- }
- // fallback to the previous index if we don't know the new index of the item
- return lastKnownIndex
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
deleted file mode 100644
index 275629b..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
+++ /dev/null
@@ -1,512 +0,0 @@
-/*
- * Copyright 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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.copy
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
-import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.layout.Remeasurement
-import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.lazy.layout.AwaitFirstLayoutModifier
-import androidx.tv.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
-import androidx.tv.foundation.lazy.layout.animateScrollToItem
-import kotlin.math.abs
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-/**
- * Creates a [TvLazyListState] that is remembered across compositions.
- *
- * Changes to the provided initial values will **not** result in the state being recreated or
- * changed in any way if it has already been created.
- *
- * @param initialFirstVisibleItemIndex the initial value for [TvLazyListState.firstVisibleItemIndex]
- * @param initialFirstVisibleItemScrollOffset the initial value for
- * [TvLazyListState.firstVisibleItemScrollOffset]
- */
-@Deprecated(
- "Use `rememberLazyListState` instead.",
- replaceWith =
- ReplaceWith(
- "rememberLazyListState(" +
- "initialFirstVisibleItemIndex = initialFirstVisibleItemIndex, " +
- "initialFirstVisibleItemScrollOffset = initialFirstVisibleItemScrollOffset" +
- ")",
- imports = arrayOf("androidx.compose.foundation.lazy.rememberLazyListState")
- )
-)
-@Composable
-fun rememberTvLazyListState(
- initialFirstVisibleItemIndex: Int = 0,
- initialFirstVisibleItemScrollOffset: Int = 0
-): TvLazyListState {
- return rememberSaveable(saver = TvLazyListState.Saver) {
- TvLazyListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset)
- }
-}
-
-/**
- * A state object that can be hoisted to control and observe scrolling.
- *
- * In most cases, this will be created via [rememberTvLazyListState].
- *
- * @param firstVisibleItemIndex the initial value for [TvLazyListState.firstVisibleItemIndex]
- * @param firstVisibleItemScrollOffset the initial value for
- * [TvLazyListState.firstVisibleItemScrollOffset]
- */
-@Deprecated(
- "Use `LazyListState` instead.",
- replaceWith =
- ReplaceWith(
- "LazyListState(" +
- "firstVisibleItemIndex = firstVisibleItemIndex, " +
- "firstVisibleItemScrollOffset = firstVisibleItemScrollOffset" +
- ")",
- imports = arrayOf("androidx.compose.foundation.lazy.LazyListState")
- )
-)
-@OptIn(ExperimentalFoundationApi::class)
-@Stable
-class TvLazyListState(firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0) :
- ScrollableState {
- internal var hasLookaheadPassOccurred: Boolean = false
- private set
-
- internal var postLookaheadLayoutInfo: TvLazyListLayoutInfo? = null
- private set
-
- /** The holder class for the current scroll position. */
- private val scrollPosition =
- LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
-
- private val animateScrollScope = LazyListAnimateScrollScope(this)
-
- /**
- * The index of the first item that is visible.
- *
- * Note that this property is observable and if you use it in the composable function it will be
- * recomposed on every change causing potential performance issues.
- *
- * If you want to run some side effects like sending an analytics event or updating a state
- * based on this value consider using "snapshotFlow".
- *
- * If you need to use it in the composition then consider wrapping the calculation into a
- * derived state in order to only have recompositions when the derived value changes.
- */
- val firstVisibleItemIndex: Int
- get() = scrollPosition.index
-
- /**
- * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount
- * that the item is offset backwards.
- *
- * Note that this property is observable and if you use it in the composable function it will be
- * recomposed on every scroll causing potential performance issues.
- *
- * @see firstVisibleItemIndex for samples with the recommended usage patterns.
- */
- val firstVisibleItemScrollOffset: Int
- get() = scrollPosition.scrollOffset
-
- /** Backing state for [layoutInfo] */
- private val layoutInfoState = mutableStateOf<TvLazyListLayoutInfo>(EmptyLazyListLayoutInfo)
-
- /**
- * The object of [TvLazyListLayoutInfo] calculated during the last layout pass. For example, you
- * can use it to calculate what items are currently visible.
- *
- * Note that this property is observable and is updated after every scroll or remeasure. If you
- * use it in the composable function it will be recomposed on every change causing potential
- * performance issues including infinity recomposition loop. Therefore, avoid using it in the
- * composition.
- *
- * If you want to run some side effects like sending an analytics event or updating a state
- * based on this value consider using "snapshotFlow"
- */
- val layoutInfo: TvLazyListLayoutInfo
- get() = layoutInfoState.value
-
- /**
- * [InteractionSource] that will be used to dispatch drag events when this list is being
- * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
- * [isScrollInProgress].
- */
- val interactionSource: InteractionSource
- get() = internalInteractionSource
-
- internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
-
- /**
- * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
- * - that is, it is the amount that the items are offset in y
- */
- internal var scrollToBeConsumed = 0f
- private set
-
- /** Needed for [animateScrollToItem]. Updated on every measure. */
- internal var density: Density = Density(1f, 1f)
-
- /**
- * The ScrollableController instance. We keep it as we need to call stopAnimation on it once we
- * reached the end of the list.
- */
- private val scrollableState = ScrollableState { -onScroll(-it) }
-
- /** Only used for testing to confirm that we're not making too many measure passes */
- /*@VisibleForTesting*/
- internal var numMeasurePasses: Int = 0
- private set
-
- /** Only used for testing to disable prefetching when needed to test the main logic. */
- /*@VisibleForTesting*/
- internal var prefetchingEnabled: Boolean = true
-
- /**
- * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
- */
- private var indexToPrefetch = -1
-
- /** The handle associated with the current index from [indexToPrefetch]. */
- private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
-
- /**
- * Keeps the scrolling direction during the previous calculation in order to be able to detect
- * the scrolling direction change.
- */
- private var wasScrollingForward = false
-
- /**
- * The [Remeasurement] object associated with our layout. It allows us to remeasure
- * synchronously during scroll.
- */
- internal var remeasurement: Remeasurement? = null
- private set
-
- /** The modifier which provides [remeasurement]. */
- internal val remeasurementModifier =
- object : RemeasurementModifier {
- override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
- [email protected] = remeasurement
- }
- }
-
- /**
- * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is
- * ready.
- */
- internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
-
- internal val placementAnimator = LazyListItemPlacementAnimator()
-
- internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
-
- /** Constraints passed to the prefetcher for premeasuring the prefetched items. */
- internal var premeasureConstraints = Constraints()
-
- /** Stores currently pinned items which are always composed. */
- internal val pinnedItems = LazyLayoutPinnedItemList()
-
- internal val nearestRange: IntRange by scrollPosition.nearestRangeState
-
- /**
- * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
- * pixels.
- *
- * @param index the index to which to scroll. Must be non-negative.
- * @param scrollOffset the offset that the item should end up after the scroll. Note that
- * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
- * scroll the item further upward (taking it partly offscreen).
- */
- suspend fun scrollToItem(
- /*@IntRange(from = 0)*/
- index: Int,
- scrollOffset: Int = 0
- ) {
- scroll { snapToItemIndexInternal(index, scrollOffset) }
- }
-
- internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
- scrollPosition.requestPosition(index, scrollOffset)
- // placement animation is not needed because we snap into a new position.
- placementAnimator.reset()
- remeasurement?.forceRemeasure()
- }
-
- /**
- * Call this function to take control of scrolling and gain the ability to send scroll events
- * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
- * performed within a [scroll] block (even if they don't call any other methods on this object)
- * in order to guarantee that mutual exclusion is enforced.
- *
- * If [scroll] is called from elsewhere, this will be canceled.
- */
- override suspend fun scroll(
- scrollPriority: MutatePriority,
- block: suspend ScrollScope.() -> Unit
- ) {
- awaitLayoutModifier.waitForFirstLayout()
- scrollableState.scroll(scrollPriority, block)
- }
-
- override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
-
- override val isScrollInProgress: Boolean
- get() = scrollableState.isScrollInProgress
-
- override var canScrollForward: Boolean by mutableStateOf(false)
- private set
-
- override var canScrollBackward: Boolean by mutableStateOf(false)
- private set
-
- // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
- // fine-grained control over scrolling
- /*@VisibleForTesting*/
- internal fun onScroll(distance: Float): Float {
- if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
- return 0f
- }
- check(abs(scrollToBeConsumed) <= 0.5f) {
- "entered drag with non-zero pending scroll: $scrollToBeConsumed"
- }
- scrollToBeConsumed += distance
-
- // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
- // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
- // we have less than 0.5 pixels
- if (abs(scrollToBeConsumed) > 0.5f) {
- val preScrollToBeConsumed = scrollToBeConsumed
- remeasurement?.forceRemeasure()
- if (prefetchingEnabled) {
- notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
- }
- }
-
- // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
- if (abs(scrollToBeConsumed) <= 0.5f) {
- // We consumed all of it - we'll hold onto the fractional scroll for later, so report
- // that we consumed the whole thing
- return distance
- } else {
- val scrollConsumed = distance - scrollToBeConsumed
- // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
- // nested scrolling)
- scrollToBeConsumed = 0f // We're not consuming the rest, give it back
- return scrollConsumed
- }
- }
-
- private fun notifyPrefetch(delta: Float) {
- if (!prefetchingEnabled) {
- return
- }
- val info = layoutInfo
- if (info.visibleItemsInfo.isNotEmpty()) {
- val scrollingForward = delta < 0
- val indexToPrefetch =
- if (scrollingForward) {
- info.visibleItemsInfo.last().index + 1
- } else {
- info.visibleItemsInfo.first().index - 1
- }
- if (
- indexToPrefetch != this.indexToPrefetch &&
- indexToPrefetch in 0 until info.totalItemsCount
- ) {
- if (wasScrollingForward != scrollingForward) {
- // the scrolling direction has been changed which means the last prefetched
- // is not going to be reached anytime soon so it is safer to dispose it.
- // if this item is already visible it is safe to call the method anyway
- // as it will be no-op
- currentPrefetchHandle?.cancel()
- }
- this.wasScrollingForward = scrollingForward
- this.indexToPrefetch = indexToPrefetch
- currentPrefetchHandle =
- prefetchState.schedulePrefetch(indexToPrefetch, premeasureConstraints)
- }
- }
- }
-
- private fun cancelPrefetchIfVisibleItemsChanged(info: TvLazyListLayoutInfo) {
- if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
- val expectedPrefetchIndex =
- if (wasScrollingForward) {
- info.visibleItemsInfo.last().index + 1
- } else {
- info.visibleItemsInfo.first().index - 1
- }
- if (indexToPrefetch != expectedPrefetchIndex) {
- indexToPrefetch = -1
- currentPrefetchHandle?.cancel()
- currentPrefetchHandle = null
- }
- }
- }
-
- internal val prefetchState = LazyLayoutPrefetchState()
-
- /**
- * Animate (smooth scroll) to the given item.
- *
- * @param index the index to which to scroll. Must be non-negative.
- * @param scrollOffset the offset that the item should end up after the scroll. Note that
- * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
- * scroll the item further upward (taking it partly offscreen).
- */
- suspend fun animateScrollToItem(
- /*@IntRange(from = 0)*/
- index: Int,
- scrollOffset: Int = 0
- ) {
- animateScrollScope.animateScrollToItem(index, scrollOffset)
- }
-
- /** Updates the state with the new calculated scroll position and consumed scroll. */
- internal fun applyMeasureResult(result: LazyListMeasureResult, isLookingAhead: Boolean) {
- if (!isLookingAhead && hasLookaheadPassOccurred) {
- // If there was already a lookahead pass, record this result as postLookahead result
- postLookaheadLayoutInfo = result
- } else {
- if (isLookingAhead) {
- hasLookaheadPassOccurred = true
- }
- scrollPosition.updateFromMeasureResult(result)
- scrollToBeConsumed -= result.consumedScroll
- layoutInfoState.value = result
-
- canScrollForward = result.canScrollForward
- canScrollBackward =
- (result.firstVisibleItem?.index ?: 0) != 0 ||
- result.firstVisibleItemScrollOffset != 0
-
- if (isLookingAhead) updateScrollDeltaForPostLookahead(result.scrollBackAmount)
- numMeasurePasses++
-
- cancelPrefetchIfVisibleItemsChanged(result)
- }
- }
-
- internal var coroutineScope: CoroutineScope? = null
-
- internal val scrollDeltaBetweenPasses: Float
- get() = _scrollDeltaBetweenPasses.value
-
- private var _scrollDeltaBetweenPasses: AnimationState<Float, AnimationVector1D> =
- AnimationState(Float.VectorConverter, 0f, 0f)
-
- // Updates the scroll delta between lookahead & post-lookahead pass
- private fun updateScrollDeltaForPostLookahead(delta: Float) {
- if (delta <= with(density) { DeltaThresholdForScrollAnimation.toPx() }) {
- // If the delta is within the threshold, scroll by the delta amount instead of animating
- return
- }
-
- // Scroll delta is updated during lookahead, we don't need to trigger lookahead when
- // the delta changes.
- Snapshot.withoutReadObservation {
- val currentDelta = _scrollDeltaBetweenPasses.value
-
- if (_scrollDeltaBetweenPasses.isRunning) {
- _scrollDeltaBetweenPasses = _scrollDeltaBetweenPasses.copy(currentDelta - delta)
- coroutineScope?.launch {
- _scrollDeltaBetweenPasses.animateTo(
- 0f,
- spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 0.5f),
- true
- )
- }
- } else {
- _scrollDeltaBetweenPasses = AnimationState(Float.VectorConverter, -delta)
- coroutineScope?.launch {
- _scrollDeltaBetweenPasses.animateTo(
- 0f,
- spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 0.5f),
- true
- )
- }
- }
- }
- }
-
- /**
- * When the user provided custom keys for the items we can try to detect when there were items
- * added or removed before our current first visible item and keep this item as the first
- * visible one even given that its index has been changed.
- */
- internal fun updateScrollPositionIfTheFirstItemWasMoved(
- itemProvider: LazyListItemProvider,
- firstItemIndex: Int = Snapshot.withoutReadObservation { scrollPosition.index }
- ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
-
- companion object {
- /** The default [Saver] implementation for [TvLazyListState]. */
- val Saver: Saver<TvLazyListState, *> =
- listSaver(
- save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
- restore = {
- TvLazyListState(
- firstVisibleItemIndex = it[0],
- firstVisibleItemScrollOffset = it[1]
- )
- }
- )
- }
-}
-
-private val DeltaThresholdForScrollAnimation = 1.dp
-
-private object EmptyLazyListLayoutInfo : TvLazyListLayoutInfo {
- override val visibleItemsInfo = emptyList<TvLazyListItemInfo>()
- override val viewportStartOffset = 0
- override val viewportEndOffset = 0
- override val totalItemsCount = 0
- override val viewportSize = IntSize.Zero
- override val orientation = Orientation.Vertical
- override val reverseLayout = false
- override val beforeContentPadding = 0
- override val afterContentPadding = 0
- override val mainAxisItemSpacing = 0
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
deleted file mode 100644
index 7681d2296..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
-
-@Suppress("ComposableModifierFactory")
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberLazyListSemanticState(
- state: TvLazyListState,
- isVertical: Boolean
-): LazyLayoutSemanticState {
- return remember(state, isVertical) {
- LazyLayoutSemanticState(state = state, isVertical = isVertical)
- }
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListIntervalContent.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListIntervalContent.kt
deleted file mode 100644
index 1ada0a1..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListIntervalContent.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
-import androidx.compose.foundation.lazy.layout.MutableIntervalList
-import androidx.compose.runtime.Composable
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class TvLazyListIntervalContent(
- content: TvLazyListScope.() -> Unit,
-) : LazyLayoutIntervalContent<TvLazyListInterval>(), TvLazyListScope {
- override val intervals: MutableIntervalList<TvLazyListInterval> = MutableIntervalList()
-
- private var _headerIndexes: MutableList<Int>? = null
- val headerIndexes: List<Int>
- @SuppressWarnings("PrimitiveInCollection") // List<Int>
- get() = _headerIndexes ?: emptyList()
-
- init {
- apply(content)
- }
-
- override fun items(
- count: Int,
- key: ((index: Int) -> Any)?,
- contentType: (index: Int) -> Any?,
- itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
- ) {
- intervals.addInterval(
- count,
- TvLazyListInterval(key = key, type = contentType, item = itemContent)
- )
- }
-
- override fun item(
- key: Any?,
- contentType: Any?,
- content: @Composable TvLazyListItemScope.() -> Unit
- ) {
- intervals.addInterval(
- 1,
- TvLazyListInterval(
- key = if (key != null) { _: Int -> key } else null,
- type = { contentType },
- item = { content() }
- )
- )
- }
-
- @SuppressWarnings("PrimitiveInCollection") // mutableListOf<Int>
- @ExperimentalTvFoundationApi
- override fun stickyHeader(
- key: Any?,
- contentType: Any?,
- content: @Composable TvLazyListItemScope.() -> Unit
- ) {
- val headersIndexes = _headerIndexes ?: mutableListOf<Int>().also { _headerIndexes = it }
- headersIndexes.add(intervals.size)
-
- item(key, contentType, content)
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class TvLazyListInterval(
- override val key: ((index: Int) -> Any)?,
- override val type: ((index: Int) -> Any?),
- val item: @Composable TvLazyListItemScope.(index: Int) -> Unit
-) : LazyLayoutIntervalContent.Interval
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt
deleted file mode 100644
index ead7cb9..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt
+++ /dev/null
@@ -1,47 +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.tv.foundation.lazy.list
-
-/**
- * Contains useful information about an individual item in lazy lists like [TvLazyColumn] or
- * [TvLazyRow].
- *
- * @see TvLazyListLayoutInfo
- */
-@Deprecated("Use `LazyListItemInfo` instead.")
-sealed interface TvLazyListItemInfo {
- /** The index of the item in the list. */
- val index: Int
-
- /** The key of the item which was passed to the item() or items() function. */
- val key: Any
-
- /**
- * The main axis offset of the item in pixels. It is relative to the start of the lazy list
- * container.
- */
- val offset: Int
-
- /**
- * The main axis size of the item in pixels. Note that if you emit multiple layouts in the
- * composable slot for the item then this size will be calculated as the sum of their sizes.
- */
- val size: Int
-
- /** The content type of the item which was passed to the item() or items() function. */
- val contentType: Any?
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
deleted file mode 100644
index ac22966..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
+++ /dev/null
@@ -1,99 +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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.annotation.FloatRange
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.VisibilityThreshold
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-
-@Stable
-@TvLazyListScopeMarker
-@Deprecated("Use `LazyListItemScope` instead.")
-sealed interface TvLazyListItemScope {
- /**
- * Have the content fill the [Constraints.maxWidth] and [Constraints.maxHeight] of the parent
- * measurement constraints by setting the [minimum width][Constraints.minWidth] to be equal to
- * the [maximum width][Constraints.maxWidth] multiplied by [fraction] and the
- * [minimum height][Constraints.minHeight] to be equal to the
- * [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default, the
- * [fraction] is 1, so the modifier will make the content fill the whole available space.
- * [fraction] must be between `0` and `1`.
- *
- * Regular [Modifier.fillMaxSize] can't work inside the scrolling layouts as the items are
- * measured with [Constraints.Infinity] as the constraints for the main axis.
- */
- @Deprecated("Use `LazyListItemScope.fillParentMaxSize` instead.")
- fun Modifier.fillParentMaxSize(@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f): Modifier
-
- /**
- * Have the content fill the [Constraints.maxWidth] of the parent measurement constraints by
- * setting the [minimum width][Constraints.minWidth] to be equal to the
- * [maximum width][Constraints.maxWidth] multiplied by [fraction]. Note that, by default, the
- * [fraction] is 1, so the modifier will make the content fill the whole parent width.
- * [fraction] must be between `0` and `1`.
- *
- * Regular [Modifier.fillMaxWidth] can't work inside the scrolling horizontally layouts as the
- * items are measured with [Constraints.Infinity] as the constraints for the main axis.
- */
- @Deprecated("Use `LazyListItemScope.fillParentMaxWidth` instead.")
- fun Modifier.fillParentMaxWidth(
- @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
- ): Modifier
-
- /**
- * Have the content fill the [Constraints.maxHeight] of the incoming measurement constraints by
- * setting the [minimum height][Constraints.minHeight] to be equal to the
- * [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default, the
- * [fraction] is 1, so the modifier will make the content fill the whole parent height.
- * [fraction] must be between `0` and `1`.
- *
- * Regular [Modifier.fillMaxHeight] can't work inside the scrolling vertically layouts as the
- * items are measured with [Constraints.Infinity] as the constraints for the main axis.
- */
- @Deprecated("Use `LazyListItemScope.fillParentMaxHeight` instead.")
- fun Modifier.fillParentMaxHeight(
- @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f
- ): Modifier
-
- /**
- * This modifier animates the item placement within the Lazy list.
- *
- * When you provide a key via [TvLazyListScope.item]/[TvLazyListScope.items] this modifier will
- * enable item reordering animations. Aside from item reordering all other position changes
- * caused by events like arrangement or alignment changes will also be animated.
- *
- * @param animationSpec a finite animation that will be used to animate the item placement.
- */
- @Deprecated("Use `LazyListItemScope.animateItemPlacement` instead.")
- @ExperimentalFoundationApi
- fun Modifier.animateItemPlacement(
- animationSpec: FiniteAnimationSpec<IntOffset> =
- spring(
- stiffness = Spring.StiffnessMediumLow,
- visibilityThreshold = IntOffset.VisibilityThreshold
- )
- ): Modifier
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt
deleted file mode 100644
index 58dce73..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.animation.core.FiniteAnimationSpec
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.ParentDataModifierNode
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.tv.foundation.lazy.grid.LazyLayoutAnimateItemModifierNode
-import kotlin.math.roundToInt
-
-internal class TvLazyListItemScopeImpl : TvLazyListItemScope {
- private var maxWidthState = mutableIntStateOf(Int.MAX_VALUE)
- private var maxHeightState = mutableIntStateOf(Int.MAX_VALUE)
-
- fun setMaxSize(width: Int, height: Int) {
- maxWidthState.intValue = width
- maxHeightState.intValue = height
- }
-
- override fun Modifier.fillParentMaxSize(fraction: Float) =
- then(
- ParentSizeElement(
- widthState = maxWidthState,
- heightState = maxHeightState,
- fraction = fraction,
- inspectorName = "fillParentMaxSize"
- )
- )
-
- override fun Modifier.fillParentMaxWidth(fraction: Float) =
- then(
- ParentSizeElement(
- widthState = maxWidthState,
- fraction = fraction,
- inspectorName = "fillParentMaxWidth"
- )
- )
-
- override fun Modifier.fillParentMaxHeight(fraction: Float) =
- then(
- ParentSizeElement(
- heightState = maxHeightState,
- fraction = fraction,
- inspectorName = "fillParentMaxHeight"
- )
- )
-
- @ExperimentalFoundationApi
- override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
- this then AnimateItemPlacementElement(animationSpec)
-}
-
-private class ParentSizeElement(
- val fraction: Float,
- val widthState: State<Int>? = null,
- val heightState: State<Int>? = null,
- val inspectorName: String
-) : ModifierNodeElement<ParentSizeNode>() {
- override fun create(): ParentSizeNode {
- return ParentSizeNode(
- fraction = fraction,
- widthState = widthState,
- heightState = heightState
- )
- }
-
- override fun update(node: ParentSizeNode) {
- node.fraction = fraction
- node.widthState = widthState
- node.heightState = heightState
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ParentSizeElement) return false
- return fraction == other.fraction &&
- widthState == other.widthState &&
- heightState == other.heightState
- }
-
- override fun hashCode(): Int {
- var result = widthState?.hashCode() ?: 0
- result = 31 * result + (heightState?.hashCode() ?: 0)
- result = 31 * result + fraction.hashCode()
- return result
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = inspectorName
- value = fraction
- }
-}
-
-private class ParentSizeNode(
- var fraction: Float,
- var widthState: State<Int>? = null,
- var heightState: State<Int>? = null,
-) : LayoutModifierNode, Modifier.Node() {
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- val width =
- widthState?.let {
- if (it.value != Constraints.Infinity) {
- (it.value * fraction).roundToInt()
- } else {
- Constraints.Infinity
- }
- } ?: Constraints.Infinity
-
- val height =
- heightState?.let {
- if (it.value != Constraints.Infinity) {
- (it.value * fraction).roundToInt()
- } else {
- Constraints.Infinity
- }
- } ?: Constraints.Infinity
- val childConstraints =
- Constraints(
- minWidth = if (width != Constraints.Infinity) width else constraints.minWidth,
- minHeight = if (height != Constraints.Infinity) height else constraints.minHeight,
- maxWidth = if (width != Constraints.Infinity) width else constraints.maxWidth,
- maxHeight = if (height != Constraints.Infinity) height else constraints.maxHeight,
- )
- val placeable = measurable.measure(childConstraints)
- return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
-}
-
-private class AnimateItemPlacementElement(val animationSpec: FiniteAnimationSpec<IntOffset>) :
- ModifierNodeElement<AnimateItemPlacementNode>() {
-
- override fun create(): AnimateItemPlacementNode = AnimateItemPlacementNode(animationSpec)
-
- override fun update(node: AnimateItemPlacementNode) {
- node.delegatingNode.placementAnimationSpec = animationSpec
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is AnimateItemPlacementElement) return false
- return animationSpec != other.animationSpec
- }
-
- override fun hashCode(): Int {
- return animationSpec.hashCode()
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "animateItemPlacement"
- value = animationSpec
- }
-}
-
-private class AnimateItemPlacementNode(animationSpec: FiniteAnimationSpec<IntOffset>) :
- DelegatingNode(), ParentDataModifierNode {
-
- val delegatingNode = delegate(LazyLayoutAnimateItemModifierNode(animationSpec))
-
- override fun Density.modifyParentData(parentData: Any?): Any = delegatingNode
-}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
deleted file mode 100644
index b11870c..0000000
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.tv.foundation.lazy.list
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
-
-/**
- * Contains useful information about the currently displayed layout state of lazy lists like
- * [TvLazyColumn] or [TvLazyRow]. For example you can get the list of currently displayed item.
- *
- * Use [TvLazyListState.layoutInfo] to retrieve this
- */
-@Deprecated("Use `LazyListLayoutInfo` instead.")
-sealed interface TvLazyListLayoutInfo {
- /** The list of [TvLazyListItemInfo] representing all the currently visible items. */
- val visibleItemsInfo: List<TvLazyListItemInfo>
-
- /**
- * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
- * which would be visible. Usually it is 0, but it can be negative if non-zero
- * [beforeContentPadding] was applied as the content displayed in the content padding area is
- * still visible.
- *
- * You can use it to understand what items from [visibleItemsInfo] are fully visible.
- */
- val viewportStartOffset: Int
-
- /**
- * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
- * which would be visible. It is the size of the lazy list layout minus [beforeContentPadding].
- *
- * You can use it to understand what items from [visibleItemsInfo] are fully visible.
- */
- val viewportEndOffset: Int
-
- /** The total count of items passed to [TvLazyColumn] or [TvLazyRow]. */
- val totalItemsCount: Int
-
- /**
- * The size of the viewport in pixels. It is the lazy list layout size including all the content
- * paddings.
- */
- // DO NOT ADD DEFAULT get() HERE
- val viewportSize: IntSize
-
- /** The orientation of the lazy list. */
- // DO NOT ADD DEFAULT get() HERE
- val orientation: Orientation
-
- /** True if the direction of scrolling and layout is reversed. */
- // DO NOT ADD DEFAULT get() HERE
- val reverseLayout: Boolean
-
- /**
- * The content padding in pixels applied before the first item in the direction of scrolling.
- * For example it is a top content padding for LazyColumn with reverseLayout set to false.
- */
- // DO NOT ADD DEFAULT get() HERE
- val beforeContentPadding: Int
-
- /**
- * The content padding in pixels applied after the last item in the direction of scrolling. For
- * example it is a bottom content padding for LazyColumn with reverseLayout set to false.
- */
- // DO NOT ADD DEFAULT get() HERE
- val afterContentPadding: Int
-
- /** The spacing between items in the direction of scrolling. */
- // DO NOT ADD DEFAULT get() HERE
- val mainAxisItemSpacing: Int
-}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt
index ac8a7822..bab1268 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ListItem.kt
@@ -30,7 +30,6 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
@@ -302,13 +301,19 @@
verticalAlignment = Alignment.CenterVertically
) {
leadingContent?.let {
- Box(
- modifier =
- Modifier.defaultMinSize(minWidth = minIconSize, minHeight = minIconSize)
- .graphicsLayer { alpha = ListItemDefaults.LeadingContentOpacity },
- contentAlignment = Alignment.Center,
- content = it
- )
+ CompositionLocalProvider(
+ LocalContentColor provides ListItemDefaults.LeadingContentColor
+ ) {
+ Box(
+ modifier =
+ Modifier.defaultMinSize(
+ minWidth = minIconSize,
+ minHeight = minIconSize
+ ),
+ contentAlignment = Alignment.Center,
+ content = it
+ )
+ }
Spacer(modifier = Modifier.padding(end = ListItemDefaults.LeadingContentEndPadding))
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt
index 74d3731..1a51101 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ListItemDefaults.kt
@@ -58,7 +58,9 @@
/** The default content padding [PaddingValues] used by [DenseListItem] */
internal val ContentPaddingDense = PaddingValues(horizontal = 12.dp, vertical = 10.dp)
- internal const val LeadingContentOpacity = 0.8f
+ internal val LeadingContentColor
+ @ReadOnlyComposable @Composable get() = LocalContentColor.current.copy(alpha = 0.8f)
+
internal const val OverlineContentOpacity = 0.6f
internal const val SupportingContentOpacity = 0.8f
diff --git a/versionedparcelable/versionedparcelable-compiler/src/main/java/androidx/versionedparcelable/compiler/VersionedParcelProcessor.java b/versionedparcelable/versionedparcelable-compiler/src/main/java/androidx/versionedparcelable/compiler/VersionedParcelProcessor.java
index cfe94e7..fa4c0e0 100644
--- a/versionedparcelable/versionedparcelable-compiler/src/main/java/androidx/versionedparcelable/compiler/VersionedParcelProcessor.java
+++ b/versionedparcelable/versionedparcelable-compiler/src/main/java/androidx/versionedparcelable/compiler/VersionedParcelProcessor.java
@@ -182,7 +182,7 @@
String jetifyAs = getValue(annotation, "jetifyAs", "");
String factoryClass = getValue(annotation, "factory", "");
parseDeprecated(takenIds, deprecatedIds);
- checkClass(versionedParcelable.asType().toString(), versionedParcelable, takenIds);
+ checkClass(typeString(versionedParcelable.asType()), versionedParcelable, takenIds);
ArrayList<Element> f = new ArrayList<>();
TypeElement te = (TypeElement) mEnv.getTypeUtils().asElement(
@@ -285,7 +285,7 @@
writeBuilder.beginControlFlow("if (!$T.equals($L, obj.$L))",
Arrays.class, strip(defaultValue), e.getSimpleName());
} else {
- String v = "java.lang.String".equals(e.asType().toString()) ? defaultValue
+ String v = "java.lang.String".equals(typeString(e.asType())) ? defaultValue
: strip(defaultValue);
writeBuilder.beginControlFlow("if (!$L.equals(obj.$L))",
v, e.getSimpleName());
@@ -354,6 +354,11 @@
return pkg;
}
+ /** Returns a simple string version of the type, with no annotations. */
+ private String typeString(TypeMirror type) {
+ return TypeName.get(type).toString();
+ }
+
private String getMethod(VariableElement e) {
TypeMirror type = e.asType();
String m = getMethod(type);
@@ -368,7 +373,7 @@
.asElement(te.getSuperclass()) : null;
}
// Manual handling for generic arrays to go last.
- if (type.toString().contains("[]")) {
+ if (typeString(type).contains("[]")) {
return "Array";
}
error("Can't find type for " + e + " (type: " + type + ")");
@@ -376,11 +381,11 @@
}
private boolean isArray(VariableElement e) {
- return e.asType().toString().endsWith("[]");
+ return typeString(e.asType()).endsWith("[]");
}
private boolean isNative(VariableElement e) {
- String type = e.asType().toString();
+ String type = typeString(e.asType());
return "int".equals(type)
|| "byte".equals(type)
|| "char".equals(type)
@@ -392,7 +397,7 @@
private String getMethod(TypeMirror typeMirror) {
// Get an annotation-free version of the type string through TypeName
- String typeString = TypeName.get(typeMirror).toString();
+ String typeString = typeString(typeMirror);
for (Pattern p: mMethodLookup.keySet()) {
if (p.matcher(typeString).find()) {
return mMethodLookup.get(p);
@@ -423,7 +428,7 @@
List<? extends AnnotationMirror> annotations = element.getAnnotationMirrors();
for (i = 0; i < annotations.size(); i++) {
AnnotationMirror annotation = annotations.get(i);
- if (annotation.getAnnotationType().toString().equals(PARCEL_FIELD)) {
+ if (typeString(annotation.getAnnotationType()).equals(PARCEL_FIELD)) {
String valStr = getValue(annotation, "value", null);
if (valStr == null) {
return;
@@ -435,7 +440,7 @@
takenIds.add(valStr);
break;
}
- if (annotation.getAnnotationType().toString().equals(NON_PARCEL_FIELD)) {
+ if (typeString(annotation.getAnnotationType()).equals(NON_PARCEL_FIELD)) {
break;
}
}
@@ -466,7 +471,7 @@
List<? extends AnnotationMirror> annotations = e.getAnnotationMirrors();
for (int i = 0; i < annotations.size(); i++) {
AnnotationMirror annotation = annotations.get(i);
- if (annotation.getAnnotationType().toString().equals(PARCEL_FIELD)) {
+ if (typeString(annotation.getAnnotationType()).equals(PARCEL_FIELD)) {
return annotation;
}
}
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 8d64456..ea38e1a 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -366,7 +366,7 @@
public final class LazyColumnDslKt {
method public static inline <T> void items(androidx.wear.compose.foundation.lazy.LazyColumnScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.LazyColumnItemScope,? super T,kotlin.Unit> itemContent);
- method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.LazyColumnScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.LazyColumnItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
@androidx.wear.compose.foundation.lazy.LazyColumnScopeMarker public sealed interface LazyColumnItemScope {
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 8d64456..ea38e1a 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -366,7 +366,7 @@
public final class LazyColumnDslKt {
method public static inline <T> void items(androidx.wear.compose.foundation.lazy.LazyColumnScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function2<? super androidx.wear.compose.foundation.lazy.LazyColumnItemScope,? super T,kotlin.Unit> itemContent);
- method public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.wear.compose.foundation.lazy.LazyColumnScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,? extends java.lang.Object?> contentType, kotlin.jvm.functions.Function3<? super androidx.wear.compose.foundation.lazy.LazyColumnItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
@androidx.wear.compose.foundation.lazy.LazyColumnScopeMarker public sealed interface LazyColumnItemScope {
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 5748cf9..4844a72 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -32,14 +32,14 @@
}
dependencies {
- api("androidx.compose.foundation:foundation:1.6.0")
- api("androidx.compose.ui:ui:1.7.0-beta01")
- api("androidx.compose.ui:ui-text:1.7.0-beta01")
- api("androidx.compose.runtime:runtime:1.6.0")
+ api("androidx.compose.foundation:foundation:1.7.0")
+ api("androidx.compose.ui:ui:1.7.0")
+ api("androidx.compose.ui:ui-text:1.7.0")
+ api("androidx.compose.runtime:runtime:1.7.0")
implementation(libs.kotlinStdlib)
- implementation("androidx.compose.foundation:foundation-layout:1.6.0")
- implementation("androidx.compose.ui:ui-util:1.7.0-beta01")
+ implementation("androidx.compose.foundation:foundation-layout:1.7.0")
+ implementation("androidx.compose.ui:ui-util:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
implementation("androidx.core:core:1.12.0")
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnTest.kt
index 761fc3d..03d8961 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnTest.kt
@@ -230,6 +230,27 @@
}
@Test
+ fun changeDataIndexed() {
+ val dataLists = listOf((1..3).toList(), (4..8).toList(), (3..4).toList())
+ var dataModel by mutableStateOf(emptyList<Int>())
+ val tag = "List"
+ rule.setContentWithTestViewConfiguration {
+ LazyColumn(Modifier.testTag(tag)) {
+ itemsIndexed(dataModel) { index, element -> BasicText("$index - $element") }
+ }
+ }
+
+ for (data in dataLists) {
+ rule.runOnIdle { dataModel = data }
+
+ // Confirm the children's content
+ for (index in data.indices) {
+ rule.onNodeWithText("$index - ${data[index]}").assertIsDisplayed()
+ }
+ }
+ }
+
+ @Test
fun removalWithMutableStateListOf() {
val items = mutableStateListOf("1", "2", "3")
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedTextStyle.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedTextStyle.kt
index 659932a..56c924c 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedTextStyle.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedTextStyle.kt
@@ -163,7 +163,8 @@
@Deprecated(
"This overload is provided for backwards compatibility with Compose for " +
- "Wear OS 1.0. A newer overload is available with additional font parameters."
+ "Wear OS 1.0. A newer overload is available with additional font parameters.",
+ level = DeprecationLevel.HIDDEN
)
fun copy(
background: Color = this.background,
@@ -184,7 +185,8 @@
@Deprecated(
"This overload is provided for backwards compatibility with Compose for " +
- "Wear OS 1.4. A newer overload is available with additional letter spacing parameter."
+ "Wear OS 1.4. A newer overload is available with additional letter spacing parameter.",
+ level = DeprecationLevel.HIDDEN
)
fun copy(
background: Color = this.background,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnDsl.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnDsl.kt
index 4a77593..c602876 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnDsl.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnDsl.kt
@@ -17,8 +17,6 @@
package androidx.wear.compose.foundation.lazy
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -141,11 +139,11 @@
* will be considered compatible.
* @param itemContent the content displayed by a single item
*/
-inline fun <T> LazyListScope.itemsIndexed(
+inline fun <T> LazyColumnScope.itemsIndexed(
items: List<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
- crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+ crossinline itemContent: @Composable LazyColumnItemScope.(index: Int, item: T) -> Unit
) =
items(
count = items.size,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index bfda803..3f316f2 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -29,7 +29,7 @@
/** Last known height for the anchor item or negative number if it hasn't been measured. */
val lastMeasuredItemHeight: Int,
/** Layout information for the visible items. */
- val visibleItems: List<LazyColumnVisibleItemInfo>,
+ override val visibleItems: List<LazyColumnVisibleItemInfo>,
/** see [LazyColumnLayoutInfo.totalItemsCount] */
- val totalItemsCount: Int,
-) : MeasureResult by measureResult
+ override val totalItemsCount: Int,
+) : LazyColumnLayoutInfo, MeasureResult by measureResult
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasuredItem.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasuredItem.kt
index fae8d47..796730c 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasuredItem.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasuredItem.kt
@@ -24,22 +24,22 @@
/** Represents a placeable item in the [LazyColumn] layout. */
internal data class LazyColumnMeasuredItem(
/** The index of the item in the list. */
- val index: Int,
+ override val index: Int,
/** The [Placeable] representing the content of the item. */
val placeable: Placeable,
/** The constraints of the container holding the item. */
val containerConstraints: Constraints,
/** The vertical offset of the item from the top of the list after transformations applied. */
- var offset: Int,
+ override var offset: Int,
/** Scroll progress of the item used to calculate transformations applied. */
- val scrollProgress: LazyColumnItemScrollProgress,
+ override val scrollProgress: LazyColumnItemScrollProgress,
/** The horizontal alignment to apply during placement. */
val horizontalAlignment: Alignment.Horizontal,
/** The [LayoutDirection] of the `Layout`. */
private val layoutDirection: LayoutDirection,
-) {
+) : LazyColumnVisibleItemInfo {
/** The height of the item after transformations applied. */
- val height =
+ override val height =
(placeable.parentData as? HeightProviderParentData)?.let {
it.heightProvider(placeable.height, scrollProgress)
} ?: placeable.height
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasurement.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasurement.kt
index bc40a2a..0001984 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasurement.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasurement.kt
@@ -29,7 +29,6 @@
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMap
import kotlin.math.abs
import kotlin.math.roundToInt
@@ -105,7 +104,7 @@
var bottomOffset = centerItem.offset + centerItem.height + itemSpacing
var bottomPassIndex = anchorItemIndex + 1
- while (bottomOffset < containerConstraints.maxHeight + 50 && bottomPassIndex < itemsCount) {
+ while (bottomOffset < containerConstraints.maxHeight && bottomPassIndex < itemsCount) {
val item = measuredItemProvider.downwardMeasuredItem(bottomPassIndex, bottomOffset)
bottomOffset += item.height + itemSpacing
visibleItems.add(item)
@@ -142,15 +141,7 @@
anchorItemIndex = anchorItem.index,
anchorItemScrollOffset =
anchorItem.let { it.offset + it.height - containerConstraints.maxHeight / 2 },
- visibleItems =
- visibleItems.fastMap {
- LazyColumnVisibleItemInfoImpl(
- index = it.index,
- offset = it.offset,
- height = it.height,
- scrollProgress = it.scrollProgress,
- )
- },
+ visibleItems = visibleItems,
totalItemsCount = itemsCount,
lastMeasuredItemHeight = anchorItem.height,
measureResult =
@@ -160,13 +151,6 @@
)
}
-private class LazyColumnVisibleItemInfoImpl(
- override val index: Int,
- override val offset: Int,
- override val height: Int,
- override val scrollProgress: LazyColumnItemScrollProgress
-) : LazyColumnVisibleItemInfo
-
private fun bottomItemScrollProgress(
offset: Int,
height: Int,
@@ -208,11 +192,12 @@
index: Int,
offset: Int
): LazyColumnMeasuredItem {
- val placeables =
- measure(
- index,
- containerConstraints.copy(maxHeight = Constraints.Infinity)
+ val childConstraints =
+ Constraints(
+ maxHeight = Constraints.Infinity,
+ maxWidth = containerConstraints.maxWidth
)
+ val placeables = measure(index, childConstraints)
// TODO(artemiy): Add support for multiple items.
val content = placeables.last()
val scrollProgress =
@@ -236,11 +221,12 @@
index: Int,
offset: Int
): LazyColumnMeasuredItem {
- val placeables =
- measure(
- index,
- containerConstraints.copy(maxHeight = Constraints.Infinity)
+ val childConstraints =
+ Constraints(
+ maxHeight = Constraints.Infinity,
+ maxWidth = containerConstraints.maxWidth
)
+ val placeables = measure(index, childConstraints)
// TODO(artemiy): Add support for multiple items.
val content = placeables.last()
val scrollProgress =
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnState.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnState.kt
index 5b71249..09d614c 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnState.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnState.kt
@@ -23,8 +23,11 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
import kotlin.math.abs
@@ -48,13 +51,19 @@
block: suspend ScrollScope.() -> Unit
) = scrollableState.scroll(scrollPriority, block)
- var layoutInfo: LazyColumnLayoutInfo by mutableStateOf(LazyColumnLayoutInfoImpl(emptyList(), 0))
- private set
+ private val layoutInfoState = mutableStateOf(EmptyLazyColumnMeasureResult, neverEqualPolicy())
- private data class LazyColumnLayoutInfoImpl(
- override val visibleItems: List<LazyColumnVisibleItemInfo>,
- override val totalItemsCount: Int,
- ) : LazyColumnLayoutInfo
+ /**
+ * The object of LazyColumnLayoutInfo calculated during the last layout pass. For example, you
+ * can use it to calculate what items are currently visible. Note that this property is
+ * observable and is updated after every scroll or remeasure. If you use it in the composable
+ * function it will be recomposed on every change causing potential performance issues including
+ * infinity recomposition loop. Therefore, avoid using it in the composition. If you want to run
+ * some side effects like sending an analytics event or updating a state based on this value
+ * consider using "snapshotFlow":
+ */
+ val layoutInfo: LazyColumnLayoutInfo
+ get() = layoutInfoState.value
internal var scrollToBeConsumed = 0f
private set
@@ -85,11 +94,7 @@
anchorItemIndex = measureResult.anchorItemIndex
anchorItemScrollOffset = measureResult.anchorItemScrollOffset
lastMeasuredAnchorItemHeight = measureResult.lastMeasuredItemHeight
- layoutInfo =
- LazyColumnLayoutInfoImpl(
- visibleItems = measureResult.visibleItems,
- totalItemsCount = measureResult.totalItemsCount
- )
+ layoutInfoState.value = measureResult
}
private val scrollableState = ScrollableState { -onScroll(-it) }
@@ -118,3 +123,22 @@
}
}
}
+
+private val EmptyLazyColumnMeasureResult =
+ LazyColumnMeasureResult(
+ anchorItemIndex = 0,
+ anchorItemScrollOffset = 0,
+ visibleItems = emptyList(),
+ totalItemsCount = 0,
+ lastMeasuredItemHeight = Int.MIN_VALUE,
+ measureResult =
+ object : MeasureResult {
+ override val width: Int = 0
+ override val height: Int = 0
+
+ @Suppress("PrimitiveInCollection")
+ override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
+
+ override fun placeChildren() {}
+ }
+ )
diff --git a/wear/compose/compose-material-core/build.gradle b/wear/compose/compose-material-core/build.gradle
index efc7042..df1b6c6 100644
--- a/wear/compose/compose-material-core/build.gradle
+++ b/wear/compose/compose-material-core/build.gradle
@@ -33,16 +33,16 @@
}
dependencies {
- api("androidx.compose.foundation:foundation:1.7.0-beta02")
- api("androidx.compose.ui:ui:1.6.0")
- api("androidx.compose.ui:ui-text:1.6.0")
- api("androidx.compose.runtime:runtime:1.6.0")
+ api("androidx.compose.foundation:foundation:1.7.0")
+ api("androidx.compose.ui:ui:1.7.0")
+ api("androidx.compose.ui:ui-text:1.7.0")
+ api("androidx.compose.runtime:runtime:1.7.0")
implementation(libs.kotlinStdlib)
- implementation("androidx.compose.animation:animation:1.6.0")
- implementation("androidx.compose.material:material-icons-core:1.6.0")
- implementation("androidx.compose.material:material-ripple:1.6.0")
- implementation("androidx.compose.ui:ui-util:1.6.0")
+ implementation("androidx.compose.animation:animation:1.7.0")
+ implementation("androidx.compose.material:material-icons-core:1.7.0")
+ implementation("androidx.compose.material:material-ripple:1.7.0")
+ implementation("androidx.compose.ui:ui-util:1.7.0")
implementation(project(":wear:compose:compose-foundation"))
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Resources.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Resources.kt
index 1a967f1..7b9faf1 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Resources.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Resources.kt
@@ -62,11 +62,11 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
-fun screenHeightDp() = LocalContext.current.resources.configuration.screenHeightDp
+fun screenHeightDp() = LocalConfiguration.current.screenHeightDp
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
-fun screenWidthDp() = LocalContext.current.resources.configuration.screenWidthDp
+fun screenWidthDp() = LocalConfiguration.current.screenWidthDp
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 927c577..1d2fc79 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -31,17 +31,17 @@
}
dependencies {
- api("androidx.compose.foundation:foundation:1.6.0")
- api("androidx.compose.ui:ui:1.6.0")
- api("androidx.compose.ui:ui-text:1.6.0")
- api("androidx.compose.runtime:runtime:1.6.0")
+ api("androidx.compose.foundation:foundation:1.7.0")
+ api("androidx.compose.ui:ui:1.7.0")
+ api("androidx.compose.ui:ui-text:1.7.0")
+ api("androidx.compose.runtime:runtime:1.7.0")
api(project(":wear:compose:compose-foundation"))
implementation(libs.kotlinStdlib)
- implementation("androidx.compose.animation:animation:1.6.0")
- implementation("androidx.compose.material:material-icons-core:1.6.0")
- implementation("androidx.compose.material:material-ripple:1.7.0-beta02")
- implementation("androidx.compose.ui:ui-util:1.6.0")
+ implementation("androidx.compose.animation:animation:1.7.0")
+ implementation("androidx.compose.material:material-icons-core:1.7.0")
+ implementation("androidx.compose.material:material-ripple:1.7.0")
+ implementation("androidx.compose.ui:ui-util:1.7.0")
implementation(project(":wear:compose:compose-material-core"))
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
implementation("androidx.lifecycle:lifecycle-common:2.7.0")
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index 68c71a3..b0b63b9 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -1,6 +1,28 @@
// Signature format: 4.0
package androidx.wear.compose.material3 {
+ public final class AlertDialogDefaults {
+ method @androidx.compose.runtime.Composable public void BottomButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void GroupSeparator();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
+ method public float getEdgeButtonExtraTopPadding();
+ method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> ConfirmIcon;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> DismissIcon;
+ property public final androidx.compose.foundation.layout.Arrangement.Vertical VerticalArrangement;
+ property public final float edgeButtonExtraTopPadding;
+ field public static final androidx.wear.compose.material3.AlertDialogDefaults INSTANCE;
+ }
+
+ public final class AlertDialogKt {
+ method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+ method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+ }
+
@RequiresApi(31) public final class AnimatedTextDefaults {
field public static final int CacheSize = 5; // 0x5
field public static final androidx.wear.compose.material3.AnimatedTextDefaults INSTANCE;
@@ -298,12 +320,52 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
}
+ public final class ConfirmationColors {
+ ctor public ConfirmationColors(long iconColor, long iconContainerColor, long textColor);
+ method public long getIconColor();
+ method public long getIconContainerColor();
+ method public long getTextColor();
+ property public final long iconColor;
+ property public final long iconContainerColor;
+ property public final long textColor;
+ }
+
+ public final class ConfirmationDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors confirmationColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors confirmationColors(optional long iconColor, optional long iconContainerColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> curvedText(String text, optional androidx.wear.compose.foundation.CurvedTextStyle style);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors failureColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors failureColors(optional long iconColor, optional long iconContainerColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> failureText();
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> getFailureIcon();
+ method public float getIconSize();
+ method public float getSmallIconSize();
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> getSuccessIcon();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors successColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors successColors(optional long iconColor, optional long iconContainerColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> successText();
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> FailureIcon;
+ property public final float IconSize;
+ property public final float SmallIconSize;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> SuccessIcon;
+ field public static final long ConfirmationDurationMillis = 4000L; // 0xfa0L
+ field public static final androidx.wear.compose.material3.ConfirmationDefaults INSTANCE;
+ }
+
+ public final class ConfirmationKt {
+ method @androidx.compose.runtime.Composable public static void Confirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? text, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Confirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void FailureConfirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void SuccessConfirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ }
+
public final class ContentColorKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
public final class CurvedTextDefaults {
+ method @androidx.compose.runtime.Composable public long backgroundColor();
field public static final androidx.wear.compose.material3.CurvedTextDefaults INSTANCE;
field public static final float ScrollableContentMaxSweepAngle = 70.0f;
field public static final float StaticContentMaxSweepAngle = 120.0f;
@@ -313,6 +375,49 @@
method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional float maxSweepAngle, optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
+ @androidx.compose.runtime.Immutable public final class DatePickerColors {
+ ctor public DatePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long pickerLabelColor, long nextButtonContentColor, long nextButtonContainerColor, long confirmButtonContentColor, long confirmButtonContainerColor);
+ method public long getConfirmButtonContainerColor();
+ method public long getConfirmButtonContentColor();
+ method public long getNextButtonContainerColor();
+ method public long getNextButtonContentColor();
+ method public long getPickerLabelColor();
+ method public long getSelectedPickerContentColor();
+ method public long getUnselectedPickerContentColor();
+ property public final long confirmButtonContainerColor;
+ property public final long confirmButtonContentColor;
+ property public final long nextButtonContainerColor;
+ property public final long nextButtonContentColor;
+ property public final long pickerLabelColor;
+ property public final long selectedPickerContentColor;
+ property public final long unselectedPickerContentColor;
+ }
+
+ public final class DatePickerDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.DatePickerColors datePickerColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.DatePickerColors datePickerColors(optional long selectedPickerContentColor, optional long unselectedPickerContentColor, optional long pickerLabelColor, optional long nextButtonContentColor, optional long nextButtonContainerColor, optional long confirmButtonContentColor, optional long confirmButtonContainerColor);
+ method @androidx.compose.runtime.Composable public int getDatePickerType();
+ property @androidx.compose.runtime.Composable public final int datePickerType;
+ field public static final androidx.wear.compose.material3.DatePickerDefaults INSTANCE;
+ }
+
+ public final class DatePickerKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.compose.runtime.Composable public static void DatePicker(java.time.LocalDate initialDate, kotlin.jvm.functions.Function1<? super java.time.LocalDate,kotlin.Unit> onDatePicked, optional androidx.compose.ui.Modifier modifier, optional java.time.LocalDate? minDate, optional java.time.LocalDate? maxDate, optional int datePickerType, optional androidx.wear.compose.material3.DatePickerColors colors);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DatePickerType {
+ field public static final androidx.wear.compose.material3.DatePickerType.Companion Companion;
+ }
+
+ public static final class DatePickerType.Companion {
+ method public int getDayMonthYear();
+ method public int getMonthDayYear();
+ method public int getYearMonthDay();
+ property public final int DayMonthYear;
+ property public final int MonthDayYear;
+ property public final int YearMonthDay;
+ }
+
public final class EdgeButtonKt {
method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float buttonHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
@@ -346,6 +451,7 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledVariantIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public float getDefaultButtonSize();
method public float getDefaultIconSize();
+ method public float getDisabledImageOpacity();
method public float getExtraSmallButtonSize();
method public float getLargeButtonSize();
method public float getLargeIconSize();
@@ -356,8 +462,8 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public float iconSizeFor(float size);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors iconToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor, optional long disabledContentColor);
property public final float DefaultButtonSize;
@@ -367,6 +473,7 @@
property public final float LargeIconSize;
property public final float SmallButtonSize;
property public final float SmallIconSize;
+ property public final float disabledImageOpacity;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.shape.CornerBasedShape pressedShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.shape.RoundedCornerShape shape;
field public static final androidx.wear.compose.material3.IconButtonDefaults INSTANCE;
@@ -376,7 +483,7 @@
method @androidx.compose.runtime.Composable public static void FilledIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledTonalIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.ToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
@@ -386,6 +493,26 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
+ @androidx.compose.runtime.Immutable public final class IconToggleButtonColors {
+ ctor public IconToggleButtonColors(long checkedContainerColor, long checkedContentColor, long uncheckedContainerColor, long uncheckedContentColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor);
+ method public long getCheckedContainerColor();
+ method public long getCheckedContentColor();
+ method public long getDisabledCheckedContainerColor();
+ method public long getDisabledCheckedContentColor();
+ method public long getDisabledUncheckedContainerColor();
+ method public long getDisabledUncheckedContentColor();
+ method public long getUncheckedContainerColor();
+ method public long getUncheckedContentColor();
+ property public final long checkedContainerColor;
+ property public final long checkedContentColor;
+ property public final long disabledCheckedContainerColor;
+ property public final long disabledCheckedContentColor;
+ property public final long disabledUncheckedContainerColor;
+ property public final long disabledUncheckedContentColor;
+ property public final long uncheckedContainerColor;
+ property public final long uncheckedContentColor;
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public final class InlineSliderColors {
ctor public InlineSliderColors(long containerColor, long buttonIconColor, long selectedBarColor, long unselectedBarColor, long barSeparatorColor, long disabledContainerColor, long disabledButtonIconColor, long disabledSelectedBarColor, long disabledUnselectedBarColor, long disabledBarSeparatorColor);
method public long getBarSeparatorColor();
@@ -427,6 +554,32 @@
property @SuppressCompatibility @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
}
+ public final class LevelIndicatorColors {
+ ctor public LevelIndicatorColors(long indicatorColor, long trackColor, long disabledIndicatorColor, long disabledTrackColor);
+ method public long getDisabledIndicatorColor();
+ method public long getDisabledTrackColor();
+ method public long getIndicatorColor();
+ method public long getTrackColor();
+ property public final long disabledIndicatorColor;
+ property public final long disabledTrackColor;
+ property public final long indicatorColor;
+ property public final long trackColor;
+ }
+
+ public final class LevelIndicatorDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.LevelIndicatorColors colors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.LevelIndicatorColors colors(optional long indicatorColor, optional long trackColor, optional long disabledIndicatorColor, optional long disabledTrackColor);
+ method public float getStrokeWidth();
+ property public final float StrokeWidth;
+ field public static final androidx.wear.compose.material3.LevelIndicatorDefaults INSTANCE;
+ field public static final float SweepAngle = 72.0f;
+ }
+
+ public final class LevelIndicatorKt {
+ method @androidx.compose.runtime.Composable public static void LevelIndicator(kotlin.jvm.functions.Function0<java.lang.Float> value, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional boolean enabled, optional androidx.wear.compose.material3.LevelIndicatorColors colors, optional float strokeWidth, optional float sweepAngle, optional boolean reverseDirection);
+ method @androidx.compose.runtime.Composable public static void LevelIndicator(kotlin.jvm.functions.Function0<java.lang.Integer> value, kotlin.ranges.IntProgression valueProgression, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.LevelIndicatorColors colors, optional float strokeWidth, optional float sweepAngle, optional boolean reverseDirection);
+ }
+
public final class ListHeaderDefaults {
method public androidx.compose.foundation.layout.PaddingValues getHeaderContentPadding();
method public androidx.compose.foundation.layout.PaddingValues getSubheaderContentPadding();
@@ -476,6 +629,34 @@
method public static androidx.wear.compose.material3.MotionScheme standardMotionScheme();
}
+ public final class OpenOnPhoneDialogColors {
+ ctor public OpenOnPhoneDialogColors(long iconColor, long iconContainerColor, long progressIndicatorColor, long progressTrackColor, long textColor);
+ method public long getIconColor();
+ method public long getIconContainerColor();
+ method public long getProgressIndicatorColor();
+ method public long getProgressTrackColor();
+ method public long getTextColor();
+ property public final long iconColor;
+ property public final long iconContainerColor;
+ property public final long progressIndicatorColor;
+ property public final long progressTrackColor;
+ property public final long textColor;
+ }
+
+ public final class OpenOnPhoneDialogDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.OpenOnPhoneDialogColors colors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.OpenOnPhoneDialogColors colors(optional long iconColor, optional long iconContainerColor, optional long progressIndicatorColor, optional long progressTrackColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> curvedText(optional String text, optional androidx.wear.compose.foundation.CurvedTextStyle style);
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> getIcon();
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> Icon;
+ field public static final long DurationMillis = 4000L; // 0xfa0L
+ field public static final androidx.wear.compose.material3.OpenOnPhoneDialogDefaults INSTANCE;
+ }
+
+ public final class OpenOnPhoneDialogKt {
+ method @androidx.compose.runtime.Composable public static void OpenOnPhoneDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.OpenOnPhoneDialogColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ }
+
public final class PickerDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior(androidx.wear.compose.material3.PickerState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
method public float getGradientRatio();
@@ -693,14 +874,21 @@
method @androidx.compose.runtime.Composable public static void ScreenScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
- public enum ScreenStage {
- enum_constant public static final androidx.wear.compose.material3.ScreenStage Idle;
- enum_constant public static final androidx.wear.compose.material3.ScreenStage New;
- enum_constant public static final androidx.wear.compose.material3.ScreenStage Scrolling;
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScreenStage {
+ field public static final androidx.wear.compose.material3.ScreenStage.Companion Companion;
+ }
+
+ public static final class ScreenStage.Companion {
+ method public int getIdle();
+ method public int getNew();
+ method public int getScrolling();
+ property public final int Idle;
+ property public final int New;
+ property public final int Scrolling;
}
public final class ScrollAwayKt {
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, kotlin.jvm.functions.Function0<? extends androidx.wear.compose.material3.ScreenStage> screenStage);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, kotlin.jvm.functions.Function0<androidx.wear.compose.material3.ScreenStage> screenStage);
}
public final class ScrollIndicatorDefaults {
@@ -1040,8 +1228,8 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors outlinedTextButtonColors(optional long contentColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors textToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
property public final float DefaultButtonSize;
property public final float LargeButtonSize;
property public final float SmallButtonSize;
@@ -1055,7 +1243,7 @@
public final class TextButtonKt {
method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.TextButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.ToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.TextToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
public final class TextKt {
@@ -1072,6 +1260,26 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
+ @androidx.compose.runtime.Immutable public final class TextToggleButtonColors {
+ ctor public TextToggleButtonColors(long checkedContainerColor, long checkedContentColor, long uncheckedContainerColor, long uncheckedContentColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor);
+ method public long getCheckedContainerColor();
+ method public long getCheckedContentColor();
+ method public long getDisabledCheckedContainerColor();
+ method public long getDisabledCheckedContentColor();
+ method public long getDisabledUncheckedContainerColor();
+ method public long getDisabledUncheckedContentColor();
+ method public long getUncheckedContainerColor();
+ method public long getUncheckedContentColor();
+ property public final long checkedContainerColor;
+ property public final long checkedContentColor;
+ property public final long disabledCheckedContainerColor;
+ property public final long disabledCheckedContentColor;
+ property public final long disabledUncheckedContainerColor;
+ property public final long disabledUncheckedContentColor;
+ property public final long uncheckedContainerColor;
+ property public final long uncheckedContentColor;
+ }
+
@androidx.compose.runtime.Immutable public final class TimePickerColors {
ctor public TimePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long separatorColor, long pickerLabelColor, long confirmButtonContentColor, long confirmButtonContainerColor);
method public long getConfirmButtonContainerColor();
@@ -1142,26 +1350,6 @@
method public abstract void time();
}
- @androidx.compose.runtime.Immutable public final class ToggleButtonColors {
- ctor public ToggleButtonColors(long checkedContainerColor, long checkedContentColor, long uncheckedContainerColor, long uncheckedContentColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor);
- method public long getCheckedContainerColor();
- method public long getCheckedContentColor();
- method public long getDisabledCheckedContainerColor();
- method public long getDisabledCheckedContentColor();
- method public long getDisabledUncheckedContainerColor();
- method public long getDisabledUncheckedContentColor();
- method public long getUncheckedContainerColor();
- method public long getUncheckedContentColor();
- property public final long checkedContainerColor;
- property public final long checkedContentColor;
- property public final long disabledCheckedContainerColor;
- property public final long disabledCheckedContentColor;
- property public final long disabledUncheckedContainerColor;
- property public final long disabledUncheckedContentColor;
- property public final long uncheckedContainerColor;
- property public final long uncheckedContentColor;
- }
-
public fun interface TouchExplorationStateProvider {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<java.lang.Boolean> touchExplorationState();
}
@@ -1217,32 +1405,6 @@
}
-package androidx.wear.compose.material3.dialog {
-
- public final class AlertDialogDefaults {
- method @androidx.compose.runtime.Composable public void BottomButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void GroupSeparator();
- method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
- method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
- method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
- method public float getEdgeButtonExtraTopPadding();
- method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
- property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> ConfirmIcon;
- property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> DismissIcon;
- property public final androidx.compose.foundation.layout.Arrangement.Vertical VerticalArrangement;
- property public final float edgeButtonExtraTopPadding;
- field public static final androidx.wear.compose.material3.dialog.AlertDialogDefaults INSTANCE;
- }
-
- public final class AlertDialogKt {
- method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
- method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
- }
-
-}
-
package androidx.wear.compose.material3.lazy {
public final class LazyColumnScrollTransformModifiersKt {
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index 68c71a3..b0b63b9 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -1,6 +1,28 @@
// Signature format: 4.0
package androidx.wear.compose.material3 {
+ public final class AlertDialogDefaults {
+ method @androidx.compose.runtime.Composable public void BottomButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void GroupSeparator();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
+ method public float getEdgeButtonExtraTopPadding();
+ method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> ConfirmIcon;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> DismissIcon;
+ property public final androidx.compose.foundation.layout.Arrangement.Vertical VerticalArrangement;
+ property public final float edgeButtonExtraTopPadding;
+ field public static final androidx.wear.compose.material3.AlertDialogDefaults INSTANCE;
+ }
+
+ public final class AlertDialogKt {
+ method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+ method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+ }
+
@RequiresApi(31) public final class AnimatedTextDefaults {
field public static final int CacheSize = 5; // 0x5
field public static final androidx.wear.compose.material3.AnimatedTextDefaults INSTANCE;
@@ -298,12 +320,52 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
}
+ public final class ConfirmationColors {
+ ctor public ConfirmationColors(long iconColor, long iconContainerColor, long textColor);
+ method public long getIconColor();
+ method public long getIconContainerColor();
+ method public long getTextColor();
+ property public final long iconColor;
+ property public final long iconContainerColor;
+ property public final long textColor;
+ }
+
+ public final class ConfirmationDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors confirmationColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors confirmationColors(optional long iconColor, optional long iconContainerColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> curvedText(String text, optional androidx.wear.compose.foundation.CurvedTextStyle style);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors failureColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors failureColors(optional long iconColor, optional long iconContainerColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> failureText();
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> getFailureIcon();
+ method public float getIconSize();
+ method public float getSmallIconSize();
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> getSuccessIcon();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors successColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ConfirmationColors successColors(optional long iconColor, optional long iconContainerColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> successText();
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> FailureIcon;
+ property public final float IconSize;
+ property public final float SmallIconSize;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> SuccessIcon;
+ field public static final long ConfirmationDurationMillis = 4000L; // 0xfa0L
+ field public static final androidx.wear.compose.material3.ConfirmationDefaults INSTANCE;
+ }
+
+ public final class ConfirmationKt {
+ method @androidx.compose.runtime.Composable public static void Confirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? text, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Confirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void FailureConfirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void SuccessConfirmation(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.ConfirmationColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ }
+
public final class ContentColorKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
public final class CurvedTextDefaults {
+ method @androidx.compose.runtime.Composable public long backgroundColor();
field public static final androidx.wear.compose.material3.CurvedTextDefaults INSTANCE;
field public static final float ScrollableContentMaxSweepAngle = 70.0f;
field public static final float StaticContentMaxSweepAngle = 120.0f;
@@ -313,6 +375,49 @@
method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional float maxSweepAngle, optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
}
+ @androidx.compose.runtime.Immutable public final class DatePickerColors {
+ ctor public DatePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long pickerLabelColor, long nextButtonContentColor, long nextButtonContainerColor, long confirmButtonContentColor, long confirmButtonContainerColor);
+ method public long getConfirmButtonContainerColor();
+ method public long getConfirmButtonContentColor();
+ method public long getNextButtonContainerColor();
+ method public long getNextButtonContentColor();
+ method public long getPickerLabelColor();
+ method public long getSelectedPickerContentColor();
+ method public long getUnselectedPickerContentColor();
+ property public final long confirmButtonContainerColor;
+ property public final long confirmButtonContentColor;
+ property public final long nextButtonContainerColor;
+ property public final long nextButtonContentColor;
+ property public final long pickerLabelColor;
+ property public final long selectedPickerContentColor;
+ property public final long unselectedPickerContentColor;
+ }
+
+ public final class DatePickerDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.DatePickerColors datePickerColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.DatePickerColors datePickerColors(optional long selectedPickerContentColor, optional long unselectedPickerContentColor, optional long pickerLabelColor, optional long nextButtonContentColor, optional long nextButtonContainerColor, optional long confirmButtonContentColor, optional long confirmButtonContainerColor);
+ method @androidx.compose.runtime.Composable public int getDatePickerType();
+ property @androidx.compose.runtime.Composable public final int datePickerType;
+ field public static final androidx.wear.compose.material3.DatePickerDefaults INSTANCE;
+ }
+
+ public final class DatePickerKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) @androidx.compose.runtime.Composable public static void DatePicker(java.time.LocalDate initialDate, kotlin.jvm.functions.Function1<? super java.time.LocalDate,kotlin.Unit> onDatePicked, optional androidx.compose.ui.Modifier modifier, optional java.time.LocalDate? minDate, optional java.time.LocalDate? maxDate, optional int datePickerType, optional androidx.wear.compose.material3.DatePickerColors colors);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DatePickerType {
+ field public static final androidx.wear.compose.material3.DatePickerType.Companion Companion;
+ }
+
+ public static final class DatePickerType.Companion {
+ method public int getDayMonthYear();
+ method public int getMonthDayYear();
+ method public int getYearMonthDay();
+ property public final int DayMonthYear;
+ property public final int MonthDayYear;
+ property public final int YearMonthDay;
+ }
+
public final class EdgeButtonKt {
method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float buttonHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
@@ -346,6 +451,7 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors filledVariantIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public float getDefaultButtonSize();
method public float getDefaultIconSize();
+ method public float getDisabledImageOpacity();
method public float getExtraSmallButtonSize();
method public float getLargeButtonSize();
method public float getLargeIconSize();
@@ -356,8 +462,8 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public float iconSizeFor(float size);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors iconToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor, optional long disabledContentColor);
property public final float DefaultButtonSize;
@@ -367,6 +473,7 @@
property public final float LargeIconSize;
property public final float SmallButtonSize;
property public final float SmallIconSize;
+ property public final float disabledImageOpacity;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.shape.CornerBasedShape pressedShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.shape.RoundedCornerShape shape;
field public static final androidx.wear.compose.material3.IconButtonDefaults INSTANCE;
@@ -376,7 +483,7 @@
method @androidx.compose.runtime.Composable public static void FilledIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void FilledTonalIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.ToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.IconToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.IconButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
@@ -386,6 +493,26 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
+ @androidx.compose.runtime.Immutable public final class IconToggleButtonColors {
+ ctor public IconToggleButtonColors(long checkedContainerColor, long checkedContentColor, long uncheckedContainerColor, long uncheckedContentColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor);
+ method public long getCheckedContainerColor();
+ method public long getCheckedContentColor();
+ method public long getDisabledCheckedContainerColor();
+ method public long getDisabledCheckedContentColor();
+ method public long getDisabledUncheckedContainerColor();
+ method public long getDisabledUncheckedContentColor();
+ method public long getUncheckedContainerColor();
+ method public long getUncheckedContentColor();
+ property public final long checkedContainerColor;
+ property public final long checkedContentColor;
+ property public final long disabledCheckedContainerColor;
+ property public final long disabledCheckedContentColor;
+ property public final long disabledUncheckedContainerColor;
+ property public final long disabledUncheckedContentColor;
+ property public final long uncheckedContainerColor;
+ property public final long uncheckedContentColor;
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public final class InlineSliderColors {
ctor public InlineSliderColors(long containerColor, long buttonIconColor, long selectedBarColor, long unselectedBarColor, long barSeparatorColor, long disabledContainerColor, long disabledButtonIconColor, long disabledSelectedBarColor, long disabledUnselectedBarColor, long disabledBarSeparatorColor);
method public long getBarSeparatorColor();
@@ -427,6 +554,32 @@
property @SuppressCompatibility @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
}
+ public final class LevelIndicatorColors {
+ ctor public LevelIndicatorColors(long indicatorColor, long trackColor, long disabledIndicatorColor, long disabledTrackColor);
+ method public long getDisabledIndicatorColor();
+ method public long getDisabledTrackColor();
+ method public long getIndicatorColor();
+ method public long getTrackColor();
+ property public final long disabledIndicatorColor;
+ property public final long disabledTrackColor;
+ property public final long indicatorColor;
+ property public final long trackColor;
+ }
+
+ public final class LevelIndicatorDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.LevelIndicatorColors colors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.LevelIndicatorColors colors(optional long indicatorColor, optional long trackColor, optional long disabledIndicatorColor, optional long disabledTrackColor);
+ method public float getStrokeWidth();
+ property public final float StrokeWidth;
+ field public static final androidx.wear.compose.material3.LevelIndicatorDefaults INSTANCE;
+ field public static final float SweepAngle = 72.0f;
+ }
+
+ public final class LevelIndicatorKt {
+ method @androidx.compose.runtime.Composable public static void LevelIndicator(kotlin.jvm.functions.Function0<java.lang.Float> value, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional boolean enabled, optional androidx.wear.compose.material3.LevelIndicatorColors colors, optional float strokeWidth, optional float sweepAngle, optional boolean reverseDirection);
+ method @androidx.compose.runtime.Composable public static void LevelIndicator(kotlin.jvm.functions.Function0<java.lang.Integer> value, kotlin.ranges.IntProgression valueProgression, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.LevelIndicatorColors colors, optional float strokeWidth, optional float sweepAngle, optional boolean reverseDirection);
+ }
+
public final class ListHeaderDefaults {
method public androidx.compose.foundation.layout.PaddingValues getHeaderContentPadding();
method public androidx.compose.foundation.layout.PaddingValues getSubheaderContentPadding();
@@ -476,6 +629,34 @@
method public static androidx.wear.compose.material3.MotionScheme standardMotionScheme();
}
+ public final class OpenOnPhoneDialogColors {
+ ctor public OpenOnPhoneDialogColors(long iconColor, long iconContainerColor, long progressIndicatorColor, long progressTrackColor, long textColor);
+ method public long getIconColor();
+ method public long getIconContainerColor();
+ method public long getProgressIndicatorColor();
+ method public long getProgressTrackColor();
+ method public long getTextColor();
+ property public final long iconColor;
+ property public final long iconContainerColor;
+ property public final long progressIndicatorColor;
+ property public final long progressTrackColor;
+ property public final long textColor;
+ }
+
+ public final class OpenOnPhoneDialogDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.OpenOnPhoneDialogColors colors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.OpenOnPhoneDialogColors colors(optional long iconColor, optional long iconContainerColor, optional long progressIndicatorColor, optional long progressTrackColor, optional long textColor);
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<androidx.wear.compose.foundation.CurvedScope,kotlin.Unit> curvedText(optional String text, optional androidx.wear.compose.foundation.CurvedTextStyle style);
+ method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> getIcon();
+ property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.BoxScope,kotlin.Unit> Icon;
+ field public static final long DurationMillis = 4000L; // 0xfa0L
+ field public static final androidx.wear.compose.material3.OpenOnPhoneDialogDefaults INSTANCE;
+ }
+
+ public final class OpenOnPhoneDialogKt {
+ method @androidx.compose.runtime.Composable public static void OpenOnPhoneDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedScope,kotlin.Unit>? curvedText, optional androidx.wear.compose.material3.OpenOnPhoneDialogColors colors, optional androidx.compose.ui.window.DialogProperties properties, optional long durationMillis, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ }
+
public final class PickerDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior(androidx.wear.compose.material3.PickerState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decay);
method public float getGradientRatio();
@@ -693,14 +874,21 @@
method @androidx.compose.runtime.Composable public static void ScreenScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
- public enum ScreenStage {
- enum_constant public static final androidx.wear.compose.material3.ScreenStage Idle;
- enum_constant public static final androidx.wear.compose.material3.ScreenStage New;
- enum_constant public static final androidx.wear.compose.material3.ScreenStage Scrolling;
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class ScreenStage {
+ field public static final androidx.wear.compose.material3.ScreenStage.Companion Companion;
+ }
+
+ public static final class ScreenStage.Companion {
+ method public int getIdle();
+ method public int getNew();
+ method public int getScrolling();
+ property public final int Idle;
+ property public final int New;
+ property public final int Scrolling;
}
public final class ScrollAwayKt {
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, kotlin.jvm.functions.Function0<? extends androidx.wear.compose.material3.ScreenStage> screenStage);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, kotlin.jvm.functions.Function0<androidx.wear.compose.material3.ScreenStage> screenStage);
}
public final class ScrollIndicatorDefaults {
@@ -1040,8 +1228,8 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors outlinedTextButtonColors(optional long contentColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors textToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
property public final float DefaultButtonSize;
property public final float LargeButtonSize;
property public final float SmallButtonSize;
@@ -1055,7 +1243,7 @@
public final class TextButtonKt {
method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional String? onLongClickLabel, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.TextButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.ToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.TextToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
public final class TextKt {
@@ -1072,6 +1260,26 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
+ @androidx.compose.runtime.Immutable public final class TextToggleButtonColors {
+ ctor public TextToggleButtonColors(long checkedContainerColor, long checkedContentColor, long uncheckedContainerColor, long uncheckedContentColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor);
+ method public long getCheckedContainerColor();
+ method public long getCheckedContentColor();
+ method public long getDisabledCheckedContainerColor();
+ method public long getDisabledCheckedContentColor();
+ method public long getDisabledUncheckedContainerColor();
+ method public long getDisabledUncheckedContentColor();
+ method public long getUncheckedContainerColor();
+ method public long getUncheckedContentColor();
+ property public final long checkedContainerColor;
+ property public final long checkedContentColor;
+ property public final long disabledCheckedContainerColor;
+ property public final long disabledCheckedContentColor;
+ property public final long disabledUncheckedContainerColor;
+ property public final long disabledUncheckedContentColor;
+ property public final long uncheckedContainerColor;
+ property public final long uncheckedContentColor;
+ }
+
@androidx.compose.runtime.Immutable public final class TimePickerColors {
ctor public TimePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long separatorColor, long pickerLabelColor, long confirmButtonContentColor, long confirmButtonContainerColor);
method public long getConfirmButtonContainerColor();
@@ -1142,26 +1350,6 @@
method public abstract void time();
}
- @androidx.compose.runtime.Immutable public final class ToggleButtonColors {
- ctor public ToggleButtonColors(long checkedContainerColor, long checkedContentColor, long uncheckedContainerColor, long uncheckedContentColor, long disabledCheckedContainerColor, long disabledCheckedContentColor, long disabledUncheckedContainerColor, long disabledUncheckedContentColor);
- method public long getCheckedContainerColor();
- method public long getCheckedContentColor();
- method public long getDisabledCheckedContainerColor();
- method public long getDisabledCheckedContentColor();
- method public long getDisabledUncheckedContainerColor();
- method public long getDisabledUncheckedContentColor();
- method public long getUncheckedContainerColor();
- method public long getUncheckedContentColor();
- property public final long checkedContainerColor;
- property public final long checkedContentColor;
- property public final long disabledCheckedContainerColor;
- property public final long disabledCheckedContentColor;
- property public final long disabledUncheckedContainerColor;
- property public final long disabledUncheckedContentColor;
- property public final long uncheckedContainerColor;
- property public final long uncheckedContentColor;
- }
-
public fun interface TouchExplorationStateProvider {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<java.lang.Boolean> touchExplorationState();
}
@@ -1217,32 +1405,6 @@
}
-package androidx.wear.compose.material3.dialog {
-
- public final class AlertDialogDefaults {
- method @androidx.compose.runtime.Composable public void BottomButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void GroupSeparator();
- method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
- method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
- method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
- method public float getEdgeButtonExtraTopPadding();
- method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
- property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> ConfirmIcon;
- property public final kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> DismissIcon;
- property public final androidx.compose.foundation.layout.Arrangement.Vertical VerticalArrangement;
- property public final float edgeButtonExtraTopPadding;
- field public static final androidx.wear.compose.material3.dialog.AlertDialogDefaults INSTANCE;
- }
-
- public final class AlertDialogKt {
- method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
- method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
- }
-
-}
-
package androidx.wear.compose.material3.lazy {
public final class LazyColumnScrollTransformModifiersKt {
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
index bc201f1..43171ec 100644
--- a/wear/compose/compose-material3/build.gradle
+++ b/wear/compose/compose-material3/build.gradle
@@ -32,21 +32,22 @@
}
dependencies {
- api("androidx.compose.foundation:foundation:1.6.0")
- api("androidx.compose.ui:ui:1.6.0")
- api("androidx.compose.ui:ui-text:1.6.0")
- api("androidx.compose.runtime:runtime:1.6.0")
+ api("androidx.compose.foundation:foundation:1.7.0")
+ api("androidx.compose.ui:ui:1.7.0")
+ api("androidx.compose.ui:ui-text:1.7.0")
+ api("androidx.compose.runtime:runtime:1.7.0")
api(project(":wear:compose:compose-foundation"))
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
- implementation("androidx.compose.animation:animation:1.6.0")
- implementation("androidx.compose.material:material-icons-core:1.6.0")
- implementation("androidx.compose.material:material-ripple:1.7.0-beta02")
- implementation("androidx.compose.ui:ui-util:1.6.0")
+ implementation("androidx.compose.animation:animation:1.7.0")
+ implementation("androidx.compose.material:material-icons-core:1.7.0")
+ implementation("androidx.compose.material:material-ripple:1.7.0")
+ implementation("androidx.compose.ui:ui-util:1.7.0")
implementation(project(":wear:compose:compose-material-core"))
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
implementation("androidx.graphics:graphics-shapes:1.0.0-beta01")
+ implementation project(':compose:animation:animation-graphics')
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/dialogs/AlertDialogs.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
similarity index 95%
rename from wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/dialogs/AlertDialogs.kt
rename to wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
index c71f190..4440425 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/dialogs/AlertDialogs.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.demos.dialogs
+package androidx.wear.compose.material3.demos
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -38,6 +38,8 @@
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
@@ -45,11 +47,9 @@
import androidx.wear.compose.material3.ScreenScaffold
import androidx.wear.compose.material3.SwitchButton
import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.dialog.AlertDialog
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults
-import androidx.wear.compose.material3.samples.dialog.AlertDialogWithBottomButtonSample
-import androidx.wear.compose.material3.samples.dialog.AlertDialogWithConfirmAndDismissSample
-import androidx.wear.compose.material3.samples.dialog.AlertDialogWithContentGroupsSample
+import androidx.wear.compose.material3.samples.AlertDialogWithBottomButtonSample
+import androidx.wear.compose.material3.samples.AlertDialogWithConfirmAndDismissSample
+import androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
val AlertDialogs =
listOf(
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index 9b5316a..1f5758e 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -21,7 +21,6 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
@@ -31,7 +30,6 @@
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonColors
import androidx.wear.compose.material3.ButtonDefaults
@@ -74,7 +72,7 @@
modifier = Modifier.fillMaxWidth()
)
},
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
)
}
item {
@@ -88,11 +86,11 @@
)
},
enabled = false,
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
)
}
item { ListHeader { Text("3 slot button") } }
- item { ButtonSample() }
+ item { ButtonSample(modifier = Modifier.fillMaxWidth()) }
item {
Button(
onClick = { /* Do something */ },
@@ -105,14 +103,17 @@
modifier = Modifier.size(ButtonDefaults.IconSize)
)
},
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
)
}
item { ListHeader { Text("Long Click") } }
item {
- ButtonWithOnLongClickSample({ showOnClickToast(context) }) {
- showOnLongClickToast(context)
- }
+ ButtonWithOnLongClickSample(
+ modifier = Modifier.fillMaxWidth(),
+ onClickHandler = { showOnClickToast(context) },
+ onLongClickHandler = { showOnLongClickToast(context) },
+ )
}
}
}
@@ -124,15 +125,17 @@
item { ListHeader { Text("1 slot button") } }
item { SimpleFilledTonalButtonSample() }
item {
- FilledTonalButtonWithOnLongClickSample({ showOnClickToast(context) }) {
- showOnLongClickToast(context)
- }
+ FilledTonalButtonWithOnLongClickSample(
+ onClickHandler = { showOnClickToast(context) },
+ onLongClickHandler = { showOnLongClickToast(context) }
+ )
}
item {
FilledTonalButton(
onClick = { /* Do something */ },
label = { Text("Filled Tonal Button") },
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
)
}
item { ListHeader { Text("3 slot button") } }
@@ -149,7 +152,8 @@
modifier = Modifier.size(ButtonDefaults.IconSize)
)
},
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
)
}
}
@@ -165,7 +169,8 @@
onClick = { /* Do something */ },
colors = ButtonDefaults.filledVariantButtonColors(),
label = { Text("Filled Variant Button") },
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
)
}
item { ListHeader { Text("3 slot button") } }
@@ -183,7 +188,8 @@
modifier = Modifier.size(ButtonDefaults.IconSize)
)
},
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
)
}
}
@@ -196,15 +202,17 @@
item { ListHeader { Text("1 slot button") } }
item { SimpleOutlinedButtonSample() }
item {
- OutlinedButtonWithOnLongClickSample({ showOnClickToast(context) }) {
- showOnLongClickToast(context)
- }
+ OutlinedButtonWithOnLongClickSample(
+ onClickHandler = { showOnClickToast(context) },
+ onLongClickHandler = { showOnLongClickToast(context) }
+ )
}
item {
OutlinedButton(
onClick = { /* Do something */ },
label = { Text("Outlined Button") },
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
)
}
item { ListHeader { Text("3 slot button") } }
@@ -221,7 +229,8 @@
modifier = Modifier.size(ButtonDefaults.IconSize)
)
},
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
)
}
}
@@ -234,15 +243,17 @@
item { ListHeader { Text("1 slot button") } }
item { SimpleChildButtonSample() }
item {
- ChildButtonWithOnLongClickSample({ showOnClickToast(context) }) {
- showOnLongClickToast(context)
- }
+ ChildButtonWithOnLongClickSample(
+ onClickHandler = { showOnClickToast(context) },
+ onLongClickHandler = { showOnLongClickToast(context) },
+ )
}
item {
ChildButton(
onClick = { /* Do something */ },
label = { Text("Child Button") },
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
)
}
item { ListHeader { Text("3 slot button") } }
@@ -259,7 +270,8 @@
modifier = Modifier.size(ButtonDefaults.IconSize)
)
},
- enabled = false
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
)
}
}
@@ -274,7 +286,7 @@
CompactButton(
onClick = { /* Do something */ },
colors = ButtonDefaults.buttonColors(),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -283,7 +295,7 @@
CompactButton(
onClick = { /* Do something */ },
colors = ButtonDefaults.filledVariantButtonColors(),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Filled Variant", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -292,7 +304,7 @@
CompactButton(
onClick = { /* Do something */ },
colors = ButtonDefaults.filledTonalButtonColors(),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Filled Tonal", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -302,7 +314,7 @@
onClick = { /* Do something */ },
colors = ButtonDefaults.outlinedButtonColors(),
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Outlined", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -314,7 +326,7 @@
onClick = { /* Do something */ },
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledVariantButtonColors(),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Filled Variant", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -324,7 +336,7 @@
onClick = { /* Do something */ },
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledTonalButtonColors(),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Filled Tonal", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -335,7 +347,7 @@
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.outlinedButtonColors(),
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Outlined", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -345,7 +357,7 @@
onClick = { /* Do something */ },
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.childButtonColors(),
- modifier = Modifier.width(150.dp)
+ modifier = Modifier.fillMaxWidth()
) {
Text("Child", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -354,14 +366,14 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) }
+ icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
)
}
item {
CompactButton(
onClick = { /* Do something */ },
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
- colors = ButtonDefaults.filledTonalButtonColors()
+ colors = ButtonDefaults.filledTonalButtonColors(),
)
}
item {
@@ -369,21 +381,22 @@
onClick = { /* Do something */ },
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.outlinedButtonColors(),
- border = ButtonDefaults.outlinedButtonBorder(enabled = true)
+ border = ButtonDefaults.outlinedButtonBorder(enabled = true),
)
}
item {
CompactButton(
onClick = { /* Do something */ },
icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
- colors = ButtonDefaults.childButtonColors()
+ colors = ButtonDefaults.childButtonColors(),
)
}
item { ListHeader { Text("Long Click") } }
item {
- CompactButtonWithOnLongClickSample({ showOnClickToast(context) }) {
- showOnLongClickToast(context)
- }
+ CompactButtonWithOnLongClickSample(
+ onClickHandler = { showOnClickToast(context) },
+ onLongClickHandler = { showOnLongClickToast(context) }
+ )
}
item { ListHeader { Text("Expandable") } }
item { OutlinedCompactButtonSample() }
@@ -469,6 +482,7 @@
label = label,
enabled = enabled,
colors = colors,
+ modifier = Modifier.fillMaxWidth(),
)
}
@@ -501,6 +515,7 @@
secondaryLabel = secondaryLabel,
enabled = enabled,
colors = colors,
+ modifier = Modifier.fillMaxWidth()
)
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ColorSchemeDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ColorSchemeDemo.kt
new file mode 100644
index 0000000..b99eb36
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ColorSchemeDemo.kt
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.Text
+
+@Composable
+fun ColorSchemeDemos() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Primary Colors") } }
+ item {
+ ButtonWithColor(
+ "Primary",
+ MaterialTheme.colorScheme.primary,
+ MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Primary Dim",
+ MaterialTheme.colorScheme.primaryDim,
+ MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Primary Container",
+ MaterialTheme.colorScheme.primaryContainer,
+ MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Primary",
+ MaterialTheme.colorScheme.onPrimary,
+ MaterialTheme.colorScheme.primary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Primary Container",
+ MaterialTheme.colorScheme.onPrimaryContainer,
+ MaterialTheme.colorScheme.primaryContainer
+ )
+ }
+
+ item { ListHeader { Text("Secondary Colors") } }
+ item {
+ ButtonWithColor(
+ "Secondary",
+ MaterialTheme.colorScheme.secondary,
+ MaterialTheme.colorScheme.onSecondary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Secondary Dim",
+ MaterialTheme.colorScheme.secondaryDim,
+ MaterialTheme.colorScheme.onSecondary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Secondary Container",
+ MaterialTheme.colorScheme.secondaryContainer,
+ MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Secondary",
+ MaterialTheme.colorScheme.onSecondary,
+ MaterialTheme.colorScheme.secondary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Secondary Container",
+ MaterialTheme.colorScheme.onSecondaryContainer,
+ MaterialTheme.colorScheme.secondaryContainer
+ )
+ }
+
+ item { ListHeader { Text("Tertiary Colors") } }
+ item {
+ ButtonWithColor(
+ "Tertiary",
+ MaterialTheme.colorScheme.tertiary,
+ MaterialTheme.colorScheme.onTertiary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Tertiary Dim",
+ MaterialTheme.colorScheme.tertiaryDim,
+ MaterialTheme.colorScheme.onTertiary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Tertiary Container",
+ MaterialTheme.colorScheme.tertiaryContainer,
+ MaterialTheme.colorScheme.onTertiaryContainer
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Tertiary",
+ MaterialTheme.colorScheme.onTertiary,
+ MaterialTheme.colorScheme.tertiary
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Tertiary Container",
+ MaterialTheme.colorScheme.onTertiaryContainer,
+ MaterialTheme.colorScheme.tertiaryContainer
+ )
+ }
+
+ item { ListHeader { Text("Surface Colors") } }
+ item {
+ ButtonWithColor(
+ "Surface Container",
+ MaterialTheme.colorScheme.surfaceContainer,
+ MaterialTheme.colorScheme.onSurface
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Surface Container Low",
+ MaterialTheme.colorScheme.surfaceContainerLow,
+ MaterialTheme.colorScheme.onSurface
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Surface Container High",
+ MaterialTheme.colorScheme.surfaceContainerHigh,
+ MaterialTheme.colorScheme.onSurface
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Surface",
+ MaterialTheme.colorScheme.onSurface,
+ MaterialTheme.colorScheme.surfaceContainer
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Surface Variant",
+ MaterialTheme.colorScheme.onSurfaceVariant,
+ MaterialTheme.colorScheme.surfaceContainer
+ )
+ }
+
+ item { ListHeader { Text("Background Colors") } }
+ item {
+ ButtonWithColor(
+ "Background",
+ MaterialTheme.colorScheme.background,
+ MaterialTheme.colorScheme.onBackground
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Background",
+ MaterialTheme.colorScheme.onBackground,
+ MaterialTheme.colorScheme.background
+ )
+ }
+
+ item { ListHeader { Text("Outline Colors") } }
+ item {
+ ButtonWithColor(
+ "Outline",
+ MaterialTheme.colorScheme.outline,
+ MaterialTheme.colorScheme.surfaceContainer
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Outline Variant",
+ MaterialTheme.colorScheme.outlineVariant,
+ MaterialTheme.colorScheme.surfaceContainer
+ )
+ }
+
+ item { ListHeader { Text("Error Colors") } }
+ item {
+ ButtonWithColor(
+ "Error",
+ MaterialTheme.colorScheme.error,
+ MaterialTheme.colorScheme.onError
+ )
+ }
+ item {
+ ButtonWithColor(
+ "Error Container",
+ MaterialTheme.colorScheme.errorContainer,
+ MaterialTheme.colorScheme.onErrorContainer
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Error",
+ MaterialTheme.colorScheme.onError,
+ MaterialTheme.colorScheme.error
+ )
+ }
+ item {
+ ButtonWithColor(
+ "On Error Container",
+ MaterialTheme.colorScheme.onErrorContainer,
+ MaterialTheme.colorScheme.errorContainer
+ )
+ }
+ }
+}
+
+@Composable
+private fun ButtonWithColor(text: String, containerColor: Color, contentColor: Color) {
+ Button(
+ onClick = {},
+ label = { Text(text, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) },
+ modifier = Modifier.fillMaxWidth(),
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = containerColor,
+ contentColor = contentColor
+ )
+ )
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Confirmations.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Confirmations.kt
new file mode 100644
index 0000000..9233b25
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Confirmations.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.Confirmation
+import androidx.wear.compose.material3.ConfirmationDefaults
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.samples.ConfirmationSample
+import androidx.wear.compose.material3.samples.FailureConfirmationSample
+import androidx.wear.compose.material3.samples.LongTextConfirmationSample
+import androidx.wear.compose.material3.samples.SuccessConfirmationSample
+
+val Comfirmations =
+ listOf(
+ ComposableDemo("Generic confirmation") { ConfirmationSample() },
+ ComposableDemo("Long content confirmation") { LongTextConfirmationSample() },
+ ComposableDemo("Success confirmation") { SuccessConfirmationSample() },
+ ComposableDemo("Failure confirmation") { FailureConfirmationSample() },
+ ComposableDemo("Confirmation without text") { ConfirmationWithoutText() },
+ ComposableDemo("Confirmation with custom colors") { ConfirmationWithCustomColors() },
+ )
+
+@Composable
+fun ConfirmationWithoutText() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") }
+ )
+ }
+
+ Confirmation(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = null
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ contentDescription = null,
+ modifier = Modifier.size(ConfirmationDefaults.IconSize).align(Alignment.Center),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+}
+
+@Composable
+fun ConfirmationWithCustomColors() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") }
+ )
+ }
+
+ Confirmation(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ colors =
+ ConfirmationDefaults.confirmationColors(
+ iconColor = MaterialTheme.colorScheme.tertiary,
+ iconContainerColor = MaterialTheme.colorScheme.onTertiary,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ curvedText = ConfirmationDefaults.curvedText("Custom confirmation")
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ contentDescription = null,
+ modifier = Modifier.size(ConfirmationDefaults.IconSize).align(Alignment.Center),
+ )
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CurvedTextDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CurvedTextDemo.kt
new file mode 100644
index 0000000..9be1de6
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CurvedTextDemo.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.CurvedAlignment
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.angularSizeDp
+import androidx.wear.compose.foundation.background
+import androidx.wear.compose.foundation.curvedBox
+import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.CurvedTextDefaults
+import androidx.wear.compose.material3.curvedText
+import androidx.wear.compose.material3.samples.CurvedTextBottom
+import androidx.wear.compose.material3.samples.CurvedTextTop
+
+val CurvedTextDemos =
+ listOf(
+ ComposableDemo("Top placement") { CurvedTextTop() },
+ ComposableDemo("Bottom placement") { CurvedTextBottom() },
+ ComposableDemo("Larger Font") { LargerFont() },
+ ComposableDemo("Kerning Test") { KerningDemo() },
+ ComposableDemo("Small Arc") { SmallArcDemo() },
+ ComposableDemo("Large Arc") { LargeArcDemo() },
+ )
+
+@Composable
+fun LargerFont() {
+ val backgroundColor = CurvedTextDefaults.backgroundColor()
+ CurvedLayout(modifier = Modifier.background(Color.DarkGray)) {
+ curvedRow(
+ CurvedModifier.background(backgroundColor, StrokeCap.Round),
+ radialAlignment = CurvedAlignment.Radial.Center
+ ) {
+ curvedText("Larger", fontSize = 24.sp)
+ curvedBox(CurvedModifier.angularSizeDp(5.dp)) {}
+ curvedText("Normal")
+ }
+ }
+}
+
+@Composable
+fun KerningDemo() {
+ val backgroundColor = CurvedTextDefaults.backgroundColor()
+ Box(Modifier.background(Color.DarkGray)) {
+ CurvedLayout {
+ curvedText("MMMMMMMM", CurvedModifier.background(backgroundColor, StrokeCap.Round))
+ }
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) {
+ curvedText("MMMMMMMM", CurvedModifier.background(backgroundColor, StrokeCap.Round))
+ }
+ }
+}
+
+@Composable
+fun SmallArcDemo() {
+ val backgroundColor = CurvedTextDefaults.backgroundColor()
+ CurvedLayout(Modifier.background(Color.DarkGray)) {
+ // Default sweep is 70 degrees
+ curvedText(
+ "Long text that will be cut for sure.",
+ CurvedModifier.background(backgroundColor, StrokeCap.Round),
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
+
+@Composable
+fun LargeArcDemo() {
+ val backgroundColor = CurvedTextDefaults.backgroundColor()
+ CurvedLayout(Modifier.background(Color.DarkGray)) {
+ // Static content can use 120 degrees
+ curvedText(
+ "Long text that will be cut for sure.",
+ CurvedModifier.background(backgroundColor, StrokeCap.Round),
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
new file mode 100644
index 0000000..fda5b03
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.samples.DatePickerFromDateToDateSample
+import androidx.wear.compose.material3.samples.DatePickerSample
+import androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
+
+@RequiresApi(Build.VERSION_CODES.O)
+val DatePickerDemos =
+ listOf(
+ ComposableDemo("Date Year-Month-Day") { DatePickerYearMonthDaySample() },
+ ComposableDemo("Date System date format") { DatePickerSample() },
+ ComposableDemo("Date Range") { DatePickerFromDateToDateSample() },
+ )
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt
index cd8d533..8a9bbea 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Haptics.kt
@@ -18,14 +18,13 @@
import android.view.HapticFeedbackConstants
import android.view.View
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.Text
@Composable
@@ -64,7 +63,8 @@
Pair(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE, "Virtual Key Release"),
)
- ScalingLazyDemo(contentPadding = PaddingValues(horizontal = 20.dp)) {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Haptic Constants") } }
items(hapticConstants.size) { index ->
val (constant, name) = hapticConstants[index]
HapticsDemo(haptics, constant, name)
@@ -89,6 +89,6 @@
private class HapticFeedbackProvider(private val view: View) {
fun performHapticFeedback(feedbackConstant: Int) {
- view.let { view -> view.performHapticFeedback(feedbackConstant) }
+ view.performHapticFeedback(feedbackConstant)
}
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index 127bec3..0d75c07 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.ButtonDefaults
@@ -41,6 +42,8 @@
import androidx.wear.compose.material3.samples.FilledTonalIconButtonSample
import androidx.wear.compose.material3.samples.FilledVariantIconButtonSample
import androidx.wear.compose.material3.samples.IconButtonSample
+import androidx.wear.compose.material3.samples.IconButtonWithCornerAnimationSample
+import androidx.wear.compose.material3.samples.IconButtonWithImageSample
import androidx.wear.compose.material3.samples.IconButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.OutlinedIconButtonSample
import androidx.wear.compose.material3.touchTargetAwareSize
@@ -106,24 +109,9 @@
item { ListHeader { Text("Corner Animation") } }
item {
Row {
- val interactionSource1 = remember { MutableInteractionSource() }
- FilledIconButton(
- onClick = {},
- shape = IconButtonDefaults.animatedShape(interactionSource1),
- interactionSource = interactionSource1
- ) {
- StandardIcon(ButtonDefaults.IconSize)
- }
+ IconButtonWithCornerAnimationSample()
Spacer(modifier = Modifier.width(5.dp))
- val interactionSource2 = remember { MutableInteractionSource() }
- FilledIconButton(
- onClick = {},
- colors = IconButtonDefaults.filledVariantIconButtonColors(),
- shape = IconButtonDefaults.animatedShape(interactionSource2),
- interactionSource = interactionSource2
- ) {
- StandardIcon(ButtonDefaults.IconSize)
- }
+ IconButtonWithCornerAnimationSample()
}
}
item { ListHeader { Text("Morphed Animation") } }
@@ -192,6 +180,43 @@
}
@Composable
+fun ImageButtonDemo() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Image Button") } }
+ item {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButtonWithImageSample(
+ painterResource(R.drawable.card_background),
+ enabled = true,
+ )
+ Spacer(modifier = Modifier.width(5.dp))
+ IconButtonWithImageSample(
+ painterResource(R.drawable.card_background),
+ enabled = false
+ )
+ }
+ }
+ item { ListHeader { Text("Animated Shape") } }
+ item {
+ val interactionSource = remember { MutableInteractionSource() }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButtonWithImageSample(
+ painterResource(R.drawable.card_background),
+ enabled = true,
+ interactionSource = interactionSource,
+ shape = IconButtonDefaults.animatedShape(interactionSource)
+ )
+ Spacer(modifier = Modifier.width(5.dp))
+ IconButtonWithImageSample(
+ painterResource(R.drawable.card_background),
+ enabled = false,
+ )
+ }
+ }
+ }
+}
+
+@Composable
private fun IconButtonWithSize(size: Dp) {
FilledTonalIconButton(
modifier = Modifier.touchTargetAwareSize(size),
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Material3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Material3Demos.kt
index 3d39cb3..394a52f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Material3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Material3Demos.kt
@@ -21,6 +21,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
@@ -28,7 +30,8 @@
@Composable
fun ScalingLazyDemo(
- contentPadding: PaddingValues = PaddingValues(),
+ contentPadding: PaddingValues =
+ PaddingValues(horizontal = LocalConfiguration.current.screenWidthDp.dp * 0.052f),
content: ScalingLazyListScope.() -> Unit
) {
val scrollState = rememberScalingLazyListState()
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/OpenOnPhoneDialogDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/OpenOnPhoneDialogDemo.kt
new file mode 100644
index 0000000..ab628e5
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/OpenOnPhoneDialogDemo.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.OpenOnPhoneDialog
+import androidx.wear.compose.material3.OpenOnPhoneDialogDefaults
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.samples.OpenOnPhoneDialogSample
+
+val OpenOnPhoneDialogDemos =
+ listOf(
+ ComposableDemo("Default OpenOnPhone Dialog") { OpenOnPhoneDialogSample() },
+ ComposableDemo("With custom text") { OpenOnPhoneDialogWithCustomText() },
+ ComposableDemo("With custom colors") { OpenOnPhoneDialogWithCustomColors() },
+ )
+
+@Composable
+fun OpenOnPhoneDialogWithCustomText() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Open on phone") }
+ )
+ }
+
+ OpenOnPhoneDialog(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = OpenOnPhoneDialogDefaults.curvedText("Custom text")
+ )
+}
+
+@Composable
+fun OpenOnPhoneDialogWithCustomColors() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Open on phone") }
+ )
+ }
+
+ OpenOnPhoneDialog(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ colors =
+ OpenOnPhoneDialogDefaults.colors(
+ iconColor = MaterialTheme.colorScheme.tertiary,
+ iconContainerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ progressIndicatorColor = MaterialTheme.colorScheme.tertiary,
+ progressTrackColor = MaterialTheme.colorScheme.onTertiary,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ )
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt
index 7149870..83f1564 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/PickerDemo.kt
@@ -16,88 +16,27 @@
package androidx.wear.compose.material3.demos
-import android.os.Build
-import androidx.compose.foundation.layout.Arrangement
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.height
import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.unit.dp
import androidx.wear.compose.integration.demos.common.ComposableDemo
-import androidx.wear.compose.material3.Button
-import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.Picker
import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.TimePicker
-import androidx.wear.compose.material3.TimePickerType
import androidx.wear.compose.material3.rememberPickerState
import androidx.wear.compose.material3.samples.AutoCenteringPickerGroup
import androidx.wear.compose.material3.samples.PickerAnimateScrollToOption
import androidx.wear.compose.material3.samples.PickerGroupSample
import androidx.wear.compose.material3.samples.SimplePicker
-import androidx.wear.compose.material3.samples.TimePickerSample
-import androidx.wear.compose.material3.samples.TimePickerWith12HourClockSample
-import androidx.wear.compose.material3.samples.TimePickerWithSecondsSample
-import java.time.LocalTime
-import java.time.format.DateTimeFormatter
val PickerDemos =
listOf(
- // Requires API level 26 or higher due to java.time dependency.
- *(if (Build.VERSION.SDK_INT >= 26)
- arrayOf(
- ComposableDemo("Time HH:MM:SS") { TimePickerWithSecondsSample() },
- ComposableDemo("Time HH:MM") {
- var showTimePicker by remember { mutableStateOf(true) }
- var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
- val formatter = DateTimeFormatter.ofPattern("HH:mm")
- if (showTimePicker) {
- TimePicker(
- onTimePicked = {
- timePickerTime = it
- showTimePicker = false
- },
- timePickerType = TimePickerType.HoursMinutes24H,
- // Initialize with last picked time on reopen
- initialTime = timePickerTime
- )
- } else {
- Column(
- modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text("Selected Time")
- Spacer(Modifier.height(12.dp))
- Button(
- onClick = { showTimePicker = true },
- label = { Text(timePickerTime.format(formatter)) },
- icon = {
- Icon(
- imageVector = Icons.Filled.Edit,
- contentDescription = "Edit"
- )
- },
- )
- }
- }
- },
- ComposableDemo("Time 12 Hour") { TimePickerWith12HourClockSample() },
- ComposableDemo("Time System time format") { TimePickerSample() },
- )
- else emptyArray<ComposableDemo>()),
ComposableDemo("Simple Picker") { SimplePicker() },
ComposableDemo("No gradient") { PickerWithoutGradient() },
ComposableDemo("Animate picker change") { PickerAnimateScrollToOption() },
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt
new file mode 100644
index 0000000..92d8d87
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimePickerDemo.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TimePicker
+import androidx.wear.compose.material3.TimePickerType
+import androidx.wear.compose.material3.samples.TimePickerSample
+import androidx.wear.compose.material3.samples.TimePickerWith12HourClockSample
+import androidx.wear.compose.material3.samples.TimePickerWithSecondsSample
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+
+@RequiresApi(Build.VERSION_CODES.O)
+val TimePickerDemos =
+ listOf(
+ ComposableDemo("Time HH:MM:SS") { TimePickerWithSecondsSample() },
+ ComposableDemo("Time HH:MM") { TimePicker24hWithoutSecondsDemo() },
+ ComposableDemo("Time 12 Hour") { TimePickerWith12HourClockSample() },
+ ComposableDemo("Time System time format") { TimePickerSample() },
+ )
+
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+private fun TimePicker24hWithoutSecondsDemo() {
+ var showTimePicker by remember { mutableStateOf(true) }
+ var timePickerTime by remember { mutableStateOf(LocalTime.now()) }
+ val formatter = DateTimeFormatter.ofPattern("HH:mm")
+ if (showTimePicker) {
+ TimePicker(
+ onTimePicked = {
+ timePickerTime = it
+ showTimePicker = false
+ },
+ timePickerType = TimePickerType.HoursMinutes24H,
+ // Initialize with last picked time on reopen
+ initialTime = timePickerTime
+ )
+ } else {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Selected Time")
+ Spacer(Modifier.height(12.dp))
+ Button(
+ onClick = { showTimePicker = true },
+ label = { Text(timePickerTime.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
index a32b271..37bfa5b 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TimeTextDemo.kt
@@ -16,17 +16,32 @@
package androidx.wear.compose.material3.demos
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
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.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.OutlinedButton
+import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TimeText
import androidx.wear.compose.material3.TimeTextDefaults
import androidx.wear.compose.material3.samples.TimeTextClockOnly
-import androidx.wear.compose.material3.samples.TimeTextWithIcon
import androidx.wear.compose.material3.samples.TimeTextWithStatus
val TimeTextDemos =
@@ -35,7 +50,9 @@
ComposableDemo("Clock with Status") { TimeTextWithStatus() },
ComposableDemo("Clock with long Status") { TimeTextWithLongStatus() },
ComposableDemo("Clock with Icon") { TimeTextWithIcon() },
- ComposableDemo("Clock with custom colors") { TimeTextWithCustomColors() }
+ ComposableDemo("Clock with custom colors") { TimeTextWithCustomColors() },
+ ComposableDemo("Clock with custom font size") { TimeTextCustomSize() },
+ ComposableDemo("Clock on list") { TimeTextOnScreen() }
)
@Composable
@@ -59,3 +76,77 @@
time()
}
}
+
+@Composable
+fun TimeTextCustomSize() {
+ val customStyle = TimeTextDefaults.timeTextStyle(color = Color.Green, fontSize = 24.sp)
+
+ TimeText {
+ text("ETA", customStyle)
+ separator()
+ time()
+ }
+}
+
+@Composable
+fun TimeTextWithIcon() {
+ TimeText {
+ time()
+ separator()
+ composable {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Favorite",
+ modifier = Modifier.size(13.dp)
+ )
+ }
+ }
+}
+
+@Composable
+fun TimeTextOnScreen() {
+ val colors =
+ listOf(
+ ButtonDefaults.buttonColors(),
+ ButtonDefaults.filledTonalButtonColors(),
+ ButtonDefaults.filledVariantButtonColors(),
+ ButtonDefaults.childButtonColors(),
+ )
+
+ val screenWidth = LocalConfiguration.current.screenWidthDp.dp
+ val padding = PaddingValues(horizontal = screenWidth * 0.052f) // 5.2% margin on the sides.
+
+ // It's preferable to use the ScreenScaffold, this is a demo of the simplest usage.
+ Box(Modifier.fillMaxSize()) {
+ ScalingLazyColumn(Modifier.fillMaxSize(), contentPadding = padding) {
+ item { Text("Buttons") }
+ item {
+ OutlinedButton(onClick = {}) {
+ Text("Outlined Button", Modifier.align(Alignment.CenterVertically))
+ }
+ }
+ item {
+ Button(
+ onClick = {},
+ colors =
+ ButtonDefaults.imageBackgroundButtonColors(
+ painterResource(R.drawable.card_background)
+ )
+ ) {
+ Text("Image Button", Modifier.align(Alignment.CenterVertically))
+ }
+ }
+ items(colors.size) {
+ Button(onClick = {}, colors = colors[it]) {
+ Text(
+ "Item with long content so test overlap.",
+ Modifier.align(Alignment.CenterVertically)
+ )
+ }
+ }
+ items(10) { Text("Some extra items ($it) to scroll", Modifier.padding(5.dp)) }
+ }
+ // Timetext later so it's on top.
+ TimeText { time() }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 63cf38f..2f9ce14 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -22,7 +22,6 @@
import androidx.wear.compose.integration.demos.common.Centralize
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.integration.demos.common.Material3DemoCategory
-import androidx.wear.compose.material3.demos.dialogs.AlertDialogs
import androidx.wear.compose.material3.samples.AnimatedTextSample
import androidx.wear.compose.material3.samples.AnimatedTextSampleButtonResponse
import androidx.wear.compose.material3.samples.AnimatedTextSampleSharedFontRegistry
@@ -44,12 +43,11 @@
Material3DemoCategory(
"Material 3",
listOf(
- Material3DemoCategory(
- "Dialogs",
- listOf(
- Material3DemoCategory("AlertDialog", AlertDialogs),
- )
- ),
+ ComposableDemo("Color Scheme") { ColorSchemeDemos() },
+ Material3DemoCategory("Curved Text", CurvedTextDemos),
+ Material3DemoCategory("Alert Dialog", AlertDialogs),
+ Material3DemoCategory("Confirmation", Comfirmations),
+ Material3DemoCategory("Open on phone Dialog", OpenOnPhoneDialogDemos),
ComposableDemo("Scaffold") { ScaffoldSample() },
Material3DemoCategory("ScrollAway", ScrollAwayDemos),
ComposableDemo("Haptics") { Centralize { HapticsDemos() } },
@@ -68,6 +66,7 @@
),
ComposableDemo("Compact Button") { CompactButtonDemo() },
ComposableDemo("Icon Button") { IconButtonDemo() },
+ ComposableDemo("Image Button") { ImageButtonDemo() },
ComposableDemo("Text Button") { TextButtonDemo() },
Material3DemoCategory(
"Edge Button",
@@ -121,6 +120,13 @@
),
Material3DemoCategory("Slider", SliderDemos),
Material3DemoCategory("Picker", PickerDemos),
+ // Requires API level 26 or higher due to java.time dependency.
+ *(if (Build.VERSION.SDK_INT >= 26)
+ arrayOf(
+ Material3DemoCategory("TimePicker", TimePickerDemos),
+ Material3DemoCategory("DatePicker", DatePickerDemos)
+ )
+ else emptyArray<Material3DemoCategory>()),
Material3DemoCategory("Progress Indicator", ProgressIndicatorDemos),
Material3DemoCategory("Scroll Indicator", ScrollIndicatorDemos),
Material3DemoCategory("Placeholder", PlaceholderDemos),
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/dialog/AlertDialogSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt
similarity index 96%
rename from wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/dialog/AlertDialogSample.kt
rename to wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt
index e20161b..dae4eef 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/dialog/AlertDialogSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.samples.dialog
+package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Box
@@ -31,12 +31,12 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.AlertDialog
+import androidx.wear.compose.material3.AlertDialogDefaults
import androidx.wear.compose.material3.FilledTonalButton
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.dialog.AlertDialog
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults
@Sampled
@Composable
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
index 15b722e..b751fc8 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
@@ -41,19 +42,24 @@
@Sampled
@Composable
-fun ButtonWithOnLongClickSample(onClickHandler: () -> Unit, onLongClickHandler: () -> Unit) {
+fun ButtonWithOnLongClickSample(
+ onClickHandler: () -> Unit,
+ onLongClickHandler: () -> Unit,
+ modifier: Modifier = Modifier.fillMaxWidth(),
+) {
Button(
onClick = onClickHandler,
onLongClick = onLongClickHandler,
onLongClickLabel = "Long click",
label = { Text("Button") },
- secondaryLabel = { Text("with long click") }
+ secondaryLabel = { Text("with long click") },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun ButtonSample() {
+fun ButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { /* Do something */ },
label = { Text("Button") },
@@ -64,34 +70,41 @@
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
- }
+ },
+ modifier = modifier
)
}
@Sampled
@Composable
-fun SimpleFilledTonalButtonSample() {
- FilledTonalButton(onClick = { /* Do something */ }, label = { Text("Filled Tonal Button") })
+fun SimpleFilledTonalButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ modifier = modifier,
+ )
}
@Sampled
@Composable
fun FilledTonalButtonWithOnLongClickSample(
onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit
+ onLongClickHandler: () -> Unit,
+ modifier: Modifier = Modifier.fillMaxWidth()
) {
FilledTonalButton(
onClick = onClickHandler,
onLongClick = onLongClickHandler,
onLongClickLabel = "Long click",
label = { Text("Filled Tonal Button") },
- secondaryLabel = { Text("with long click") }
+ secondaryLabel = { Text("with long click") },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun FilledTonalButtonSample() {
+fun FilledTonalButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
FilledTonalButton(
onClick = { /* Do something */ },
label = { Text("Filled Tonal Button") },
@@ -102,23 +115,25 @@
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
- }
+ },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun SimpleFilledVariantButtonSample() {
+fun SimpleFilledVariantButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { /* Do something */ },
colors = ButtonDefaults.filledVariantButtonColors(),
- label = { Text("Filled Variant Button") }
+ label = { Text("Filled Variant Button") },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun FilledVariantButtonSample() {
+fun FilledVariantButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { /* Do something */ },
colors = ButtonDefaults.filledVariantButtonColors(),
@@ -130,34 +145,41 @@
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
- }
+ },
+ modifier = modifier
)
}
@Sampled
@Composable
-fun SimpleOutlinedButtonSample() {
- OutlinedButton(onClick = { /* Do something */ }, label = { Text("Outlined Button") })
+fun SimpleOutlinedButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ modifier = modifier,
+ )
}
@Sampled
@Composable
fun OutlinedButtonWithOnLongClickSample(
onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit
+ onLongClickHandler: () -> Unit,
+ modifier: Modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = onClickHandler,
onLongClick = onLongClickHandler,
onLongClickLabel = "Long click",
label = { Text("Outlined Button") },
- secondaryLabel = { Text("with long click") }
+ secondaryLabel = { Text("with long click") },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun OutlinedButtonSample() {
+fun OutlinedButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = { /* Do something */ },
label = { Text("Outlined Button") },
@@ -168,31 +190,41 @@
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
- }
+ },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun SimpleChildButtonSample() {
- ChildButton(onClick = { /* Do something */ }, label = { Text("Child Button") })
+fun SimpleChildButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ modifier = modifier,
+ )
}
@Sampled
@Composable
-fun ChildButtonWithOnLongClickSample(onClickHandler: () -> Unit, onLongClickHandler: () -> Unit) {
+fun ChildButtonWithOnLongClickSample(
+ onClickHandler: () -> Unit,
+ onLongClickHandler: () -> Unit,
+ modifier: Modifier = Modifier.fillMaxWidth()
+) {
ChildButton(
onClick = onClickHandler,
onLongClick = onLongClickHandler,
onLongClickLabel = "Long click",
label = { Text("Child Button") },
- secondaryLabel = { Text("with long click") }
+ secondaryLabel = { Text("with long click") },
+ modifier = modifier,
)
}
@Sampled
@Composable
-fun ChildButtonSample() {
+fun ChildButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
ChildButton(
onClick = { /* Do something */ },
label = { Text("Child Button") },
@@ -203,41 +235,14 @@
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
- }
+ },
+ modifier = modifier
)
}
@Sampled
@Composable
-fun CompactButtonSample() {
- CompactButton(
- onClick = { /* Do something */ },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.SmallIconSize)
- )
- }
- ) {
- Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
- }
-}
-
-@Sampled
-@Composable
-fun CompactButtonWithOnLongClickSample(onClickHandler: () -> Unit, onLongClickHandler: () -> Unit) {
- CompactButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Long clickable") }
- )
-}
-
-@Sampled
-@Composable
-fun FilledTonalCompactButtonSample() {
+fun CompactButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
CompactButton(
onClick = { /* Do something */ },
icon = {
@@ -247,7 +252,42 @@
modifier = Modifier.size(ButtonDefaults.SmallIconSize)
)
},
- colors = ButtonDefaults.filledTonalButtonColors()
+ modifier = modifier,
+ ) {
+ Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+}
+
+@Sampled
+@Composable
+fun CompactButtonWithOnLongClickSample(
+ onClickHandler: () -> Unit,
+ onLongClickHandler: () -> Unit,
+ modifier: Modifier = Modifier.fillMaxWidth()
+) {
+ CompactButton(
+ onClick = onClickHandler,
+ onLongClick = onLongClickHandler,
+ onLongClickLabel = "Long click",
+ label = { Text("Long clickable") },
+ modifier = modifier,
+ )
+}
+
+@Sampled
+@Composable
+fun FilledTonalCompactButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
+ CompactButton(
+ onClick = { /* Do something */ },
+ icon = {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.SmallIconSize)
+ )
+ },
+ colors = ButtonDefaults.filledTonalButtonColors(),
+ modifier = modifier,
) {
Text("Filled Tonal Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@@ -255,7 +295,7 @@
@Sampled
@Composable
-fun OutlinedCompactButtonSample() {
+fun OutlinedCompactButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
CompactButton(
onClick = { /* Do something */ },
icon = {
@@ -266,7 +306,8 @@
)
},
colors = ButtonDefaults.outlinedButtonColors(),
- border = ButtonDefaults.outlinedButtonBorder(enabled = true)
+ border = ButtonDefaults.outlinedButtonBorder(enabled = true),
+ modifier = modifier,
) {
Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis)
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ConfirmationSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ConfirmationSample.kt
new file mode 100644
index 0000000..46bde32
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ConfirmationSample.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.Confirmation
+import androidx.wear.compose.material3.ConfirmationDefaults
+import androidx.wear.compose.material3.FailureConfirmation
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.SuccessConfirmation
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Composable
+fun ConfirmationSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") }
+ )
+ }
+
+ // Has an icon and a short curved text content, which will be displayed along the bottom edge of
+ // the screen.
+ Confirmation(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ curvedText = ConfirmationDefaults.curvedText("Confirmed")
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ contentDescription = null,
+ modifier = Modifier.size(ConfirmationDefaults.IconSize),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun LongTextConfirmationSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") }
+ )
+ }
+
+ // Has an icon and a text content. Text will be displayed in the center of the screen below the
+ // icon.
+ Confirmation(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ text = { Text(text = "Your message has been sent") },
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Add,
+ contentDescription = null,
+ modifier = Modifier.size(ConfirmationDefaults.SmallIconSize),
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun FailureConfirmationSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") }
+ )
+ }
+
+ FailureConfirmation(show = showConfirmation, onDismissRequest = { showConfirmation = false })
+}
+
+@Sampled
+@Composable
+fun SuccessConfirmationSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Show Confirmation") }
+ )
+ }
+
+ SuccessConfirmation(show = showConfirmation, onDismissRequest = { showConfirmation = false })
+}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CurvedTextSamples.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CurvedTextSamples.kt
new file mode 100644
index 0000000..8fef8a0
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CurvedTextSamples.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.StrokeCap.Companion.Round
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.angularSizeDp
+import androidx.wear.compose.foundation.background
+import androidx.wear.compose.foundation.curvedBox
+import androidx.wear.compose.foundation.curvedComposable
+import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.curvedText
+
+@Sampled
+@Composable
+fun CurvedTextTop() {
+ val backgroundColor = MaterialTheme.colorScheme.onPrimary
+ val customColor = MaterialTheme.colorScheme.tertiaryDim
+ CurvedLayout {
+ curvedRow(CurvedModifier.background(backgroundColor, Round)) {
+ curvedText("Calling", color = customColor)
+ curvedBox(CurvedModifier.angularSizeDp(5.dp)) {}
+ curvedText("Camilia Garcia")
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun CurvedTextBottom() {
+ val backgroundColor = MaterialTheme.colorScheme.onPrimary
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) {
+ curvedRow(CurvedModifier.background(backgroundColor, Round)) {
+ curvedComposable {
+ Icon(
+ Icons.Filled.Warning,
+ contentDescription = "Warning",
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ }
+ curvedText("Error - network lost")
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
new file mode 100644
index 0000000..2b25252
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.DatePicker
+import androidx.wear.compose.material3.DatePickerType
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.Text
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+@Sampled
+@Composable
+fun DatePickerSample() {
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+ val formatter =
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+ .withLocale(LocalConfiguration.current.locales[0])
+ if (showDatePicker) {
+ DatePicker(
+ initialDate = datePickerDate, // Initialize with last picked date on reopen
+ onDatePicked = {
+ datePickerDate = it
+ showDatePicker = false
+ }
+ )
+ } else {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Button(
+ onClick = { showDatePicker = true },
+ label = { Text("Selected Date") },
+ secondaryLabel = { Text(datePickerDate.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun DatePickerYearMonthDaySample() {
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
+ val formatter = DateTimeFormatter.ofPattern("yyyy MMM d")
+ if (showDatePicker) {
+ DatePicker(
+ initialDate = datePickerDate, // Initialize with last picked date on reopen
+ onDatePicked = {
+ datePickerDate = it
+ showDatePicker = false
+ },
+ datePickerType = DatePickerType.YearMonthDay
+ )
+ } else {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Button(
+ onClick = { showDatePicker = true },
+ label = { Text("Selected Date") },
+ secondaryLabel = { Text(datePickerDate.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun DatePickerFromDateToDateSample() {
+ var showDatePicker by remember { mutableStateOf(true) }
+ var datePickerDate by remember { mutableStateOf(LocalDate.of(2024, 9, 2)) }
+ val formatter =
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+ .withLocale(LocalConfiguration.current.locales[0])
+ if (showDatePicker) {
+ DatePicker(
+ initialDate = datePickerDate, // Initialize with last picked date on reopen
+ onDatePicked = {
+ datePickerDate = it
+ showDatePicker = false
+ },
+ minDate = LocalDate.of(2023, 10, 15),
+ maxDate = LocalDate.of(2025, 2, 4),
+ datePickerType = DatePickerType.YearMonthDay
+ )
+ } else {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Button(
+ onClick = { showDatePicker = true },
+ label = { Text("Selected Date") },
+ secondaryLabel = { Text(datePickerDate.format(formatter)) },
+ icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") },
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconButtonSample.kt
index e75fafa..46f3bc4 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconButtonSample.kt
@@ -17,11 +17,17 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.Image
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
import androidx.wear.compose.material3.FilledIconButton
import androidx.wear.compose.material3.FilledTonalIconButton
import androidx.wear.compose.material3.Icon
@@ -88,7 +94,7 @@
@Sampled
fun IconButtonWithCornerAnimationSample() {
val interactionSource = remember { MutableInteractionSource() }
- IconButton(
+ FilledIconButton(
onClick = { /* Do something */ },
shape = IconButtonDefaults.animatedShape(interactionSource),
interactionSource = interactionSource
@@ -96,3 +102,26 @@
Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
}
}
+
+@Composable
+@Sampled
+fun IconButtonWithImageSample(
+ painter: Painter,
+ enabled: Boolean,
+ interactionSource: MutableInteractionSource? = null,
+ shape: Shape = IconButtonDefaults.shape
+) {
+ IconButton(
+ onClick = { /* Do something */ },
+ interactionSource = interactionSource,
+ shape = shape,
+ ) {
+ Image(
+ painter = painter,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ if (enabled) Modifier else Modifier.alpha(IconButtonDefaults.disabledImageOpacity)
+ )
+ }
+}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/OpenOnPhoneDialogSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/OpenOnPhoneDialogSample.kt
new file mode 100644
index 0000000..741132e
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/OpenOnPhoneDialogSample.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.OpenOnPhoneDialog
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Composable
+fun OpenOnPhoneDialogSample() {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ Box(Modifier.fillMaxSize()) {
+ FilledTonalButton(
+ modifier = Modifier.align(Alignment.Center),
+ onClick = { showConfirmation = true },
+ label = { Text("Open on phone") }
+ )
+ }
+
+ OpenOnPhoneDialog(
+ show = showConfirmation,
+ onDismissRequest = { showConfirmation = false },
+ )
+}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/StepperSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/StepperSample.kt
index f4f33f7..65b3277 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/StepperSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/StepperSample.kt
@@ -17,14 +17,19 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.LevelIndicator
import androidx.wear.compose.material3.Stepper
import androidx.wear.compose.material3.StepperDefaults
import androidx.wear.compose.material3.Text
@@ -34,16 +39,24 @@
@OptIn(ExperimentalWearMaterial3Api::class)
@Composable
fun StepperSample() {
- 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
- ) {
- Text("Value: $value")
+ var value by remember { mutableFloatStateOf(2f) }
+ val valueRange = 0f..4f
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = valueRange,
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ steps = 7
+ ) {
+ Text(String.format("Value: %.1f".format(value)))
+ }
+ LevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.align(Alignment.CenterStart)
+ )
}
}
@@ -51,15 +64,23 @@
@OptIn(ExperimentalWearMaterial3Api::class)
@Composable
fun StepperWithIntegerSample() {
- var value by remember { mutableStateOf(2) }
- Stepper(
- value = value,
- onValueChange = { value = it },
- increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
- decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
- valueProgression = 1..10
- ) {
- Text("Value: $value")
+ var value by remember { mutableIntStateOf(3) }
+ val valueProgression = 0..10
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ valueProgression = valueProgression
+ ) {
+ Text(String.format("Value: %d".format(value)))
+ }
+ LevelIndicator(
+ value = { value },
+ valueProgression = valueProgression,
+ modifier = Modifier.align(Alignment.CenterStart)
+ )
}
}
@@ -67,20 +88,26 @@
@OptIn(ExperimentalWearMaterial3Api::class)
@Composable
fun StepperWithRangeSemanticsSample() {
- var value by remember { mutableStateOf(2f) }
- val valueRange = 1f..4f
+ var value by remember { mutableFloatStateOf(2f) }
+ val valueRange = 0f..4f
val onValueChange = { i: Float -> value = i }
val steps = 7
-
- Stepper(
- value = value,
- onValueChange = onValueChange,
- valueRange = valueRange,
- modifier = Modifier.rangeSemantics(value, true, onValueChange, valueRange, steps),
- increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
- decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
- steps = steps,
- ) {
- Text("Value: $value")
+ Box(modifier = Modifier.fillMaxSize()) {
+ Stepper(
+ value = value,
+ onValueChange = onValueChange,
+ valueRange = valueRange,
+ modifier = Modifier.rangeSemantics(value, true, onValueChange, valueRange, steps),
+ increaseIcon = { Icon(StepperDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(StepperDefaults.Decrease, "Decrease") },
+ steps = steps,
+ ) {
+ Text("Value: $value")
+ }
+ LevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.align(Alignment.CenterStart)
+ )
}
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
index b4bf119..f7d9a8e 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TimeTextSample.kt
@@ -17,14 +17,10 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.TimeTextDefaults
@Sampled
@Composable
@@ -35,25 +31,11 @@
@Sampled
@Composable
fun TimeTextWithStatus() {
+ val primaryStyle =
+ TimeTextDefaults.timeTextStyle(color = MaterialTheme.colorScheme.primaryContainer)
TimeText {
- text("ETA 12:48")
+ text("ETA 12:48", style = primaryStyle)
separator()
time()
}
}
-
-@Sampled
-@Composable
-fun TimeTextWithIcon() {
- TimeText {
- time()
- separator()
- composable {
- Icon(
- imageVector = Icons.Filled.Favorite,
- contentDescription = "Favorite",
- modifier = Modifier.size(13.dp)
- )
- }
- }
-}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/dialog/AlertDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
similarity index 94%
rename from wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/dialog/AlertDialogScreenshotTest.kt
rename to wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
index ce54d35..e049442 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/dialog/AlertDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.dialog
+package androidx.wear.compose.material3
import android.content.res.Configuration
import android.os.Build
@@ -41,14 +41,6 @@
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
-import androidx.wear.compose.material3.FilledTonalButton
-import androidx.wear.compose.material3.Icon
-import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
-import androidx.wear.compose.material3.ScreenSize
-import androidx.wear.compose.material3.TEST_TAG
-import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.methodNameWithValidCharacters
-import androidx.wear.compose.material3.setContentWithTheme
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import org.junit.Rule
@@ -278,7 +270,7 @@
}
onNodeWithTag(TEST_TAG)
.captureToImage()
- .assertAgainstGolden(screenshotRule, testName.methodNameWithValidCharacters())
+ .assertAgainstGolden(screenshotRule, testName.goldenIdentifier())
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/dialog/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
similarity index 96%
rename from wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/dialog/AlertDialogTest.kt
rename to wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index 14069c6..6e59369 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/dialog/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.dialog
+package androidx.wear.compose.material3
import android.os.Build
import androidx.compose.foundation.background
@@ -39,17 +39,6 @@
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.test.filters.SdkSuppress
-import androidx.wear.compose.material3.Button
-import androidx.wear.compose.material3.LocalContentColor
-import androidx.wear.compose.material3.LocalTextAlign
-import androidx.wear.compose.material3.LocalTextMaxLines
-import androidx.wear.compose.material3.LocalTextStyle
-import androidx.wear.compose.material3.MaterialTheme
-import androidx.wear.compose.material3.TEST_TAG
-import androidx.wear.compose.material3.TestImage
-import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.setContentWithTheme
-import androidx.wear.compose.material3.setContentWithThemeForSizeAssertions
import junit.framework.TestCase.assertEquals
import org.junit.Rule
import org.junit.Test
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt
new file mode 100644
index 0000000..73e047e
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.content.res.Configuration
+import android.os.Build
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.material3.ConfirmationDefaults.curvedText
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(TestParameterInjector::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+class ConfirmationScreenshotTest {
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @Test
+ fun confirmation_icon_linearText(@TestParameter screenSize: ScreenSize) {
+
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ Confirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ text = { Text("Your message has been sent") }
+ ) {
+ DefaultSmallIcon()
+ }
+ }
+ }
+
+ @Test
+ fun confirmation_icon_curvedText(@TestParameter screenSize: ScreenSize) {
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ Confirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ curvedText = curvedText("Confirmed")
+ ) {
+ DefaultIcon()
+ }
+ }
+ }
+
+ @Test
+ fun confirmation_icon_noText(@TestParameter screenSize: ScreenSize) {
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ Confirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ curvedText = null
+ ) {
+ DefaultIcon()
+ }
+ }
+ }
+
+ @Test
+ fun successConfirmation_icon_text(@TestParameter screenSize: ScreenSize) {
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ SuccessConfirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ curvedText = curvedText("Success")
+ )
+ }
+ }
+
+ @Test
+ fun successConfirmation_icon_noText(@TestParameter screenSize: ScreenSize) {
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ SuccessConfirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ curvedText = null
+ )
+ }
+ }
+
+ @Test
+ fun failureConfirmation_icon_text(@TestParameter screenSize: ScreenSize) {
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ FailureConfirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ curvedText = curvedText("Failure")
+ )
+ }
+ }
+
+ @Test
+ fun failureConfirmation_icon_noText(@TestParameter screenSize: ScreenSize) {
+ rule.verifyConfirmationScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ screenSize = screenSize
+ ) { modifier ->
+ FailureConfirmation(
+ show = true,
+ modifier = modifier,
+ onDismissRequest = {},
+ curvedText = null,
+ )
+ }
+ }
+
+ private fun ComposeContentTestRule.verifyConfirmationScreenshot(
+ testName: TestName,
+ screenshotRule: AndroidXScreenshotTestRule,
+ screenSize: ScreenSize,
+ content: @Composable (modifier: Modifier) -> Unit
+ ) {
+ setContentWithTheme {
+ val originalConfiguration = LocalConfiguration.current
+ val originalContext = LocalContext.current
+ val fixedScreenSizeConfiguration =
+ remember(originalConfiguration) {
+ Configuration(originalConfiguration).apply {
+ screenWidthDp = screenSize.size
+ screenHeightDp = screenSize.size
+ }
+ }
+ originalContext.resources.configuration.updateFrom(fixedScreenSizeConfiguration)
+
+ CompositionLocalProvider(
+ LocalContext provides originalContext,
+ LocalConfiguration provides fixedScreenSizeConfiguration,
+ ) {
+ content(Modifier.size(screenSize.size.dp).testTag(TEST_TAG))
+ }
+ }
+
+ onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, testName.goldenIdentifier())
+ }
+
+ @Composable
+ private fun DefaultIcon() {
+ Icon(
+ Icons.Filled.Add,
+ modifier = Modifier.size(ConfirmationDefaults.IconSize),
+ tint = MaterialTheme.colorScheme.primary,
+ contentDescription = null
+ )
+ }
+
+ @Composable
+ private fun DefaultSmallIcon() {
+ Icon(
+ Icons.Filled.Add,
+ modifier = Modifier.size(ConfirmationDefaults.SmallIconSize),
+ tint = MaterialTheme.colorScheme.primary,
+ contentDescription = null
+ )
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
new file mode 100644
index 0000000..31206a1
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
@@ -0,0 +1,605 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+
+class ConfirmationTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun confirmation_linearText_supports_testtag() {
+ rule.setContentWithTheme {
+ Confirmation(
+ show = true,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ text = {},
+ ) {}
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun confirmation_curvedText_supports_testtag() {
+ rule.setContentWithTheme {
+ Confirmation(
+ show = true,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ curvedText = {}
+ ) {}
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun successConfirmation_supports_testtag() {
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ show = true,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun failureConfirmation_supports_testtag() {
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ show = true,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun confirmation_linearText_supports_swipeToDismiss() {
+ rule.setContentWithTheme {
+ var showDialog by remember { mutableStateOf(true) }
+ Confirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ text = {},
+ onDismissRequest = { showDialog = false },
+ show = showDialog
+ ) {}
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun confirmation_curvedText_supports_swipeToDismiss() {
+ rule.setContentWithTheme {
+ var showDialog by remember { mutableStateOf(true) }
+ Confirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { showDialog = false },
+ show = showDialog,
+ curvedText = {}
+ ) {}
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun successConfirmation_supports_swipeToDismiss() {
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ var showDialog by remember { mutableStateOf(true) }
+ SuccessConfirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { showDialog = false },
+ show = showDialog,
+ )
+ }
+ // Advancing time so that animation will finish its motion.
+ rule.mainClock.advanceTimeBy(1000)
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+ rule.mainClock.advanceTimeBy(1000)
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun failureConfirmation_supports_swipeToDismiss() {
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ var showDialog by remember { mutableStateOf(true) }
+ FailureConfirmation(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { showDialog = false },
+ show = showDialog,
+ )
+ }
+ // Advancing time so that animation will finish its motion.
+ rule.mainClock.advanceTimeBy(1000)
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+ rule.mainClock.advanceTimeBy(1000)
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun hides_confirmation_linearText_when_show_false() {
+ rule.setContentWithTheme {
+ Confirmation(
+ show = false,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ text = {},
+ ) {}
+ }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun hides_confirmation_curvedText_when_show_false() {
+ rule.setContentWithTheme {
+ Confirmation(
+ show = false,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ curvedText = {}
+ ) {}
+ }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun hides_successConfirmation_when_show_false() {
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ show = false,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun hides_failureConfirmation_when_show_false() {
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ show = false,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun confirmation_displays_icon_with_linearText() {
+ rule.setContentWithTheme {
+ Confirmation(
+ text = { Text("Text", modifier = Modifier.testTag(TextTestTag)) },
+ onDismissRequest = {},
+ show = true
+ ) {
+ TestImage(IconTestTag)
+ }
+ }
+ rule.onNodeWithTag(IconTestTag).assertExists()
+ rule.onNodeWithTag(TextTestTag).assertExists()
+ }
+
+ @Test
+ fun confirmation_displays_icon_with_curvedText() {
+ rule.setContentWithTheme {
+ Confirmation(
+ onDismissRequest = {},
+ show = true,
+ curvedText = { curvedText(CurvedText) }
+ ) {
+ TestImage(IconTestTag)
+ }
+ }
+ rule.onNodeWithTag(IconTestTag).assertExists()
+ rule.onNodeWithContentDescription(CurvedText).assertExists()
+ }
+
+ @Test
+ fun successConfirmation_displays_icon_with_text() {
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ onDismissRequest = {},
+ show = true,
+ curvedText = ConfirmationDefaults.curvedText(CurvedText)
+ ) {
+ TestImage(IconTestTag)
+ }
+ }
+ rule.onNodeWithTag(IconTestTag).assertExists()
+ rule.onNodeWithContentDescription(CurvedText).assertExists()
+ }
+
+ @Test
+ fun failureConfirmation_displays_icon_with_text() {
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ onDismissRequest = {},
+ show = true,
+ curvedText = ConfirmationDefaults.curvedText(CurvedText)
+ ) {
+ TestImage(IconTestTag)
+ }
+ }
+ rule.onNodeWithTag(IconTestTag).assertExists()
+ rule.onNodeWithContentDescription(CurvedText).assertExists()
+ }
+
+ @Test
+ fun confirmation_linearText_dismissed_after_timeout() {
+ var dismissed = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ Confirmation(text = {}, onDismissRequest = { dismissed = true }, show = true) {}
+ }
+ // Timeout longer than default confirmation duration
+ rule.mainClock.advanceTimeBy(ConfirmationDefaults.ConfirmationDurationMillis + 1000)
+ assert(dismissed)
+ }
+
+ @Test
+ fun confirmation_curvedText_dismissed_after_timeout() {
+ var dismissed = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ Confirmation(onDismissRequest = { dismissed = true }, show = true, curvedText = {}) {}
+ }
+ // Timeout longer than default confirmation duration
+ rule.mainClock.advanceTimeBy(ConfirmationDefaults.ConfirmationDurationMillis + 1000)
+ assert(dismissed)
+ }
+
+ @Test
+ fun successConfirmation_dismissed_after_timeout() {
+ var dismissed = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ onDismissRequest = { dismissed = true },
+ show = true,
+ )
+ }
+ // Timeout longer than default confirmation duration
+ rule.mainClock.advanceTimeBy(ConfirmationDefaults.ConfirmationDurationMillis + 1000)
+ assert(dismissed)
+ }
+
+ @Test
+ fun failureConfirmation_dismissed_after_timeout() {
+ var dismissed = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ onDismissRequest = { dismissed = true },
+ show = true,
+ )
+ }
+ // Timeout longer than default confirmation duration
+ rule.mainClock.advanceTimeBy(ConfirmationDefaults.ConfirmationDurationMillis + 1000)
+ assert(dismissed)
+ }
+
+ @Test
+ fun confirmation_linearText_positioning() {
+ rule.setContentWithThemeForSizeAssertions(useUnmergedTree = true) {
+ Confirmation(
+ show = true,
+ text = {
+ Text(
+ "Title",
+ modifier = Modifier.testTag(TextTestTag),
+ textAlign = TextAlign.Center
+ )
+ },
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ }
+
+ // Calculating the center of the icon
+ val iconCenter =
+ rule.onNodeWithTag(IconTestTag).getUnclippedBoundsInRoot().run { (top + bottom) / 2 }
+ val textTop = rule.onNodeWithTag(TextTestTag).getUnclippedBoundsInRoot().top
+
+ // Stepping down half of the container height with vertical content padding
+ textTop.assertIsEqualTo(
+ iconCenter +
+ ConfirmationDefaults.ConfirmationIconContainerSmallSize / 2 +
+ ConfirmationDefaults.LinearContentSpacing
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun confirmation_linearText_correct_colors() {
+ var expectedIconColor: Color = Color.Unspecified
+ var expectedIconContainerColor: Color = Color.Unspecified
+ var expectedTextColor: Color = Color.Unspecified
+
+ rule.setContentWithTheme {
+ Confirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ text = { Text("Text") },
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ expectedIconColor = MaterialTheme.colorScheme.primary
+ expectedIconContainerColor = MaterialTheme.colorScheme.onPrimary
+ expectedTextColor = MaterialTheme.colorScheme.onBackground
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedIconColor)
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun confirmation_curvedText_correct_colors() {
+ var expectedIconColor: Color = Color.Unspecified
+ var expectedIconContainerColor: Color = Color.Unspecified
+ var expectedTextColor: Color = Color.Unspecified
+ rule.setContentWithTheme {
+ Confirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ curvedText = ConfirmationDefaults.curvedText(CurvedText)
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ expectedIconColor = MaterialTheme.colorScheme.primary
+ expectedIconContainerColor = MaterialTheme.colorScheme.onPrimary
+ expectedTextColor = MaterialTheme.colorScheme.onBackground
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedIconColor)
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun successConfirmation_correct_colors() {
+ var expectedIconColor: Color = Color.Unspecified
+ var expectedIconContainerColor: Color = Color.Unspecified
+ var expectedTextColor: Color = Color.Unspecified
+
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ )
+ expectedIconColor = MaterialTheme.colorScheme.primary
+ expectedIconContainerColor = MaterialTheme.colorScheme.onPrimary
+ expectedTextColor = MaterialTheme.colorScheme.onBackground
+ }
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedIconColor)
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun failureConfirmation_correct_colors() {
+ var expectedIconColor: Color = Color.Unspecified
+ var expectedIconContainerColor: Color = Color.Unspecified
+ var expectedTextColor: Color = Color.Unspecified
+ val backgroundColor = Color.Black
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG).background(backgroundColor),
+ show = true,
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ expectedIconColor = MaterialTheme.colorScheme.errorContainer
+ // As we have .8 alpha, we have to merge this color with background
+ expectedIconContainerColor =
+ MaterialTheme.colorScheme.onErrorContainer.copy(.8f).compositeOver(backgroundColor)
+ expectedTextColor = MaterialTheme.colorScheme.onBackground
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedIconColor)
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun confirmation_linearText_custom_colors() {
+ val customIconColor: Color = Color.Red
+ val customIconContainerColor: Color = Color.Green
+ val customTextColor: Color = Color.Blue
+
+ rule.setContentWithTheme {
+ Confirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ text = { Text("Text") },
+ colors =
+ ConfirmationDefaults.confirmationColors(
+ iconColor = customIconColor,
+ iconContainerColor = customIconContainerColor,
+ textColor = customTextColor
+ )
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun confirmation_curvedText_custom_colors() {
+ val customIconColor: Color = Color.Red
+ val customIconContainerColor: Color = Color.Green
+ val customTextColor: Color = Color.Blue
+
+ rule.setContentWithTheme {
+ Confirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ colors =
+ ConfirmationDefaults.confirmationColors(
+ iconColor = customIconColor,
+ iconContainerColor = customIconContainerColor,
+ textColor = customTextColor
+ ),
+ curvedText = ConfirmationDefaults.curvedText(CurvedText)
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun successConfirmation_curvedText_custom_colors() {
+ val customIconColor: Color = Color.Red
+ val customIconContainerColor: Color = Color.Green
+ val customTextColor: Color = Color.Blue
+
+ rule.setContentWithTheme {
+ SuccessConfirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ colors =
+ ConfirmationDefaults.successColors(
+ iconColor = customIconColor,
+ iconContainerColor = customIconContainerColor,
+ textColor = customTextColor
+ ),
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customTextColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun failureConfirmation_curvedText_custom_colors() {
+ val customIconColor: Color = Color.Red
+ val customIconContainerColor: Color = Color.Green
+ val customTextColor: Color = Color.Blue
+
+ rule.setContentWithTheme {
+ FailureConfirmation(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true,
+ colors =
+ ConfirmationDefaults.failureColors(
+ iconColor = customIconColor,
+ iconContainerColor = customIconContainerColor,
+ textColor = customTextColor
+ ),
+ ) {
+ TestIcon(Modifier.testTag(IconTestTag))
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconContainerColor)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customTextColor)
+ }
+}
+
+private const val IconTestTag = "icon"
+private const val TextTestTag = "text"
+private const val CurvedText = "CurvedText"
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt
new file mode 100644
index 0000000..7f334a8
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.content.res.Configuration
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.material3.internal.Strings
+import java.time.LocalDate
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class DatePickerScreenshotTest {
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @Test
+ fun datePicker_dayMonthYear_ltr() {
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Ltr,
+ content = { DatePickerDayMonthYear() }
+ )
+ }
+
+ @Test
+ fun datePicker_dayMonthYear_rtl() {
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Rtl,
+ content = { DatePickerDayMonthYear() }
+ )
+ }
+
+ @Test
+ fun datePicker_dayMonthYear_largeScreen() {
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ content = { DatePickerDayMonthYear() }
+ )
+ }
+
+ @Test
+ fun datePicker_dayMonthYear_monthFocused_ltr() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Ltr,
+ action = { rule.nextButton().performClick() },
+ content = { DatePickerDayMonthYear() }
+ )
+
+ @Test
+ fun datePicker_dayMonthYear_monthFocused_rtl() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Rtl,
+ action = { rule.nextButton().performClick() },
+ content = { DatePickerDayMonthYear() }
+ )
+
+ @Test
+ fun datePicker_dayMonthYear_monthFocused_largeScreen() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ action = { rule.nextButton().performClick() },
+ content = { DatePickerDayMonthYear() }
+ )
+
+ @Test
+ fun datePicker_dayMonthYear_yearFocused_ltr() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Ltr,
+ action = {
+ rule.nextButton().performClick()
+ rule.nextButton().performClick()
+ },
+ content = { DatePickerDayMonthYear() }
+ )
+
+ @Test
+ fun datePicker_dayMonthYear_yearFocused_rtl() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Rtl,
+ action = {
+ rule.nextButton().performClick()
+ rule.nextButton().performClick()
+ },
+ content = { DatePickerDayMonthYear() }
+ )
+
+ @Test
+ fun datePicker_dayMonthYear_yearFocused_largeScreen() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ action = {
+ rule.nextButton().performClick()
+ rule.nextButton().performClick()
+ },
+ content = { DatePickerDayMonthYear() }
+ )
+
+ @Test
+ fun datePicker_monthDayYear_ltr() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Ltr,
+ content = { DatePickerMonthDayYear() }
+ )
+
+ @Test
+ fun datePicker_monthDayYear_rtl() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Rtl,
+ content = { DatePickerMonthDayYear() }
+ )
+
+ @Test
+ fun datePicker_monthDayYear_largeScreen() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ content = { DatePickerMonthDayYear() }
+ )
+
+ @Test
+ fun datePicker_yearMonthDay_ltr() {
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Ltr,
+ content = { DatePickerYearMonthDay() }
+ )
+ }
+
+ @Test
+ fun datePicker_yearMonthDay_rtl() {
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ layoutDirection = LayoutDirection.Rtl,
+ content = { DatePickerYearMonthDay() }
+ )
+ }
+
+ @Test
+ fun datePicker_yearMonthDay_largeScreen() {
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ isLargeScreen = true,
+ content = { DatePickerYearMonthDay() }
+ )
+ }
+
+ @Test
+ fun datePicker_yearMonthDay_year_does_not_repeat() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ DatePicker(
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ datePickerType = DatePickerType.YearMonthDay,
+ initialDate =
+ LocalDate.of(/* year= */ 2024, /* month= */ 9, /* dayOfMonth= */ 15),
+ minDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15),
+ maxDate = LocalDate.of(/* year= */ 2024, /* month= */ 10, /* dayOfMonth= */ 15),
+ )
+ }
+ )
+
+ @Test
+ fun datePicker_monthYearDay_month_does_not_repeat() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ DatePicker(
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ datePickerType = DatePickerType.MonthDayYear,
+ initialDate =
+ LocalDate.of(/* year= */ 2024, /* month= */ 1, /* dayOfMonth= */ 15),
+ minDate = LocalDate.of(/* year= */ 2024, /* month= */ 1, /* dayOfMonth= */ 1),
+ maxDate = LocalDate.of(/* year= */ 2024, /* month= */ 2, /* dayOfMonth= */ 15),
+ )
+ }
+ )
+
+ @Test
+ fun datePicker_dayMonthYear_day_does_not_repeat() =
+ rule.verifyDatePickerScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ DatePicker(
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ datePickerType = DatePickerType.DayMonthYear,
+ initialDate =
+ LocalDate.of(/* year= */ 2024, /* month= */ 2, /* dayOfMonth= */ 1),
+ maxDate = LocalDate.of(/* year= */ 2024, /* month= */ 2, /* dayOfMonth= */ 1),
+ )
+ }
+ )
+
+ @Composable
+ private fun DatePickerDayMonthYear() {
+ DatePicker(
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ datePickerType = DatePickerType.DayMonthYear,
+ initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ )
+ }
+
+ @Composable
+ private fun DatePickerMonthDayYear() {
+ DatePicker(
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ datePickerType = DatePickerType.MonthDayYear,
+ initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ )
+ }
+
+ @Composable
+ private fun DatePickerYearMonthDay() {
+ DatePicker(
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ datePickerType = DatePickerType.YearMonthDay,
+ initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ )
+ }
+
+ private fun SemanticsNodeInteractionsProvider.nextButton(): SemanticsNodeInteraction =
+ onAllNodesWithContentDescription(
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .resources
+ .getString(Strings.PickerNextButtonContentDescription.value)
+ )
+ .onFirst()
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun ComposeContentTestRule.verifyDatePickerScreenshot(
+ methodName: String,
+ screenshotRule: AndroidXScreenshotTestRule,
+ testTag: String = TEST_TAG,
+ layoutDirection: LayoutDirection = LayoutDirection.Ltr,
+ isLargeScreen: Boolean = false,
+ action: (() -> Unit)? = null,
+ content: @Composable () -> Unit
+ ) {
+ val screenSizeDp = if (isLargeScreen) SCREENSHOT_SIZE_LARGE else SCREENSHOT_SIZE
+ setContentWithTheme {
+ val originalConfiguration = LocalConfiguration.current
+ val fixedScreenSizeConfiguration =
+ remember(originalConfiguration) {
+ Configuration(originalConfiguration).apply {
+ screenWidthDp = screenSizeDp
+ screenHeightDp = screenSizeDp
+ }
+ }
+ CompositionLocalProvider(
+ LocalLayoutDirection provides layoutDirection,
+ LocalConfiguration provides fixedScreenSizeConfiguration
+ ) {
+ Box(
+ modifier =
+ Modifier.size(screenSizeDp.dp)
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ content()
+ }
+ }
+ }
+ action?.let { it() }
+
+ onNodeWithTag(testTag).captureToImage().assertAgainstGolden(screenshotRule, methodName)
+ }
+}
+
+private const val SCREENSHOT_SIZE = 192
+private const val SCREENSHOT_SIZE_LARGE = 228
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
new file mode 100644
index 0000000..c0a2138
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
@@ -0,0 +1,517 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.content.res.Resources
+import android.os.Build
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollToIndex
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.wear.compose.material3.internal.Strings
+import androidx.wear.compose.material3.samples.DatePickerSample
+import androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class DatePickerTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun supports_testtag() {
+ rule.setContentWithTheme {
+ DatePicker(
+ initialDate = LocalDate.now(),
+ onDatePicked = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun samples_build() {
+ rule.setContentWithTheme {
+ DatePickerSample()
+ DatePickerYearMonthDaySample()
+ }
+ }
+
+ @Test
+ fun dayMonthYear_initial_state() {
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = {},
+ initialDate = initialDate,
+ datePickerType = DatePickerType.DayMonthYear
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.dayOfMonth,
+ selectionMode = SelectionMode.Day
+ )
+ .assertIsFocused()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .assertIsDisplayed()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .assertIsDisplayed()
+ rule.nextButton().assertIsDisplayed()
+ rule.confirmButton().assertDoesNotExist()
+ }
+
+ @Test
+ fun monthDayYear_initial_state() {
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 2, /* dayOfMonth= */ 29)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = {},
+ initialDate = initialDate,
+ datePickerType = DatePickerType.MonthDayYear
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .assertIsFocused()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.dayOfMonth,
+ selectionMode = SelectionMode.Day
+ )
+ .assertIsDisplayed()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .assertIsDisplayed()
+ rule.nextButton().assertIsDisplayed()
+ rule.confirmButton().assertDoesNotExist()
+ }
+
+ @Test
+ fun yearMonthDay_initial_state() {
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 12, /* dayOfMonth= */ 31)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = {},
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .assertIsFocused()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .assertIsDisplayed()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.dayOfMonth,
+ selectionMode = SelectionMode.Day
+ )
+ .assertIsDisplayed()
+ rule.nextButton().assertIsDisplayed()
+ rule.confirmButton().assertDoesNotExist()
+ }
+
+ @Test
+ fun dayMonthYear_switch_to_month() {
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ rule.setContentWithTheme {
+ DatePicker(
+ initialDate = initialDate,
+ onDatePicked = {},
+ datePickerType = DatePickerType.DayMonthYear
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .performClick()
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .assertIsFocused()
+ }
+
+ @Test
+ fun dayMonthYear_switch_to_year() {
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ rule.setContentWithTheme {
+ DatePicker(
+ initialDate = initialDate,
+ onDatePicked = {},
+ datePickerType = DatePickerType.DayMonthYear
+ )
+ }
+
+ rule.nextButton().performClick()
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performClick()
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .assertIsFocused()
+ }
+
+ @Test
+ fun date_picked() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ val expectedDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 5)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.DayMonthYear
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.dayOfMonth,
+ selectionMode = SelectionMode.Day
+ )
+ .performScrollToIndex(expectedDate.dayOfMonth - 1)
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .performScrollToIndex(expectedDate.monthValue - 1)
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(expectedDate.year - 1900)
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun date_picked_between_fromDate_and_toDate() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ val expectedDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 5)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.DayMonthYear,
+ minDate = LocalDate.of(/* year= */ 2024, /* month= */ 1, /* dayOfMonth= */ 1),
+ maxDate = LocalDate.of(/* year= */ 2025, /* month= */ 12, /* dayOfMonth= */ 6)
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.dayOfMonth,
+ selectionMode = SelectionMode.Day
+ )
+ .performScrollToIndex(expectedDate.dayOfMonth - 1)
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .performScrollToIndex(expectedDate.monthValue - 1)
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(expectedDate.year - 1900)
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun auto_scroll_day_to_fromDate() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 9, /* dayOfMonth= */ 6)
+ val expectedDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay,
+ minDate = LocalDate.of(/* year= */ 2024, /* month= */ 8, /* dayOfMonth= */ 15),
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.monthValue,
+ selectionMode = SelectionMode.Month
+ )
+ .performScrollToIndex(0)
+ rule.nextButton().performClick()
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun auto_scroll_month_to_fromDate_month() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 7, /* dayOfMonth= */ 15)
+ val expectedDate = LocalDate.of(/* year= */ 2023, /* month= */ 8, /* dayOfMonth= */ 15)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay,
+ minDate = LocalDate.of(/* year= */ 2023, /* month= */ 8, /* dayOfMonth= */ 5),
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(0)
+ rule.nextButton().performClick()
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun auto_scroll_month_and_day_to_fromDate() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 9, /* dayOfMonth= */ 6)
+ val expectedDate = LocalDate.of(/* year= */ 2023, /* month= */ 10, /* dayOfMonth= */ 15)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay,
+ minDate = LocalDate.of(/* year= */ 2023, /* month= */ 10, /* dayOfMonth= */ 15),
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(0)
+ rule.nextButton().performClick()
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun auto_scroll_month_to_toDate_month() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 9, /* dayOfMonth= */ 2)
+ val expectedDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 2)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay,
+ maxDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 4)
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(2025 - 1900)
+ rule.nextButton().performClick()
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun auto_scroll_day_to_toDate() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 2, /* dayOfMonth= */ 12)
+ val expectedDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 4)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay,
+ maxDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 4)
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(2025 - 1900)
+ rule.nextButton().performClick()
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ @Test
+ fun auto_scroll_month_and_day_to_toDate() {
+ lateinit var pickedDate: LocalDate
+ val initialDate = LocalDate.of(/* year= */ 2024, /* month= */ 9, /* dayOfMonth= */ 10)
+ val expectedDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 4)
+ rule.setContentWithTheme {
+ DatePicker(
+ onDatePicked = { pickedDate = it },
+ initialDate = initialDate,
+ datePickerType = DatePickerType.YearMonthDay,
+ maxDate = LocalDate.of(/* year= */ 2025, /* month= */ 2, /* dayOfMonth= */ 4)
+ )
+ }
+
+ rule
+ .onNodeWithDateValue(
+ selectedValue = initialDate.year,
+ selectionMode = SelectionMode.Year
+ )
+ .performScrollToIndex(2025 - 1900)
+ rule.nextButton().performClick()
+ rule.confirmButton().performClick()
+ rule.waitForIdle()
+
+ assertThat(pickedDate).isEqualTo(expectedDate)
+ }
+
+ private fun SemanticsNodeInteractionsProvider.onNodeWithDateValue(
+ selectedValue: Int,
+ selectionMode: SelectionMode,
+ ): SemanticsNodeInteraction =
+ onAllNodesWithContentDescription(
+ if (selectionMode == SelectionMode.Month) {
+ monthNames[(selectedValue - 1) % 12]
+ } else {
+ contentDescriptionForValue(
+ InstrumentationRegistry.getInstrumentation().context.resources,
+ selectedValue,
+ selectionMode.contentDescriptionResource
+ )
+ }
+ )
+ .onFirst()
+
+ private fun SemanticsNodeInteractionsProvider.confirmButton(): SemanticsNodeInteraction =
+ onAllNodesWithContentDescription(
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .resources
+ .getString(Strings.PickerConfirmButtonContentDescription.value)
+ )
+ .onFirst()
+
+ private fun SemanticsNodeInteractionsProvider.nextButton(): SemanticsNodeInteraction =
+ onAllNodesWithContentDescription(
+ InstrumentationRegistry.getInstrumentation()
+ .context
+ .resources
+ .getString(Strings.PickerNextButtonContentDescription.value)
+ )
+ .onFirst()
+
+ private fun contentDescriptionForValue(
+ resources: Resources,
+ selectedValue: Int,
+ contentDescriptionResource: Strings,
+ ): String = "${resources.getString(contentDescriptionResource.value)}, $selectedValue"
+
+ private enum class SelectionMode(val contentDescriptionResource: Strings) {
+ Day(Strings.DatePickerDay),
+ Month(Strings.DatePickerMonth),
+ Year(Strings.DatePickerYear),
+ }
+
+ private val monthNames: List<String>
+ get() {
+ val monthFormatter = DateTimeFormatter.ofPattern("MMMM")
+ val months = 1..12
+ return months.map { LocalDate.of(2022, it, 1).format(monthFormatter) }
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
index 188a8f8..9c0a3f0 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
@@ -618,7 +618,7 @@
private fun ComposeContentTestRule.verifyIconToggleButtonColors(
status: Status,
checked: Boolean,
- colors: @Composable () -> ToggleButtonColors,
+ colors: @Composable () -> IconToggleButtonColors,
containerColor: @Composable () -> Color,
contentColor: @Composable () -> Color,
) {
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/LevelIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/LevelIndicatorScreenshotTest.kt
new file mode 100644
index 0000000..6c481ea
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/LevelIndicatorScreenshotTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.content.res.Configuration
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(TestParameterInjector::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class LevelIndicatorScreenshotTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @Test
+ fun level_indicator_0_percent(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 0f, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_25_percent(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 25f, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_75_percent(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 75f, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_100_percent(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 100f, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_25_percent_reversed(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 25f, reverseDirection = true, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_disabled(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 25f, enabled = false, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_rtl(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(value = 25f, ltr = false, shape = shape, testName = testName)
+
+ @Test
+ fun level_indicator_double_stroke_width(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(
+ value = 25f,
+ strokeWidth = LevelIndicatorDefaults.StrokeWidth * 2,
+ shape = shape,
+ testName = testName
+ )
+
+ @Test
+ fun level_indicator_half_sweep_angle(@TestParameter shape: ScreenShape) =
+ verifyScreenshot(
+ value = 25f,
+ sweepAngle = LevelIndicatorDefaults.SweepAngle / 2f,
+ shape = shape,
+ testName = testName
+ )
+
+ private fun verifyScreenshot(
+ value: Float,
+ testName: TestName,
+ shape: ScreenShape,
+ ltr: Boolean = true,
+ enabled: Boolean = true,
+ strokeWidth: Dp = LevelIndicatorDefaults.StrokeWidth,
+ sweepAngle: Float = LevelIndicatorDefaults.SweepAngle,
+ reverseDirection: Boolean = false,
+ ) {
+ val valueRange = 0f..100f
+ val screenSizeDp = SCREEN_SIZE_SMALL
+
+ rule.setContentWithTheme {
+ val actualLayoutDirection = if (ltr) LayoutDirection.Ltr else LayoutDirection.Rtl
+
+ val currentConfig = LocalConfiguration.current
+ val updatedConfig =
+ Configuration().apply {
+ setTo(currentConfig)
+ screenLayout =
+ if (shape == ScreenShape.ROUND_DEVICE) Configuration.SCREENLAYOUT_ROUND_YES
+ else Configuration.SCREENLAYOUT_ROUND_NO
+ screenWidthDp = screenSizeDp
+ screenHeightDp = screenSizeDp
+ }
+ CompositionLocalProvider(
+ LocalLayoutDirection provides actualLayoutDirection,
+ LocalConfiguration provides updatedConfig
+ ) {
+ Box(
+ modifier =
+ Modifier.testTag(TEST_TAG)
+ .size(screenSizeDp.dp)
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ LevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.align(Alignment.CenterStart),
+ enabled = enabled,
+ strokeWidth = strokeWidth,
+ sweepAngle = sweepAngle,
+ reverseDirection = reverseDirection,
+ )
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, testName.goldenIdentifier())
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/LevelIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/LevelIndicatorTest.kt
new file mode 100644
index 0000000..4645acf
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/LevelIndicatorTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LevelIndicatorTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun supports_test_tag() {
+ rule.setContentWithTheme { LevelIndicator() }
+
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun gives_indicator_correct_color() {
+ var expectedColor: Color = Color.Unspecified
+ rule.setContentWithTheme {
+ // Show level = 100 so that the indicator color is shown
+ LevelIndicator(value = 100f)
+ expectedColor = MaterialTheme.colorScheme.secondaryDim
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedColor)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun gives_track_correct_color() {
+ var expectedColor: Color = Color.Unspecified
+ rule.setContentWithTheme {
+ // Show level = 0 so that the track color is shown
+ LevelIndicator(value = 0f)
+ expectedColor = MaterialTheme.colorScheme.surfaceContainer
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedColor)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun gives_indicator_custom_color() {
+ val customColor = Color.Red
+ rule.setContentWithTheme {
+ // Show level = 100 so that the indicator color is shown
+ LevelIndicator(
+ value = 100f,
+ colors = LevelIndicatorDefaults.colors(indicatorColor = customColor)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customColor)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun gives_track_custom_color() {
+ val customColor = Color.Red
+ rule.setContentWithTheme {
+ // Show level = 0 so that the track color is shown
+ LevelIndicator(
+ value = 0f,
+ colors = LevelIndicatorDefaults.colors(trackColor = customColor)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customColor)
+ }
+
+ @Composable
+ private fun LevelIndicator(
+ value: Float = 50f,
+ colors: LevelIndicatorColors = LevelIndicatorDefaults.colors(),
+ enabled: Boolean = true,
+ ) {
+ val valueRange = 0f..100f
+
+ Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
+ LevelIndicator(
+ value = { value },
+ valueRange = valueRange,
+ modifier = Modifier.testTag(TEST_TAG).align(Alignment.CenterStart),
+ colors = colors,
+ enabled = enabled
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
index 2ea8c78..c49f71f 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
@@ -79,6 +79,22 @@
val SCREEN_SIZE_SMALL = 192
val SCREEN_SIZE_LARGE = 228
+enum class ScreenSize(val size: Int) {
+ SMALL(SCREEN_SIZE_SMALL),
+ LARGE(SCREEN_SIZE_LARGE)
+}
+
+enum class ScreenShape(val isRound: Boolean) {
+ ROUND_DEVICE(true),
+ SQUARE_DEVICE(false)
+}
+
+/**
+ * Valid characters for golden identifiers are [A-Za-z0-9_-] TestParameterInjector adds '[' +
+ * parameter_values + ']' to the test name.
+ */
+fun TestName.goldenIdentifier(): String = methodName.replace("[", "_").replace("]", "")
+
internal const val TEST_TAG = "test-item"
@Composable
@@ -223,17 +239,6 @@
}
}
-enum class ScreenSize(val size: Int) {
- SMALL(SCREEN_SIZE_SMALL),
- LARGE(SCREEN_SIZE_LARGE)
-}
-
-/**
- * Valid characters for golden identifiers are [A-Za-z0-9_-] TestParameterInjector adds '[' +
- * parameter_values + ']' to the test name.
- */
-fun TestName.methodNameWithValidCharacters(): String = methodName.replace("[", "_").replace("]", "")
-
/**
* Asserts that the layout of this node has height equal to [expectedHeight].
*
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt
new file mode 100644
index 0000000..74568a6
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.content.res.Configuration
+import android.os.Build
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(TestParameterInjector::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+class OpenOnPhoneDialogScreenshotTest {
+ @get:Rule val rule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule val testName = TestName()
+
+ @Test
+ fun openOnPhone_50_percent_progress(@TestParameter screenSize: ScreenSize) {
+ rule.verifyOpenOnPhoneScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ advanceTimeBy = OpenOnPhoneDialogDefaults.DurationMillis / 2,
+ screenSize = screenSize
+ )
+ }
+
+ @Test
+ fun openOnPhone_100_percent_progress(@TestParameter screenSize: ScreenSize) {
+ rule.verifyOpenOnPhoneScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ advanceTimeBy = OpenOnPhoneDialogDefaults.DurationMillis,
+ screenSize = screenSize
+ )
+ }
+
+ private fun ComposeContentTestRule.verifyOpenOnPhoneScreenshot(
+ testName: TestName,
+ screenshotRule: AndroidXScreenshotTestRule,
+ screenSize: ScreenSize,
+ advanceTimeBy: Long,
+ ) {
+ rule.mainClock.autoAdvance = false
+ setContentWithTheme {
+ val originalConfiguration = LocalConfiguration.current
+ val originalContext = LocalContext.current
+ val fixedScreenSizeConfiguration =
+ remember(originalConfiguration) {
+ Configuration(originalConfiguration).apply {
+ screenWidthDp = screenSize.size
+ screenHeightDp = screenSize.size
+ }
+ }
+ originalContext.resources.configuration.updateFrom(fixedScreenSizeConfiguration)
+
+ CompositionLocalProvider(
+ LocalContext provides originalContext,
+ LocalConfiguration provides fixedScreenSizeConfiguration,
+ ) {
+ OpenOnPhoneDialog(
+ show = true,
+ modifier = Modifier.size(screenSize.size.dp).testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ }
+
+ rule.mainClock.advanceTimeBy(advanceTimeBy)
+ onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, testName.goldenIdentifier())
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
new file mode 100644
index 0000000..c5de9c1
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeRight
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+
+class OpenOnPhoneDialogTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun openOnPhone_supports_testtag() {
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(
+ show = true,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun openOnPhone_supports_swipeToDismiss() {
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ var showDialog by remember { mutableStateOf(true) }
+ OpenOnPhoneDialog(
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = { showDialog = false },
+ show = showDialog
+ )
+ }
+ rule.mainClock.advanceTimeBy(OpenOnPhoneDialogDefaults.DurationMillis / 2)
+ rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
+ // Advancing time so that the dialog is dismissed
+ rule.mainClock.advanceTimeBy(300)
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun hides_openOnPhone_when_show_false() {
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(
+ show = false,
+ modifier = Modifier.testTag(TEST_TAG),
+ onDismissRequest = {},
+ )
+ }
+ rule.onNodeWithTag(TEST_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun openOnPhone_displays_icon() {
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(onDismissRequest = {}, show = true) { TestImage(IconTestTag) }
+ }
+ rule.onNodeWithTag(IconTestTag).assertExists()
+ }
+
+ @Test
+ fun openOnPhone_dismissed_after_timeout() {
+ var dismissed = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(onDismissRequest = { dismissed = true }, show = true) {}
+ }
+ // Timeout longer than default confirmation duration
+ rule.mainClock.advanceTimeBy(OpenOnPhoneDialogDefaults.DurationMillis + 1000)
+ assert(dismissed)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun openOnPhone_correct_colors() {
+ rule.mainClock.autoAdvance = false
+ var expectedIconColor: Color = Color.Unspecified
+ var expectedIconContainerColor: Color = Color.Unspecified
+ var expectedProgressIndicatorColor: Color = Color.Unspecified
+ var expectedProgressTrackColor: Color = Color.Unspecified
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ show = true
+ )
+ expectedIconColor = MaterialTheme.colorScheme.primary
+ expectedIconContainerColor = MaterialTheme.colorScheme.primaryContainer
+ expectedProgressIndicatorColor = MaterialTheme.colorScheme.primary
+ expectedProgressTrackColor = MaterialTheme.colorScheme.onPrimary
+ }
+ // Advance time by half of the default confirmation duration, so that the track and
+ // indicator are shown
+ rule.mainClock.advanceTimeBy(OpenOnPhoneDialogDefaults.DurationMillis / 2)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(expectedIconColor)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedIconContainerColor)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedProgressIndicatorColor)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedProgressTrackColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+ @Test
+ fun openOnPhone_custom_colors() {
+ rule.mainClock.autoAdvance = false
+ val customIconColor: Color = Color.Red
+ val customIconContainerColor: Color = Color.Green
+ val customProgressIndicatorColor: Color = Color.Blue
+ val customProgressTrackColor: Color = Color.Magenta
+ rule.setContentWithTheme {
+ OpenOnPhoneDialog(
+ onDismissRequest = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ colors =
+ OpenOnPhoneDialogDefaults.colors(
+ iconColor = customIconColor,
+ iconContainerColor = customIconContainerColor,
+ progressIndicatorColor = customProgressIndicatorColor,
+ progressTrackColor = customProgressTrackColor
+ ),
+ show = true
+ )
+ }
+ // Advance time by half of the default confirmation duration, so that the track and
+ // indicator are shown
+ rule.mainClock.advanceTimeBy(OpenOnPhoneDialogDefaults.DurationMillis / 2)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIconContainerColor)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(customProgressIndicatorColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customProgressTrackColor)
+ }
+}
+
+private const val IconTestTag = "icon"
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorScreenshotTest.kt
index 74b8597..e3a05b37 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorScreenshotTest.kt
@@ -146,9 +146,9 @@
LocalLayoutDirection provides actualLayoutDirection,
LocalConfiguration provides updatedConfig
) {
- ScrollIndicatorImpl(
+ IndicatorImpl(
state =
- object : ScrollIndicatorState {
+ object : IndicatorState {
override val positionFraction: Float
get() = position
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorTest.kt
index f521fef..6d17642 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScrollIndicatorTest.kt
@@ -52,7 +52,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-public class ScrollIndicatorTest {
+class ScrollIndicatorTest {
@get:Rule val rule = createComposeRule()
private var itemSizePx: Int = 50
@@ -296,10 +296,10 @@
itemsCount: Int = 0,
) {
lateinit var state: ScalingLazyListState
- lateinit var scrollIndicatorState: ScrollIndicatorState
+ lateinit var indicatorState: IndicatorState
rule.setContent {
state = rememberScalingLazyListState()
- scrollIndicatorState = ScalingLazyColumnStateAdapter(state)
+ indicatorState = ScalingLazyColumnStateAdapter(state)
ScalingLazyColumn(
state = state,
verticalArrangement = verticalArrangement,
@@ -322,12 +322,10 @@
}
rule.runOnIdle {
- Truth.assertThat(scrollIndicatorState.positionFraction)
+ Truth.assertThat(indicatorState.positionFraction)
.isWithin(0.05f)
.of(expectedIndicatorPosition)
- Truth.assertThat(scrollIndicatorState.sizeFraction)
- .isWithin(0.05f)
- .of(expectedIndicatorSize)
+ Truth.assertThat(indicatorState.sizeFraction).isWithin(0.05f).of(expectedIndicatorSize)
}
}
@@ -352,10 +350,10 @@
itemsCount: Int = 0,
) {
lateinit var state: LazyListState
- lateinit var scrollIndicatorState: ScrollIndicatorState
+ lateinit var indicatorState: IndicatorState
rule.setContent {
state = rememberLazyListState()
- scrollIndicatorState = LazyColumnStateAdapter(state)
+ indicatorState = LazyColumnStateAdapter(state)
LazyColumn(
state = state,
verticalArrangement = verticalArrangement,
@@ -377,12 +375,10 @@
}
rule.runOnIdle {
- Truth.assertThat(scrollIndicatorState.positionFraction)
+ Truth.assertThat(indicatorState.positionFraction)
.isWithin(0.05f)
.of(expectedIndicatorPosition)
- Truth.assertThat(scrollIndicatorState.sizeFraction)
- .isWithin(0.05f)
- .of(expectedIndicatorSize)
+ Truth.assertThat(indicatorState.sizeFraction).isWithin(0.05f).of(expectedIndicatorSize)
}
}
@@ -407,11 +403,11 @@
itemsCount: Int = 0,
) {
lateinit var state: ScrollState
- lateinit var scrollIndicatorState: ScrollIndicatorState
+ lateinit var indicatorState: IndicatorState
var viewPortSize = IntSize.Zero
rule.setContent {
state = rememberScrollState()
- scrollIndicatorState = ScrollStateAdapter(state) { viewPortSize }
+ indicatorState = ScrollStateAdapter(state) { viewPortSize }
Box(
modifier =
Modifier.onSizeChanged { viewPortSize = it }
@@ -437,12 +433,10 @@
}
}
rule.runOnIdle {
- Truth.assertThat(scrollIndicatorState.positionFraction)
+ Truth.assertThat(indicatorState.positionFraction)
.isWithin(0.05f)
.of(expectedIndicatorPosition)
- Truth.assertThat(scrollIndicatorState.sizeFraction)
- .isWithin(0.05f)
- .of(expectedIndicatorSize)
+ Truth.assertThat(indicatorState.sizeFraction).isWithin(0.05f).of(expectedIndicatorSize)
}
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt
index 3b9584c..166a54c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt
@@ -611,7 +611,7 @@
private fun ComposeContentTestRule.verifyTextToggleButtonColors(
status: Status,
checked: Boolean,
- colors: @Composable () -> ToggleButtonColors,
+ colors: @Composable () -> TextToggleButtonColors,
containerColor: @Composable () -> Color,
contentColor: @Composable () -> Color,
) {
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
index 352df46..5c81306 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimeTextScreenshotTest.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.material3
import android.os.Build
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
@@ -61,7 +62,7 @@
@Test
fun time_text_with_clock_only_on_round_device() = verifyScreenshot {
TimeText(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
time()
@@ -72,7 +73,7 @@
fun time_text_with_clock_only_on_non_round_device() =
verifyScreenshot(false) {
TimeText(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
time()
@@ -82,7 +83,7 @@
@Test
fun time_text_with_status_on_round_device() = verifyScreenshot {
TimeText(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text("ETA 12:48")
@@ -95,7 +96,7 @@
fun time_text_with_status_on_non_round_device() =
verifyScreenshot(false) {
TimeText(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text("ETA 12:48")
@@ -107,7 +108,7 @@
@Test
fun time_text_with_icon_on_round_device() = verifyScreenshot {
TimeText(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
time()
@@ -126,7 +127,7 @@
fun time_text_with_icon_on_non_round_device() =
verifyScreenshot(false) {
TimeText(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
time()
@@ -149,7 +150,7 @@
TimeText(
contentColor = Color.Green,
timeTextStyle = timeTextStyle,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text("ETA", customStyle)
@@ -166,7 +167,7 @@
TimeText(
contentColor = Color.Green,
timeTextStyle = timeTextStyle,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text("Long status that should be ellipsized.")
@@ -184,7 +185,7 @@
TimeText(
contentColor = Color.Green,
timeTextStyle = timeTextStyle,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text("ETA", customStyle)
@@ -205,7 +206,7 @@
contentColor = Color.Green,
timeTextStyle = timeTextStyle,
maxSweepAngle = 180f,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text(
@@ -226,7 +227,7 @@
TimeText(
contentColor = Color.Green,
timeTextStyle = timeTextStyle,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text(
@@ -249,7 +250,7 @@
contentColor = Color.Green,
timeTextStyle = timeTextStyle,
maxSweepAngle = 90f,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
) {
text(
@@ -292,7 +293,7 @@
TimeText(
contentColor = Color.Green,
maxSweepAngle = 180f,
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.testTag(TEST_TAG).background(Color.DarkGray),
timeSource = MockTimeSource,
content = content
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/dialog/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
similarity index 92%
rename from wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/dialog/AlertDialog.kt
rename to wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index 2b34a71..f0cc000 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/dialog/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.dialog
+package androidx.wear.compose.material3
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -46,25 +46,12 @@
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.material3.Button
-import androidx.wear.compose.material3.ButtonDefaults
-import androidx.wear.compose.material3.EdgeButton
-import androidx.wear.compose.material3.FilledIconButton
-import androidx.wear.compose.material3.FilledTonalIconButton
-import androidx.wear.compose.material3.Icon
-import androidx.wear.compose.material3.LocalContentColor
-import androidx.wear.compose.material3.LocalTextAlign
-import androidx.wear.compose.material3.LocalTextMaxLines
-import androidx.wear.compose.material3.LocalTextStyle
-import androidx.wear.compose.material3.MaterialTheme
-import androidx.wear.compose.material3.PaddingDefaults
-import androidx.wear.compose.material3.ScreenScaffold
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults.bottomSpacing
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults.contentTopSpacing
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults.iconBottomSpacing
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults.textMessageTopSpacing
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults.textPaddingFraction
-import androidx.wear.compose.material3.dialog.AlertDialogDefaults.titlePaddingFraction
+import androidx.wear.compose.material3.AlertDialogDefaults.bottomSpacing
+import androidx.wear.compose.material3.AlertDialogDefaults.contentTopSpacing
+import androidx.wear.compose.material3.AlertDialogDefaults.iconBottomSpacing
+import androidx.wear.compose.material3.AlertDialogDefaults.textMessageTopSpacing
+import androidx.wear.compose.material3.AlertDialogDefaults.textPaddingFraction
+import androidx.wear.compose.material3.AlertDialogDefaults.titlePaddingFraction
import androidx.wear.compose.materialcore.isSmallScreen
import androidx.wear.compose.materialcore.screenWidthDp
@@ -79,7 +66,7 @@
*
* Example of an [AlertDialog] with an icon, title and two buttons to confirm and dismiss:
*
- * @sample androidx.wear.compose.material3.samples.dialog.AlertDialogWithConfirmAndDismissSample
+ * @sample androidx.wear.compose.material3.samples.AlertDialogWithConfirmAndDismissSample
* @param show A boolean indicating whether the dialog should be displayed.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping
* right (typically also called by the [dismissButton]).
@@ -146,11 +133,11 @@
*
* Example of an [AlertDialog] with an icon, title, text and bottom [EdgeButton]:
*
- * @sample androidx.wear.compose.material3.samples.dialog.AlertDialogWithBottomButtonSample
+ * @sample androidx.wear.compose.material3.samples.AlertDialogWithBottomButtonSample
*
* Example of an [AlertDialog] with content groups and a bottom [EdgeButton]:
*
- * @sample androidx.wear.compose.material3.samples.dialog.AlertDialogWithContentGroupsSample
+ * @sample androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
* @param show A boolean indicating whether the dialog should be displayed.
* @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping to
* the right or by other dismiss action.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt
index 84634d8..ec6ee95a 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ColorScheme.kt
@@ -207,7 +207,6 @@
// Button Colors
internal var defaultButtonColorsCached: ButtonColors? = null
- internal var defaultFilledButtonColorsCached: ButtonColors? = null
internal var defaultFilledVariantButtonColorsCached: ButtonColors? = null
internal var defaultFilledTonalButtonColorsCached: ButtonColors? = null
internal var defaultOutlinedButtonColorsCached: ButtonColors? = null
@@ -221,7 +220,7 @@
internal var defaultOutlinedIconButtonColorsCached: IconButtonColors? = null
// Icon Toggle Button
- internal var defaultIconToggleButtonColorsCached: ToggleButtonColors? = null
+ internal var defaultIconToggleButtonColorsCached: IconToggleButtonColors? = null
// Text Button
internal var defaultTextButtonColorsCached: TextButtonColors? = null
@@ -231,7 +230,7 @@
internal var defaultOutlinedTextButtonColorsCached: TextButtonColors? = null
// Text Toggle Button
- internal var defaultTextToggleButtonColorsCached: ToggleButtonColors? = null
+ internal var defaultTextToggleButtonColorsCached: TextToggleButtonColors? = null
// Card
internal var defaultCardColorsCached: CardColors? = null
@@ -252,8 +251,20 @@
// Progress Indicator
internal var defaultProgressIndicatorColorsCached: ProgressIndicatorColors? = null
+ // Level Indicator
+ internal var defaultLevelIndicatorColorsCached: LevelIndicatorColors? = null
+
+ // Confirmation
+ internal var defaultConfirmationColorsCached: ConfirmationColors? = null
+ internal var defaultSuccessConfirmationColorsCached: ConfirmationColors? = null
+ internal var defaultFailureConfirmationColorsCached: ConfirmationColors? = null
+
+ // Open on Phone dialog
+ internal var mDefaultOpenOnPhoneDialogColorsCached: OpenOnPhoneDialogColors? = null
+
// Picker
internal var defaultTimePickerColorsCached: TimePickerColors? = null
+ internal var defaultDatePickerColorsCached: DatePickerColors? = null
}
/**
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
new file mode 100644
index 0000000..e88a30b
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
@@ -0,0 +1,656 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+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.LaunchedEffect
+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.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalAccessibilityManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.DialogProperties
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.CurvedScope
+import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.padding
+import androidx.wear.compose.material3.tokens.ColorSchemeKeyTokens
+import androidx.wear.compose.materialcore.screenHeightDp
+import androidx.wear.compose.materialcore.screenWidthDp
+import kotlinx.coroutines.delay
+
+/**
+ * Shows a [Confirmation] dialog with an icon and optional very short curved text. The length of the
+ * curved text should be very short and should not exceed 1-2 words. If a longer text required, then
+ * another [Confirmation] overload with a column content should be used instead.
+ *
+ * The confirmation will be showing a message to the user for [durationMillis]. After a specified
+ * timeout, the [onDismissRequest] callback will be invoked, where it's up to the caller to handle
+ * the dismissal. To hide the confirmation, [show] parameter should be set to false.
+ *
+ * Example of a [Confirmation] with an icon and a curved text content:
+ *
+ * @sample androidx.wear.compose.material3.samples.ConfirmationSample
+ * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
+ * swiping right or when the [durationMillis] has passed.
+ * @param curvedText A slot for displaying curved text content which will be shown along the bottom
+ * edge of the dialog.
+ * @param modifier Modifier to be applied to the confirmation content.
+ * @param colors A [ConfirmationColors] object for customizing the colors used in this
+ * [Confirmation].
+ * @param properties An optional [DialogProperties] object for configuring the dialog's behavior.
+ * @param durationMillis The duration in milliseconds for which the dialog is displayed. Defaults to
+ * [ConfirmationDefaults.ConfirmationDurationMillis].
+ * @param content A slot for displaying an icon inside the confirmation dialog. It's recommended to
+ * set its size to [ConfirmationDefaults.IconSize]
+ */
+@Composable
+fun Confirmation(
+ show: Boolean,
+ onDismissRequest: () -> Unit,
+ curvedText: (CurvedScope.() -> Unit)?,
+ modifier: Modifier = Modifier,
+ colors: ConfirmationColors = ConfirmationDefaults.confirmationColors(),
+ properties: DialogProperties = DialogProperties(),
+ durationMillis: Long = ConfirmationDefaults.ConfirmationDurationMillis,
+ content: @Composable BoxScope.() -> Unit
+) {
+ ConfirmationImpl(
+ show = show,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ iconContainer = confirmationIconContainer(true, colors.iconContainerColor),
+ curvedText = curvedText,
+ colors = colors,
+ properties = properties,
+ durationMillis = durationMillis,
+ content = content
+ )
+}
+
+/**
+ * Shows a [Confirmation] dialog with an icon and optional short text. The length of the text should
+ * not exceed 3 lines. If the text is very short and fits into 1-2 words, consider using another
+ * [Confirmation] overload with curvedContent instead.
+ *
+ * The confirmation will show a message to the user for [durationMillis]. After a specified timeout,
+ * the [onDismissRequest] callback will be invoked, where it's up to the caller to handle the
+ * dismissal. To hide the confirmation, [show] parameter should be set to false.
+ *
+ * Example of a [Confirmation] with an icon and a text which fits into 3 lines:
+ *
+ * @sample androidx.wear.compose.material3.samples.LongTextConfirmationSample
+ * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
+ * swiping right or when the [durationMillis] has passed.
+ * @param text A slot for displaying text below the icon. It should not exceed 3 lines.
+ * @param modifier Modifier to be applied to the confirmation content.
+ * @param colors A [ConfirmationColors] object for customizing the colors used in this
+ * [Confirmation].
+ * @param properties An optional [DialogProperties] object for configuring the dialog's behavior.
+ * @param durationMillis The duration in milliseconds for which the dialog is displayed. Defaults to
+ * [ConfirmationDefaults.ConfirmationDurationMillis].
+ * @param content A slot for displaying an icon inside the confirmation dialog, which can be
+ * animated. It's recommended to set its size to [ConfirmationDefaults.SmallIconSize]
+ */
+@Composable
+fun Confirmation(
+ show: Boolean,
+ onDismissRequest: () -> Unit,
+ text: @Composable (ColumnScope.() -> Unit)?,
+ modifier: Modifier = Modifier,
+ colors: ConfirmationColors = ConfirmationDefaults.confirmationColors(),
+ properties: DialogProperties = DialogProperties(),
+ durationMillis: Long = ConfirmationDefaults.ConfirmationDurationMillis,
+ content: @Composable BoxScope.() -> Unit
+) {
+
+ val a11yDurationMillis =
+ LocalAccessibilityManager.current?.calculateRecommendedTimeoutMillis(
+ originalTimeoutMillis = durationMillis,
+ containsIcons = true,
+ containsText = text != null,
+ containsControls = false,
+ ) ?: durationMillis
+
+ LaunchedEffect(show, a11yDurationMillis) {
+ if (show) {
+ delay(a11yDurationMillis)
+ onDismissRequest()
+ }
+ }
+
+ Dialog(
+ showDialog = show,
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ ) {
+ Box(Modifier.fillMaxSize()) {
+ val horizontalPadding =
+ screenWidthDp().dp * ConfirmationDefaults.HorizontalLinearContentPaddingFraction
+ Column(
+ modifier = Modifier.align(Alignment.Center).padding(horizontal = horizontalPadding),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ contentAlignment = Alignment.Center
+ ) {
+ confirmationIconContainer(false, colors.iconContainerColor)()
+ CompositionLocalProvider(LocalContentColor provides colors.iconColor) {
+ content()
+ }
+ }
+ CompositionLocalProvider(
+ LocalContentColor provides colors.textColor,
+ LocalTextStyle provides MaterialTheme.typography.titleMedium,
+ LocalTextAlign provides TextAlign.Center,
+ LocalTextMaxLines provides ConfirmationDefaults.LinearContentMaxLines
+ ) {
+ if (text != null) {
+ Spacer(Modifier.height(ConfirmationDefaults.LinearContentSpacing))
+ text()
+ Spacer(Modifier.height(ConfirmationDefaults.LinearContentSpacing))
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Shows a [Confirmation] dialog with a success icon and optional short curved text. This
+ * confirmation indicates a successful operation or action.
+ *
+ * The confirmation will show a message to the user for [durationMillis]. After a specified timeout,
+ * the [onDismissRequest] callback will be invoked, where it's up to the caller to handle the
+ * dismissal. To hide the confirmation, [show] parameter should be set to false.
+ *
+ * Example of a [SuccessConfirmation] usage:
+ *
+ * @sample androidx.wear.compose.material3.samples.SuccessConfirmationSample
+ * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
+ * swiping right or when the [durationMillis] has passed.
+ * @param modifier Modifier to be applied to the confirmation content.
+ * @param curvedText A slot for displaying curved text content which will be shown along the bottom
+ * edge of the dialog. Defaults to a localized success message.
+ * @param colors A [ConfirmationColors] object for customizing the colors used in this
+ * [SuccessConfirmation].
+ * @param properties An optional [DialogProperties] object for configuring the dialog's behavior.
+ * @param durationMillis The duration in milliseconds for which the dialog is displayed. Defaults to
+ * [ConfirmationDefaults.ConfirmationDurationMillis].
+ * @param content A slot for displaying an icon inside the confirmation dialog, which can be
+ * animated. Defaults to an animated [ConfirmationDefaults.SuccessIcon].
+ */
+@Composable
+fun SuccessConfirmation(
+ show: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ curvedText: (CurvedScope.() -> Unit)? = ConfirmationDefaults.successText(),
+ colors: ConfirmationColors = ConfirmationDefaults.successColors(),
+ properties: DialogProperties = DialogProperties(),
+ durationMillis: Long = ConfirmationDefaults.ConfirmationDurationMillis,
+ content: @Composable BoxScope.() -> Unit = ConfirmationDefaults.SuccessIcon,
+) {
+ ConfirmationImpl(
+ show = show,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ content = content,
+ iconContainer = successIconContainer(colors.iconContainerColor),
+ curvedText = curvedText,
+ colors = colors,
+ properties = properties,
+ durationMillis = durationMillis
+ )
+}
+
+/**
+ * Shows a [Confirmation] dialog with a failure icon and an optional short curved text. This
+ * confirmation indicates an unsuccessful operation or action.
+ *
+ * The confirmation will show a message to the user for [durationMillis]. After a specified timeout,
+ * the [onDismissRequest] callback will be invoked, where it's up to the caller to handle the
+ * dismissal. To hide the confirmation, [show] parameter should be set to false.
+ *
+ * Example of a [FailureConfirmation] usage:
+ *
+ * @sample androidx.wear.compose.material3.samples.FailureConfirmationSample
+ * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
+ * swiping right or when the [durationMillis] has passed.
+ * @param modifier Modifier to be applied to the confirmation content.
+ * @param curvedText A slot for displaying curved text content which will be shown along the bottom
+ * edge of the dialog. Defaults to a localized failure message.
+ * @param colors A [ConfirmationColors] object for customizing the colors used in this
+ * [FailureConfirmation].
+ * @param properties An optional [DialogProperties] object for configuring the dialog's behavior.
+ * @param durationMillis The duration in milliseconds for which the dialog is displayed. Defaults to
+ * [ConfirmationDefaults.ConfirmationDurationMillis].
+ * @param content A slot for displaying an icon inside the confirmation dialog, which can be
+ * animated. Defaults to [ConfirmationDefaults.FailureIcon].
+ */
+@Composable
+fun FailureConfirmation(
+ show: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ curvedText: (CurvedScope.() -> Unit)? = ConfirmationDefaults.failureText(),
+ colors: ConfirmationColors = ConfirmationDefaults.failureColors(),
+ properties: DialogProperties = DialogProperties(),
+ durationMillis: Long = ConfirmationDefaults.ConfirmationDurationMillis,
+ content: @Composable BoxScope.() -> Unit = ConfirmationDefaults.FailureIcon,
+) {
+ ConfirmationImpl(
+ show = show,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ iconContainer = failureIconContainer(colors.iconContainerColor),
+ curvedText = curvedText,
+ colors = colors,
+ properties = properties,
+ durationMillis = durationMillis,
+ content = content
+ )
+}
+
+/** Contains default values used by [Confirmation] composable. */
+object ConfirmationDefaults {
+
+ /**
+ * Returns a lambda to display a curved success message. The success message is retrieved from
+ * the application's string resources.
+ */
+ @Composable
+ fun successText(): CurvedScope.() -> Unit =
+ curvedText(
+ LocalContext.current.resources.getString(R.string.wear_m3c_confirmation_success_message)
+ )
+
+ /**
+ * Returns a lambda to display a curved failure message. The failure message is retrieved from
+ * the application's string resources.
+ */
+ @Composable
+ fun failureText(): CurvedScope.() -> Unit =
+ curvedText(
+ LocalContext.current.resources.getString(R.string.wear_m3c_confirmation_failure_message)
+ )
+
+ /**
+ * A default composable used in [SuccessConfirmation] that displays a success icon with an
+ * animation.
+ */
+ @OptIn(ExperimentalAnimationGraphicsApi::class)
+ val SuccessIcon: @Composable BoxScope.() -> Unit = {
+ val animation = AnimatedImageVector.animatedVectorResource(R.drawable.check_animation)
+ var atEnd by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ delay(FailureIconDelay)
+ atEnd = true
+ }
+ Icon(
+ painter = rememberAnimatedVectorPainter(animation, atEnd),
+ contentDescription = null,
+ modifier = Modifier.size(IconSize)
+ )
+ }
+
+ /**
+ * A default composable used in [FailureConfirmation] that displays a failure icon with an
+ * animation.
+ */
+ @OptIn(ExperimentalAnimationGraphicsApi::class)
+ val FailureIcon: @Composable BoxScope.() -> Unit = {
+ val animation = AnimatedImageVector.animatedVectorResource(R.drawable.failure_animation)
+ var atEnd by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ delay(FailureIconDelay)
+ atEnd = true
+ }
+ Icon(
+ painter = rememberAnimatedVectorPainter(animation, atEnd),
+ contentDescription = null,
+ modifier = Modifier.size(IconSize)
+ )
+ }
+
+ /**
+ * A default composable that displays text along a curved path, used in [Confirmation].
+ *
+ * @param text The text to display.
+ * @param style The style to apply to the text. Defaults to
+ * CurvedTextStyle(MaterialTheme.typography.titleLarge).
+ */
+ @Composable
+ fun curvedText(
+ text: String,
+ style: CurvedTextStyle = CurvedTextStyle(MaterialTheme.typography.titleLarge)
+ ): CurvedScope.() -> Unit = {
+ curvedText(
+ text = text,
+ style = style,
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ modifier = CurvedModifier.padding(PaddingDefaults.edgePadding),
+ angularDirection = CurvedDirection.Angular.Reversed
+ )
+ }
+
+ /**
+ * Creates a [ConfirmationColors] that represents the default colors used in a [Confirmation].
+ */
+ @Composable fun confirmationColors() = MaterialTheme.colorScheme.defaultConfirmationColors
+
+ /**
+ * Creates a [ConfirmationColors] with modified colors used in [Confirmation].
+ *
+ * @param iconColor The icon color.
+ * @param iconContainerColor The icon container color.
+ * @param textColor The text color.
+ */
+ @Composable
+ fun confirmationColors(
+ iconColor: Color = Color.Unspecified,
+ iconContainerColor: Color = Color.Unspecified,
+ textColor: Color = Color.Unspecified,
+ ) =
+ MaterialTheme.colorScheme.defaultConfirmationColors.copy(
+ iconColor = iconColor,
+ iconContainerColor = iconContainerColor,
+ textColor = textColor,
+ )
+
+ /**
+ * Creates a [ConfirmationColors] that represents the default colors used in a
+ * [SuccessConfirmation].
+ */
+ @Composable fun successColors() = MaterialTheme.colorScheme.defaultSuccessConfirmationColors
+
+ /**
+ * Creates a [ConfirmationColors] with modified colors used in [SuccessConfirmation].
+ *
+ * @param iconColor The icon color.
+ * @param iconContainerColor The icon container color.
+ * @param textColor The text color.
+ */
+ @Composable
+ fun successColors(
+ iconColor: Color = Color.Unspecified,
+ iconContainerColor: Color = Color.Unspecified,
+ textColor: Color = Color.Unspecified,
+ ) =
+ MaterialTheme.colorScheme.defaultSuccessConfirmationColors.copy(
+ iconColor = iconColor,
+ iconContainerColor = iconContainerColor,
+ textColor = textColor,
+ )
+
+ /**
+ * Creates a [ConfirmationColors] that represents the default colors used in a
+ * [FailureConfirmation].
+ */
+ @Composable fun failureColors() = MaterialTheme.colorScheme.defaultFailureConfirmationColors
+
+ /**
+ * Creates a [ConfirmationColors] with modified colors used in [FailureConfirmation].
+ *
+ * @param iconColor The icon color.
+ * @param iconContainerColor The icon container color.
+ * @param textColor The text color.
+ */
+ @Composable
+ fun failureColors(
+ iconColor: Color = Color.Unspecified,
+ iconContainerColor: Color = Color.Unspecified,
+ textColor: Color = Color.Unspecified,
+ ) =
+ MaterialTheme.colorScheme.defaultFailureConfirmationColors.copy(
+ iconColor = iconColor,
+ iconContainerColor = iconContainerColor,
+ textColor = textColor,
+ )
+
+ /** Default timeout for the [Confirmation] dialog, in milliseconds. */
+ const val ConfirmationDurationMillis = 4000L
+
+ /** Default icon size for the [Confirmation] with curved content */
+ val IconSize = 52.dp
+
+ /** Default icon size for the [Confirmation] with linear content */
+ val SmallIconSize = 36.dp
+
+ private val ColorScheme.defaultConfirmationColors: ConfirmationColors
+ get() {
+ return defaultConfirmationColorsCached
+ ?: ConfirmationColors(
+ iconColor = fromToken(ColorSchemeKeyTokens.Primary),
+ iconContainerColor = fromToken(ColorSchemeKeyTokens.OnPrimary),
+ textColor = fromToken(ColorSchemeKeyTokens.OnBackground)
+ )
+ .also { defaultConfirmationColorsCached = it }
+ }
+
+ private val ColorScheme.defaultSuccessConfirmationColors: ConfirmationColors
+ get() {
+ return defaultSuccessConfirmationColorsCached
+ ?: ConfirmationColors(
+ iconColor = fromToken(ColorSchemeKeyTokens.Primary),
+ iconContainerColor = fromToken(ColorSchemeKeyTokens.OnPrimary),
+ textColor = fromToken(ColorSchemeKeyTokens.OnBackground)
+ )
+ .also { defaultSuccessConfirmationColorsCached = it }
+ }
+
+ private val ColorScheme.defaultFailureConfirmationColors: ConfirmationColors
+ get() {
+ return defaultFailureConfirmationColorsCached
+ ?: ConfirmationColors(
+ iconColor = fromToken(ColorSchemeKeyTokens.ErrorContainer),
+ iconContainerColor =
+ fromToken(ColorSchemeKeyTokens.OnErrorContainer).copy(.8f),
+ textColor = fromToken(ColorSchemeKeyTokens.OnBackground)
+ )
+ .also { defaultFailureConfirmationColorsCached = it }
+ }
+
+ internal val FailureIconDelay = 67L
+
+ internal val SuccessWidthFraction = 0.496f
+ internal val SuccessHeightFraction = 0.6f
+ internal val FailureSizeFraction = 0.52f
+
+ internal val ConfirmationIconContainerSmallSize = 80.dp
+ internal val ConfirmationIconContainerSizeFraction = 0.52
+
+ internal val ExtraBottomPaddingFraction = 0.02f
+
+ internal val LinearContentSpacing = 8.dp
+ internal val LinearContentMaxLines = 3
+ internal val HorizontalLinearContentPaddingFraction = 0.12f
+}
+
+/**
+ * Represents the colors used in [Confirmation], [SuccessConfirmation] and [FailureConfirmation].
+ *
+ * @param iconColor Color used to tint the icon.
+ * @param iconContainerColor The color of the container behind the icon.
+ * @param textColor Color used to tint the text.
+ */
+class ConfirmationColors(
+ val iconColor: Color,
+ val iconContainerColor: Color,
+ val textColor: Color,
+) {
+ internal fun copy(
+ iconColor: Color? = null,
+ iconContainerColor: Color? = null,
+ textColor: Color? = null
+ ) =
+ ConfirmationColors(
+ iconColor = iconColor ?: this.iconColor,
+ iconContainerColor = iconContainerColor ?: this.iconContainerColor,
+ textColor = textColor ?: this.textColor,
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is ConfirmationColors) return false
+
+ if (iconColor != other.iconColor) return false
+ if (iconContainerColor != other.iconContainerColor) return false
+ if (textColor != other.textColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = iconColor.hashCode()
+ result = 31 * result + iconContainerColor.hashCode()
+ result = 31 * result + textColor.hashCode()
+ return result
+ }
+}
+
+@Composable
+internal fun ConfirmationImpl(
+ show: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier,
+ iconContainer: @Composable BoxScope.() -> Unit,
+ curvedText: (CurvedScope.() -> Unit)?,
+ colors: ConfirmationColors,
+ properties: DialogProperties,
+ durationMillis: Long,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val a11yDurationMillis =
+ LocalAccessibilityManager.current?.calculateRecommendedTimeoutMillis(
+ originalTimeoutMillis = durationMillis,
+ containsIcons = true,
+ containsText = curvedText != null,
+ containsControls = false,
+ ) ?: durationMillis
+
+ LaunchedEffect(show, a11yDurationMillis) {
+ if (show) {
+ delay(a11yDurationMillis)
+ onDismissRequest()
+ }
+ }
+
+ Dialog(
+ showDialog = show,
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ val bottomPadding =
+ if (curvedText != null)
+ screenHeightDp() * ConfirmationDefaults.ExtraBottomPaddingFraction
+ else 0f
+ Box(
+ Modifier.fillMaxSize().padding(bottom = bottomPadding.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ iconContainer()
+ CompositionLocalProvider(LocalContentColor provides colors.iconColor) { content() }
+ }
+ CompositionLocalProvider(LocalContentColor provides colors.textColor) {
+ curvedText?.let { CurvedLayout(anchor = 90f, contentBuilder = curvedText) }
+ }
+ }
+ }
+}
+
+private fun confirmationIconContainer(
+ curvedContent: Boolean,
+ color: Color
+): @Composable BoxScope.() -> Unit = {
+ val iconShape =
+ if (curvedContent) MaterialTheme.shapes.extraLarge else MaterialTheme.shapes.large
+ val width =
+ if (curvedContent) {
+ (screenWidthDp() * ConfirmationDefaults.ConfirmationIconContainerSizeFraction).dp
+ } else ConfirmationDefaults.ConfirmationIconContainerSmallSize
+
+ Box(
+ Modifier.size(width)
+ .graphicsLayer {
+ shape = iconShape
+ clip = true
+ }
+ .background(color)
+ .align(Alignment.Center)
+ )
+}
+
+private fun successIconContainer(color: Color): @Composable BoxScope.() -> Unit = {
+ val width = screenWidthDp() * ConfirmationDefaults.SuccessWidthFraction
+ val height = screenWidthDp() * ConfirmationDefaults.SuccessHeightFraction
+ Box(
+ Modifier.size(width.dp, height.dp)
+ .graphicsLayer {
+ rotationZ = 45f
+ shape = CircleShape
+ clip = true
+ }
+ .background(color)
+ )
+}
+
+private fun failureIconContainer(color: Color): @Composable BoxScope.() -> Unit = {
+ val iconShape = MaterialTheme.shapes.extraLarge
+ val width = screenWidthDp() * ConfirmationDefaults.FailureSizeFraction
+ Box(
+ Modifier.size(width.dp)
+ .graphicsLayer {
+ shape = iconShape
+ clip = true
+ }
+ .background(color)
+ )
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CurvedText.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CurvedText.kt
index 841c464..265ad90 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CurvedText.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CurvedText.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.material3
+import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
@@ -61,9 +62,10 @@
* Additionally, for [color], if [color] is not set, and [style] does not have a color, then
* [LocalContentColor] will be used.
*
- * For samples explicitly specifying style see:
+ * For samples using curved text in a [CurvedLayout] see:
*
- * For examples using CompositionLocal to specify the style, see:
+ * @sample androidx.wear.compose.material3.samples.CurvedTextTop
+ * @sample androidx.wear.compose.material3.samples.CurvedTextBottom
*
* For more information, see the
* [Curved Text](https://developer.android.com/training/wearables/compose/curved-text) guide.
@@ -86,9 +88,6 @@
* needs to be reversed in a Rtl layout. If not specified, it will be inherited from the enclosing
* [curvedRow] or [CurvedLayout] See [CurvedDirection.Angular].
* @param overflow How visual overflow should be handled.
- *
- * TODO(b/283777480): Add CurvedText samples
- * TODO(b/283777480): Add CurvedText samples
*/
fun CurvedScope.curvedText(
text: String,
@@ -143,4 +142,10 @@
* scrollable content.
*/
const val StaticContentMaxSweepAngle: Float = 120f
+
+ /**
+ * The recommended background color to use when displaying curved text so it is visible on top
+ * of other content.
+ */
+ @Composable fun backgroundColor() = MaterialTheme.colorScheme.background.copy(alpha = 0.85f)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
new file mode 100644
index 0000000..8283267
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
@@ -0,0 +1,823 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+import android.text.format.DateFormat
+import androidx.annotation.RequiresApi
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableIntState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.semantics.focused
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.ButtonDefaults.buttonColors
+import androidx.wear.compose.material3.ButtonDefaults.filledTonalButtonColors
+import androidx.wear.compose.material3.internal.Strings.Companion.DatePickerDay
+import androidx.wear.compose.material3.internal.Strings.Companion.DatePickerMonth
+import androidx.wear.compose.material3.internal.Strings.Companion.DatePickerYear
+import androidx.wear.compose.material3.internal.Strings.Companion.PickerConfirmButtonContentDescription
+import androidx.wear.compose.material3.internal.Strings.Companion.PickerNextButtonContentDescription
+import androidx.wear.compose.material3.internal.getString
+import androidx.wear.compose.material3.tokens.DatePickerTokens
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.time.temporal.TemporalAdjusters
+
+/**
+ * Full screen date picker with day, month, year.
+ *
+ * This component is designed to take most/all of the screen and utilizes large fonts.
+ *
+ * Example of a [DatePicker]:
+ *
+ * @sample androidx.wear.compose.material3.samples.DatePickerSample
+ *
+ * Example of a [DatePicker] shows the picker options in year-month-day order:
+ *
+ * @sample androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
+ *
+ * Example of a [DatePicker] with fromDate and toDate:
+ *
+ * @sample androidx.wear.compose.material3.samples.DatePickerFromDateToDateSample
+ * @param initialDate The initial value to be displayed in the DatePicker.
+ * @param onDatePicked The callback that is called when the user confirms the date selection. It
+ * provides the selected date as [LocalDate]
+ * @param modifier Modifier to be applied to the `Box` containing the UI elements.
+ * @param minDate Optional minimum date that can be selected in the DatePicker (inclusive).
+ * @param maxDate Optional maximum date that can be selected in the DatePicker (inclusive).
+ * @param datePickerType The different [DatePickerType] supported by this date picker.
+ * @param colors [DatePickerColors] to be applied to the DatePicker.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@Composable
+fun DatePicker(
+ initialDate: LocalDate,
+ onDatePicked: (LocalDate) -> Unit,
+ modifier: Modifier = Modifier,
+ minDate: LocalDate? = null,
+ maxDate: LocalDate? = null,
+ datePickerType: DatePickerType = DatePickerDefaults.datePickerType,
+ colors: DatePickerColors = DatePickerDefaults.datePickerColors()
+) {
+ val inspectionMode = LocalInspectionMode.current
+ val fullyDrawn = remember { Animatable(if (inspectionMode) 1f else 0f) }
+
+ if (minDate != null && maxDate != null) {
+ verifyDates(initialDate, minDate, maxDate)
+ }
+
+ val datePickerState = remember(initialDate) { DatePickerState(initialDate, minDate, maxDate) }
+
+ val touchExplorationStateProvider = remember { DefaultTouchExplorationStateProvider() }
+ val touchExplorationServicesEnabled by touchExplorationStateProvider.touchExplorationState()
+
+ // When the time picker loads, none of the individual pickers are selected in talkback mode,
+ // otherwise first picker should be focused.
+ val pickerGroupState =
+ if (touchExplorationServicesEnabled) {
+ rememberPickerGroupState(NoneSelectedIndex)
+ } else {
+ rememberPickerGroupState(0)
+ }
+
+ val isLargeScreen = LocalConfiguration.current.screenWidthDp > 225
+ val labelTextStyle =
+ if (isLargeScreen) {
+ DatePickerTokens.PickerLabelLargeTypography.value
+ } else {
+ DatePickerTokens.PickerLabelTypography.value
+ }
+ val optionTextStyle =
+ if (isLargeScreen) {
+ DatePickerTokens.PickerContentLargeTypography.value
+ } else {
+ DatePickerTokens.PickerContentTypography.value
+ }
+ val optionHeight = if (isLargeScreen) 48.dp else 36.dp
+
+ val focusRequesterConfirmButton = remember { FocusRequester() }
+
+ val yearString = getString(DatePickerYear)
+ val monthString = getString(DatePickerMonth)
+ val dayString = getString(DatePickerDay)
+
+ val prevStartMonth = remember { mutableIntStateOf(datePickerState.monthOptionStartMonth) }
+ LaunchedEffect(datePickerState.yearState.selectedOption) {
+ adjustOptionSelection(
+ prevStartState = prevStartMonth,
+ currentStartValue = datePickerState.monthOptionStartMonth,
+ currentNumberOfOptions = datePickerState.numberOfMonth,
+ pickerState = datePickerState.monthState,
+ )
+ }
+
+ val prevStartDay = remember { mutableIntStateOf(datePickerState.dayOptionStartDay) }
+ LaunchedEffect(
+ datePickerState.yearState.selectedOption,
+ datePickerState.monthState.selectedOption
+ ) {
+ adjustOptionSelection(
+ prevStartState = prevStartDay,
+ currentStartValue = datePickerState.dayOptionStartDay,
+ currentNumberOfOptions = datePickerState.numberOfDay,
+ pickerState = datePickerState.dayState,
+ )
+ }
+
+ val shortMonthNames = remember { getMonthNames("MMM") }
+ val fullMonthNames = remember { getMonthNames("MMMM") }
+ val yearContentDescription by
+ remember(
+ pickerGroupState.selectedIndex,
+ datePickerState.currentYear(),
+ ) {
+ derivedStateOf {
+ createDescriptionDatePicker(
+ pickerGroupState,
+ datePickerState.currentYear(),
+ yearString,
+ )
+ }
+ }
+ val monthContentDescription by
+ remember(
+ pickerGroupState.selectedIndex,
+ datePickerState.currentMonth(),
+ ) {
+ derivedStateOf {
+ if (pickerGroupState.selectedIndex == NoneSelectedIndex) {
+ monthString
+ } else {
+ fullMonthNames[(datePickerState.currentMonth() - 1) % 12]
+ }
+ }
+ }
+ val dayContentDescription by
+ remember(
+ pickerGroupState.selectedIndex,
+ datePickerState.currentDay(),
+ ) {
+ derivedStateOf {
+ createDescriptionDatePicker(
+ pickerGroupState,
+ datePickerState.currentDay(),
+ dayString,
+ )
+ }
+ }
+
+ val datePickerOptions = datePickerType.toDatePickerOptions()
+ val confirmButtonIndex = datePickerOptions.size
+
+ val onPickerSelected = { current: Int, next: Int ->
+ if (pickerGroupState.selectedIndex != current) {
+ pickerGroupState.selectedIndex = current
+ } else {
+ pickerGroupState.selectedIndex = next
+ if (next == confirmButtonIndex) {
+ focusRequesterConfirmButton.requestFocus()
+ }
+ }
+ }
+
+ BoxWithConstraints(modifier = modifier.fillMaxSize().alpha(fullyDrawn.value)) {
+ val boxConstraints = this
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(Modifier.height(14.dp))
+ Text(
+ text =
+ when (datePickerOptions.getOrNull(pickerGroupState.selectedIndex)) {
+ DatePickerOption.Day -> dayString
+ DatePickerOption.Month -> monthString
+ DatePickerOption.Year -> yearString
+ else -> ""
+ },
+ color = colors.pickerLabelColor,
+ style = labelTextStyle,
+ maxLines = 1,
+ )
+ Spacer(Modifier.height(if (isLargeScreen) 6.dp else 4.dp))
+ FontScaleIndependent {
+ val measurer = rememberTextMeasurer()
+ val density = LocalDensity.current
+ val (digitWidth, maxMonthWidth) =
+ remember(
+ density.density,
+ LocalConfiguration.current.screenWidthDp,
+ ) {
+ val mm =
+ measurer.measure(
+ "0123456789\n" + shortMonthNames.joinToString("\n"),
+ style = optionTextStyle,
+ density = density,
+ )
+
+ ((0..9).maxOf { mm.getBoundingBox(it).width }) to
+ ((1..12).maxOf { mm.getLineRight(it) - mm.getLineLeft(it) })
+ }
+
+ // Add spaces on to allow room to grow
+ val dayWidth =
+ with(LocalDensity.current) {
+ maxOf(
+ // Add 1dp buffer to compensate for potential conversion loss
+ (digitWidth * 2).toDp() + 1.dp,
+ minimumInteractiveComponentSize
+ )
+ }
+ val monthYearWidth =
+ with(LocalDensity.current) {
+ maxOf(
+ // Add 1dp buffer to compensate for potential conversion loss
+ maxOf(maxMonthWidth.toDp(), (digitWidth * 4).toDp()) + 1.dp,
+ minimumInteractiveComponentSize
+ )
+ }
+
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .weight(1f)
+ .offset(
+ getPickerGroupRowOffset(
+ boxConstraints.maxWidth,
+ dayWidth,
+ monthYearWidth,
+ monthYearWidth,
+ touchExplorationServicesEnabled,
+ pickerGroupState,
+ ),
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ val spacing = if (isLargeScreen) 6.dp else 4.dp
+
+ val pickerGroupItems =
+ datePickerOptions.mapIndexed { index, datePickerOption ->
+ when (datePickerOption) {
+ DatePickerOption.Day ->
+ PickerGroupItem(
+ pickerState = datePickerState.dayState,
+ modifier = Modifier.width(dayWidth).fillMaxHeight(),
+ onSelected = { onPickerSelected(index, index + 1) },
+ contentDescription = dayContentDescription,
+ option =
+ pickerTextOption(
+ textStyle = optionTextStyle,
+ indexToText = {
+ "%02d".format(datePickerState.currentDay(it))
+ },
+ optionHeight = optionHeight,
+ selectedContentColor =
+ colors.selectedPickerContentColor,
+ unselectedContentColor =
+ colors.unselectedPickerContentColor,
+ ),
+ spacing = spacing,
+ )
+ DatePickerOption.Month ->
+ PickerGroupItem(
+ pickerState = datePickerState.monthState,
+ modifier = Modifier.width(monthYearWidth).fillMaxHeight(),
+ onSelected = { onPickerSelected(index, index + 1) },
+ contentDescription = monthContentDescription,
+ option =
+ pickerTextOption(
+ textStyle = optionTextStyle,
+ indexToText = {
+ shortMonthNames[
+ (datePickerState.currentMonth(it) - 1) % 12]
+ },
+ optionHeight = optionHeight,
+ selectedContentColor =
+ colors.selectedPickerContentColor,
+ unselectedContentColor =
+ colors.unselectedPickerContentColor,
+ ),
+ spacing = spacing,
+ )
+ DatePickerOption.Year ->
+ PickerGroupItem(
+ pickerState = datePickerState.yearState,
+ modifier = Modifier.width(monthYearWidth).fillMaxHeight(),
+ onSelected = { onPickerSelected(index, index + 1) },
+ contentDescription = yearContentDescription,
+ option =
+ pickerTextOption(
+ textStyle = optionTextStyle,
+ indexToText = {
+ "%4d".format(datePickerState.currentYear(it))
+ },
+ optionHeight = optionHeight,
+ selectedContentColor =
+ colors.selectedPickerContentColor,
+ unselectedContentColor =
+ colors.unselectedPickerContentColor,
+ ),
+ spacing = spacing,
+ )
+ }
+ }
+
+ PickerGroup(
+ *pickerGroupItems.toTypedArray(),
+ pickerGroupState = pickerGroupState,
+ autoCenter = true,
+ separator = { Spacer(Modifier.width(if (isLargeScreen) 12.dp else 8.dp)) },
+ touchExplorationStateProvider = touchExplorationStateProvider,
+ )
+ }
+ }
+ Spacer(Modifier.height(if (isLargeScreen) 6.dp else 4.dp))
+ EdgeButton(
+ onClick = {
+ if (pickerGroupState.selectedIndex >= 2) {
+ val confirmedYear: Int = datePickerState.currentYear()
+ val confirmedMonth: Int = datePickerState.currentMonth()
+ val confirmedDay: Int = datePickerState.currentDay()
+ val confirmedDate =
+ LocalDate.of(confirmedYear, confirmedMonth, confirmedDay)
+ onDatePicked(confirmedDate)
+ } else {
+ onPickerSelected(
+ pickerGroupState.selectedIndex,
+ pickerGroupState.selectedIndex + 1
+ )
+ }
+ },
+ modifier =
+ Modifier.semantics {
+ focused = pickerGroupState.selectedIndex == confirmButtonIndex
+ }
+ .focusRequester(focusRequesterConfirmButton)
+ .focusable(),
+ colors =
+ if (pickerGroupState.selectedIndex >= 2) {
+ buttonColors(
+ contentColor = colors.confirmButtonContentColor,
+ containerColor = colors.confirmButtonContainerColor,
+ )
+ } else {
+ filledTonalButtonColors(
+ contentColor = colors.nextButtonContentColor,
+ containerColor = colors.nextButtonContainerColor,
+ )
+ }
+ ) {
+ Icon(
+ imageVector =
+ if (pickerGroupState.selectedIndex < 2) {
+ Icons.AutoMirrored.Filled.KeyboardArrowRight
+ } else {
+ Icons.Filled.Check
+ },
+ contentDescription =
+ if (pickerGroupState.selectedIndex >= 2) {
+ getString(PickerConfirmButtonContentDescription)
+ } else {
+ getString(PickerNextButtonContentDescription)
+ },
+ modifier = Modifier.size(24.dp).wrapContentSize(align = Alignment.Center),
+ )
+ }
+ }
+ }
+
+ if (!inspectionMode) {
+ LaunchedEffect(Unit) { fullyDrawn.animateTo(1f) }
+ }
+}
+
+/** Specifies the types of columns to display in the DatePicker. */
+@Immutable
+@JvmInline
+value class DatePickerType internal constructor(internal val value: Int) {
+
+ companion object {
+ val DayMonthYear = DatePickerType(0)
+ val MonthDayYear = DatePickerType(1)
+ val YearMonthDay = DatePickerType(2)
+ }
+
+ override fun toString(): String {
+ return when (this) {
+ DayMonthYear -> "DayMonthYear"
+ MonthDayYear -> "MonthDayYear"
+ YearMonthDay -> "YearMonthDay"
+ else -> "Unknown"
+ }
+ }
+}
+
+/** Contains the default values used by [DatePicker] */
+object DatePickerDefaults {
+
+ /** The default [DatePickerType] for [DatePicker] aligns with the current system date format. */
+ val datePickerType: DatePickerType
+ @Composable
+ get() {
+ val formatOrder = DateFormat.getDateFormatOrder(LocalContext.current)
+ return when (formatOrder[0]) {
+ 'M' -> DatePickerType.MonthDayYear
+ 'y' -> DatePickerType.YearMonthDay
+ else -> DatePickerType.DayMonthYear
+ }
+ }
+
+ /** Creates a [DatePickerColors] for a [DatePicker]. */
+ @Composable fun datePickerColors() = MaterialTheme.colorScheme.defaultDatePickerColors
+
+ /**
+ * Creates a [DatePickerColors] for a [DatePicker].
+ *
+ * @param selectedPickerContentColor The content color of selected picker.
+ * @param unselectedPickerContentColor The content color of unselected picker.
+ * @param pickerLabelColor The color of the picker label.
+ * @param nextButtonContentColor The content color of the next button.
+ * @param nextButtonContainerColor The container color of the next button.
+ * @param confirmButtonContentColor The content color of the confirm button.
+ * @param confirmButtonContainerColor The container color of the confirm button.
+ */
+ @Composable
+ fun datePickerColors(
+ selectedPickerContentColor: Color = Color.Unspecified,
+ unselectedPickerContentColor: Color = Color.Unspecified,
+ pickerLabelColor: Color = Color.Unspecified,
+ nextButtonContentColor: Color = Color.Unspecified,
+ nextButtonContainerColor: Color = Color.Unspecified,
+ confirmButtonContentColor: Color = Color.Unspecified,
+ confirmButtonContainerColor: Color = Color.Unspecified,
+ ) =
+ MaterialTheme.colorScheme.defaultDatePickerColors.copy(
+ selectedPickerContentColor = selectedPickerContentColor,
+ unselectedPickerContentColor = unselectedPickerContentColor,
+ pickerLabelColor = pickerLabelColor,
+ nextButtonContentColor = nextButtonContentColor,
+ nextButtonContainerColor = nextButtonContainerColor,
+ confirmButtonContentColor = confirmButtonContentColor,
+ confirmButtonContainerColor = confirmButtonContainerColor,
+ )
+
+ private val ColorScheme.defaultDatePickerColors: DatePickerColors
+ get() {
+ return defaultDatePickerColorsCached
+ ?: DatePickerColors(
+ selectedPickerContentColor =
+ fromToken(DatePickerTokens.SelectedPickerContentColor),
+ unselectedPickerContentColor =
+ fromToken(DatePickerTokens.UnselectedPickerContentColor),
+ pickerLabelColor = fromToken(DatePickerTokens.PickerLabelColor),
+ nextButtonContentColor = fromToken(DatePickerTokens.NextButtonContentColor),
+ nextButtonContainerColor =
+ fromToken(DatePickerTokens.NextButtonContainerColor),
+ confirmButtonContentColor =
+ fromToken(DatePickerTokens.ConfirmButtonContentColor),
+ confirmButtonContainerColor =
+ fromToken(DatePickerTokens.ConfirmButtonContainerColor),
+ )
+ .also { defaultDatePickerColorsCached = it }
+ }
+}
+
+@Immutable
+class DatePickerColors
+constructor(
+ val selectedPickerContentColor: Color,
+ val unselectedPickerContentColor: Color,
+ val pickerLabelColor: Color,
+ val nextButtonContentColor: Color,
+ val nextButtonContainerColor: Color,
+ val confirmButtonContentColor: Color,
+ val confirmButtonContainerColor: Color,
+) {
+ internal fun copy(
+ selectedPickerContentColor: Color,
+ unselectedPickerContentColor: Color,
+ pickerLabelColor: Color,
+ nextButtonContentColor: Color,
+ nextButtonContainerColor: Color,
+ confirmButtonContentColor: Color,
+ confirmButtonContainerColor: Color,
+ ) =
+ DatePickerColors(
+ selectedPickerContentColor =
+ selectedPickerContentColor.takeOrElse { this.selectedPickerContentColor },
+ unselectedPickerContentColor =
+ unselectedPickerContentColor.takeOrElse { this.unselectedPickerContentColor },
+ pickerLabelColor = pickerLabelColor.takeOrElse { this.pickerLabelColor },
+ nextButtonContentColor =
+ nextButtonContentColor.takeOrElse { this.nextButtonContentColor },
+ nextButtonContainerColor =
+ nextButtonContainerColor.takeOrElse { this.nextButtonContainerColor },
+ confirmButtonContentColor =
+ confirmButtonContentColor.takeOrElse { this.confirmButtonContentColor },
+ confirmButtonContainerColor =
+ confirmButtonContainerColor.takeOrElse { this.confirmButtonContainerColor },
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is DatePickerColors) return false
+
+ if (selectedPickerContentColor != other.selectedPickerContentColor) return false
+ if (unselectedPickerContentColor != other.unselectedPickerContentColor) return false
+ if (pickerLabelColor != other.pickerLabelColor) return false
+ if (nextButtonContentColor != other.nextButtonContentColor) return false
+ if (nextButtonContainerColor != other.nextButtonContainerColor) return false
+ if (confirmButtonContentColor != other.confirmButtonContentColor) return false
+ if (confirmButtonContainerColor != other.confirmButtonContainerColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = selectedPickerContentColor.hashCode()
+ result = 31 * result + unselectedPickerContentColor.hashCode()
+ result = 31 * result + pickerLabelColor.hashCode()
+ result = 31 * result + nextButtonContentColor.hashCode()
+ result = 31 * result + nextButtonContainerColor.hashCode()
+ result = 31 * result + confirmButtonContentColor.hashCode()
+ result = 31 * result + confirmButtonContainerColor.hashCode()
+
+ return result
+ }
+}
+
+/** Represents the possible column options for the DatePicker. */
+private enum class DatePickerOption {
+ Day,
+ Month,
+ Year
+}
+
+private fun DatePickerType.toDatePickerOptions() =
+ when (value) {
+ DatePickerType.YearMonthDay.value ->
+ arrayOf(DatePickerOption.Year, DatePickerOption.Month, DatePickerOption.Day)
+ DatePickerType.MonthDayYear.value ->
+ arrayOf(DatePickerOption.Month, DatePickerOption.Day, DatePickerOption.Year)
+ else -> arrayOf(DatePickerOption.Day, DatePickerOption.Month, DatePickerOption.Year)
+ }
+
+@RequiresApi(Build.VERSION_CODES.O)
+private fun verifyDates(
+ date: LocalDate,
+ fromDate: LocalDate,
+ toDate: LocalDate,
+) {
+ require(toDate >= fromDate) { "toDate should be greater than or equal to fromDate" }
+ require(date in fromDate..toDate) { "date should lie between fromDate and toDate" }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+private fun getMonthNames(pattern: String): List<String> {
+ val monthFormatter = DateTimeFormatter.ofPattern(pattern)
+ val months = 1..12
+ return months.map { LocalDate.of(2022, it, 1).format(monthFormatter) }
+}
+
+private fun getPickerGroupRowOffset(
+ rowWidth: Dp,
+ dayPickerWidth: Dp,
+ monthPickerWidth: Dp,
+ yearPickerWidth: Dp,
+ touchExplorationServicesEnabled: Boolean,
+ pickerGroupState: PickerGroupState,
+): Dp {
+ val currentOffset = (rowWidth - (dayPickerWidth + monthPickerWidth + yearPickerWidth)) / 2
+
+ return if (touchExplorationServicesEnabled && pickerGroupState.selectedIndex < 0) {
+ ((rowWidth - dayPickerWidth) / 2) - currentOffset
+ } else if (touchExplorationServicesEnabled && pickerGroupState.selectedIndex > 2) {
+ ((rowWidth - yearPickerWidth) / 2) - (dayPickerWidth + monthPickerWidth + currentOffset)
+ } else {
+ 0.dp
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+private class DatePickerState(
+ private val date: LocalDate,
+ private val fromDate: LocalDate?,
+ private val toDate: LocalDate?,
+) {
+ // Year range 1900 - 2100 was suggested in b/277885199
+ private val startYear = fromDate?.year ?: 1900
+
+ private val numOfYears =
+ if (toDate != null) {
+ toDate.year - startYear + 1
+ } else {
+ 2100 - startYear + 1
+ }
+
+ val yearState =
+ PickerState(
+ initialNumberOfOptions = numOfYears,
+ initiallySelectedOption = date.year - startYear,
+ repeatItems = numOfYears > 2
+ )
+
+ val monthState =
+ PickerState(
+ initialNumberOfOptions = numberOfMonth,
+ initiallySelectedOption = date.monthValue - monthOptionStartMonth,
+ repeatItems = numberOfMonth > 2
+ )
+
+ val dayState =
+ PickerState(
+ initialNumberOfOptions = numberOfDay,
+ initiallySelectedOption = date.dayOfMonth - dayOptionStartDay,
+ repeatItems = numberOfDay > 2
+ )
+
+ val numberOfMonth: Int
+ get() = monthOptionEndMonth - monthOptionStartMonth + 1
+
+ val monthOptionStartMonth: Int
+ get() =
+ if (fromDate != null && selectedYearEqualsFromYear) {
+ fromDate.monthValue
+ } else {
+ 1
+ }
+
+ val monthOptionEndMonth: Int
+ get() =
+ if (toDate != null && selectedYearEqualsToYear) {
+ toDate.monthValue
+ } else {
+ 12
+ }
+
+ val numberOfDay: Int
+ get() = dayOptionEndDay - dayOptionStartDay + 1
+
+ val dayOptionStartDay: Int
+ get() =
+ if (fromDate != null && selectedMonthEqualsFromMonth) {
+ fromDate.dayOfMonth
+ } else {
+ 1
+ }
+
+ val dayOptionEndDay: Int
+ get() =
+ if (toDate != null && selectedMonthEqualsToMonth) {
+ toDate.dayOfMonth
+ } else {
+ maxDayInMonth
+ }
+
+ fun currentYear(year: Int = yearState.selectedOption): Int {
+ return year + startYear
+ }
+
+ fun currentMonth(monthIndex: Int = monthState.selectedOption): Int {
+ return monthIndex + monthOptionStartMonth
+ }
+
+ fun currentDay(day: Int = dayState.selectedOption): Int {
+ return day + dayOptionStartDay
+ }
+
+ private val selectedYearEqualsFromYear: Boolean
+ get() = fromDate?.year == currentYear()
+
+ private val selectedYearEqualsToYear: Boolean
+ get() = toDate?.year == currentYear()
+
+ private val selectedMonthEqualsFromMonth: Boolean
+ get() = selectedYearEqualsFromYear && fromDate?.monthValue == currentMonth()
+
+ private val selectedMonthEqualsToMonth: Boolean
+ get() = selectedYearEqualsToYear && toDate?.monthValue == currentMonth()
+
+ private val firstDayOfMonth: LocalDate
+ get() =
+ LocalDate.of(
+ currentYear(),
+ currentMonth(),
+ 1,
+ )
+
+ private val maxDayInMonth
+ get() = firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth
+}
+
+private fun createDescriptionDatePicker(
+ pickerGroupState: PickerGroupState,
+ selectedValue: Int,
+ label: String,
+): String {
+ return when (pickerGroupState.selectedIndex) {
+ NoneSelectedIndex -> label
+ else -> "$label, $selectedValue"
+ }
+}
+
+private suspend fun adjustOptionSelection(
+ prevStartState: MutableIntState,
+ currentStartValue: Int,
+ currentNumberOfOptions: Int,
+ pickerState: PickerState
+) {
+ val prevStartValue = prevStartState.intValue
+ val prevSelectedOption = pickerState.selectedOption
+ val prevSelectedValue = prevSelectedOption + prevStartValue
+ val prevNumberOfOptions: Int = pickerState.numberOfOptions
+ // Update picker's number of options if changed.
+ if (currentNumberOfOptions != prevNumberOfOptions) {
+ pickerState.numberOfOptions = currentNumberOfOptions
+ }
+ when {
+ currentStartValue != prevStartValue && prevStartValue != 1 -> { // Scrolled from `fromDate`
+ val prevSelectedValueIndex = prevSelectedValue - 1
+ // Check if previous value still exists in current options.
+ if (prevSelectedValueIndex < currentNumberOfOptions) {
+ // Scroll to the index which has the same value with the previous value.
+ pickerState.scrollToOption(prevSelectedValueIndex)
+ } else {
+ // Scroll to the closet value to the previous value.
+ pickerState.scrollToOption(currentNumberOfOptions - 1)
+ }
+ prevStartState.intValue = currentStartValue
+ }
+ currentStartValue != 1 -> { // Scrolled to `fromDate`
+ val currentValueIndex =
+ if (prevSelectedValue >= currentStartValue) {
+ // Scroll to the index which has the same value with the previous value.
+ prevSelectedValue - currentStartValue
+ } else {
+ // Scroll to the closet value to the previous value.
+ 0
+ }
+ pickerState.scrollToOption(currentValueIndex)
+ prevStartState.intValue = currentStartValue
+ }
+ currentNumberOfOptions != prevNumberOfOptions -> { // Only number of options changed.
+ if (prevSelectedOption >= currentNumberOfOptions) {
+ // Scroll to the closet value to the previous value.
+ pickerState.animateScrollToOption(currentNumberOfOptions - 1)
+ }
+ }
+ }
+}
+
+private const val NoneSelectedIndex = -1
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/dialog/Dialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
similarity index 97%
rename from wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/dialog/Dialog.kt
rename to wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
index d42ba2d..f5c6dc6 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/dialog/Dialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.dialog
+package androidx.wear.compose.material3
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Transition
@@ -34,9 +34,6 @@
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.window.DialogProperties
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
-import androidx.wear.compose.material3.MaterialTheme
-import androidx.wear.compose.material3.ScreenScaffold
-import androidx.wear.compose.material3.SwipeToDismissBox
import androidx.wear.compose.material3.tokens.MotionTokens
/**
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
index 46550fb..faccf84 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
@@ -155,6 +155,10 @@
val shape = EdgeButtonShape(containerShapeHelper)
val containerFadeStartPx = with(LocalDensity.current) { CONTAINER_FADE_START_DP.toPx() }
+ val containerFadeEndPx = with(LocalDensity.current) { CONTAINER_FADE_END_DP.toPx() }
+ val contentFadeStartPx = with(LocalDensity.current) { CONTENT_FADE_START_DP.toPx() }
+ val contentFadeEndPx = with(LocalDensity.current) { CONTENT_FADE_END_DP.toPx() }
+
val borderModifier =
if (border != null) Modifier.border(border = border, shape = shape) else Modifier
Row(
@@ -191,7 +195,13 @@
.graphicsLayer {
// Container fades when button height goes from 18dp to 0dp
val height = (containerShapeHelper.lastSize.value?.height ?: 0f)
- alpha = easing.transform(height / containerFadeStartPx).coerceIn(0f, 1f)
+ alpha =
+ easing
+ .transform(
+ (height - containerFadeEndPx) /
+ ((containerFadeStartPx - containerFadeEndPx))
+ )
+ .coerceIn(0f, 1f)
}
.then(borderModifier)
.clip(shape = shape)
@@ -206,6 +216,16 @@
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
+ val height = (containerShapeHelper.lastSize.value?.height ?: 0f)
+
+ val alpha =
+ easing
+ .transform(
+ (height - contentFadeEndPx).toFloat() /
+ ((contentFadeStartPx - contentFadeEndPx))
+ )
+ .coerceIn(0f, 1f)
+
drawContent()
// Draw the gradient.
// We use the max dimension (width) as a proxy for screen size.
@@ -213,7 +233,7 @@
val center = Offset(r, size.height - r)
drawRect(
Brush.radialGradient(
- 0.875f to Color.White,
+ 0.875f to Color.White.copy(alpha),
1.0f to Color.Transparent,
center = center,
radius = r
@@ -230,10 +250,9 @@
)
.sizeAndOffset { containerShapeHelper.contentWindow }
.scaleAndAlignContent()
- // Limit the content size to the expected box for the button size.
+ // Limit the content size to the expected width for the button size.
.requiredSizeIn(
maxWidth = contentShapeHelper.contentWidthDp(),
- maxHeight = contentShapeHelper.contentHeightDp()
),
content =
provideScopeContent(
@@ -406,6 +425,8 @@
): MeasureResult {
val placeable = measurable.measure(Constraints())
+ val topPadding = INTERNAL_TOP_PADDING.roundToPx()
+
val wrapperWidth = placeable.width.coerceIn(constraints.minWidth, constraints.maxWidth)
val wrapperHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight)
@@ -417,25 +438,27 @@
IntOffset(
x = (wrapperWidth - placeable.width) / 2, // Always center horizontally
y =
- if (placeable.height * scale > constraints.maxHeight) {
- // Top
- 0
- } else {
- // Center vertically too.
- ((wrapperHeight - placeable.height * scale) / 2).roundToInt()
- }
+ ((wrapperHeight - placeable.height * scale) / 2)
+ .roundToInt()
+ .coerceAtLeast(topPadding)
)
placeable.placeWithLayer(position) {
scaleX = scale
scaleY = scale
+ translationX
transformOrigin = TransformOrigin(0.5f, 0f)
}
}
}
}
-// At which button height the container starts fading away. Fade ends at 0.dp
-private val CONTAINER_FADE_START_DP = 18.dp
+// Sizes at which the container will start and end fading away.
+private val CONTAINER_FADE_START_DP = 30.dp
+private val CONTAINER_FADE_END_DP = 4.dp
+
+// Sizes at which the content will start and end fading away.
+private val CONTENT_FADE_START_DP = 38.dp
+private val CONTENT_FADE_END_DP = 30.dp
// How tall the ellipsis is for the extra small button.
// Edge buttons are drawn as half a rounded rectangle on top of half an ellipsis.
@@ -450,3 +473,6 @@
// Padding around the Edge Button on it's top and bottom.
private val VERTICAL_PADDING = 3.dp
+
+// Padding between the top edge of the button and the content.
+private val INTERNAL_TOP_PADDING = 6.dp
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/FontScaleIndependent.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/FontScaleIndependent.kt
new file mode 100644
index 0000000..6ab4c35
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/FontScaleIndependent.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+
+@Composable
+internal fun FontScaleIndependent(content: @Composable () -> Unit) {
+ CompositionLocalProvider(
+ value =
+ LocalDensity provides
+ Density(
+ density = LocalDensity.current.density,
+ fontScale = 1f,
+ ),
+ content = content
+ )
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
index 9467d0e..1d4e8e4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
@@ -16,6 +16,8 @@
package androidx.wear.compose.material3
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
@@ -27,6 +29,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
@@ -37,8 +40,10 @@
import androidx.wear.compose.material3.tokens.FilledTonalIconButtonTokens
import androidx.wear.compose.material3.tokens.IconButtonTokens
import androidx.wear.compose.material3.tokens.IconToggleButtonTokens
+import androidx.wear.compose.material3.tokens.MotionTokens
import androidx.wear.compose.material3.tokens.OutlinedIconButtonTokens
import androidx.wear.compose.material3.tokens.ShapeTokens
+import androidx.wear.compose.materialcore.animateSelectionColor
/**
* Wear Material [IconButton] is a circular, icon-only button with transparent background and no
@@ -68,6 +73,10 @@
* Example of an [IconButton] with shape animation of rounded corners on press:
*
* @sample androidx.wear.compose.material3.samples.IconButtonWithCornerAnimationSample
+ *
+ * Example of an [IconButton] with image content:
+ *
+ * @sample androidx.wear.compose.material3.samples.IconButtonWithImageSample
* @param onClick Will be called when the user clicks the button.
* @param modifier Modifier to be applied to the button.
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -351,8 +360,8 @@
* @param modifier Modifier to be applied to the toggle button.
* @param enabled Controls the enabled state of the toggle button. When `false`, this toggle button
* will not be clickable.
- * @param colors [ToggleButtonColors] that will be used to resolve the container and content color
- * for this toggle button.
+ * @param colors [IconToggleButtonColors] that will be used to resolve the container and content
+ * color for this toggle button.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting [Interaction]s for this button. You can use this to change the button's appearance or
* preview the button in different states. Note that if `null` is provided, interactions will
@@ -368,7 +377,7 @@
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: ToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
+ colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = IconButtonDefaults.shape,
border: BorderStroke? = null,
@@ -402,6 +411,9 @@
val pressedShape: CornerBasedShape
@Composable get() = MaterialTheme.shapes.small
+ /** Recommended alpha to apply to an IconButton with Image content with disabled */
+ val disabledImageOpacity = DisabledContentAlpha
+
/**
* Creates a [Shape] with a animation between two CornerBasedShapes.
*
@@ -599,7 +611,7 @@
)
/**
- * Creates a [ToggleButtonColors] for a [IconToggleButton]
+ * Creates an [IconToggleButtonColors] for a [IconToggleButton]
* - by default, a colored background with a contrasting content color.
*
* If the button is disabled, then the colors will have an alpha ([DisabledContentAlpha] and
@@ -609,7 +621,7 @@
fun iconToggleButtonColors() = MaterialTheme.colorScheme.defaultIconToggleButtonColors
/**
- * Creates a [ToggleButtonColors] for a [IconToggleButton]
+ * Creates a [IconToggleButtonColors] for a [IconToggleButton]
* - by default, a colored background with a contrasting content color.
*
* If the button is disabled, then the colors will have an alpha ([DisabledContentAlpha] and
@@ -642,7 +654,7 @@
disabledCheckedContentColor: Color = Color.Unspecified,
disabledUncheckedContainerColor: Color = Color.Unspecified,
disabledUncheckedContentColor: Color = Color.Unspecified,
- ): ToggleButtonColors =
+ ): IconToggleButtonColors =
MaterialTheme.colorScheme.defaultIconToggleButtonColors.copy(
checkedContainerColor = checkedContainerColor,
checkedContentColor = checkedContentColor,
@@ -790,10 +802,10 @@
.also { defaultIconButtonColorsCached = it }
}
- private val ColorScheme.defaultIconToggleButtonColors: ToggleButtonColors
+ private val ColorScheme.defaultIconToggleButtonColors: IconToggleButtonColors
get() {
return defaultIconToggleButtonColorsCached
- ?: ToggleButtonColors(
+ ?: IconToggleButtonColors(
checkedContainerColor =
fromToken(IconToggleButtonTokens.CheckedContainerColor),
checkedContentColor = fromToken(IconToggleButtonTokens.CheckedContentColor),
@@ -907,3 +919,129 @@
return result
}
}
+
+/**
+ * Represents the different container and content colors used for [IconToggleButton] in various
+ * states, that are checked, unchecked, enabled and disabled.
+ *
+ * @param checkedContainerColor Container or background color when the toggle button is checked
+ * @param checkedContentColor Color of the content (text or icon) when the toggle button is checked
+ * @param uncheckedContainerColor Container or background color when the toggle button is unchecked
+ * @param uncheckedContentColor Color of the content (text or icon) when the toggle button is
+ * unchecked
+ * @param disabledCheckedContainerColor Container or background color when the toggle button is
+ * disabled and checked
+ * @param disabledCheckedContentColor Color of content (text or icon) when the toggle button is
+ * disabled and checked
+ * @param disabledUncheckedContainerColor Container or background color when the toggle button is
+ * disabled and unchecked
+ * @param disabledUncheckedContentColor Color of the content (text or icon) when the toggle button
+ * is disabled and unchecked
+ */
+@Immutable
+class IconToggleButtonColors(
+ val checkedContainerColor: Color,
+ val checkedContentColor: Color,
+ val uncheckedContainerColor: Color,
+ val uncheckedContentColor: Color,
+ val disabledCheckedContainerColor: Color,
+ val disabledCheckedContentColor: Color,
+ val disabledUncheckedContainerColor: Color,
+ val disabledUncheckedContentColor: Color,
+) {
+ internal fun copy(
+ checkedContainerColor: Color,
+ checkedContentColor: Color,
+ uncheckedContainerColor: Color,
+ uncheckedContentColor: Color,
+ disabledCheckedContainerColor: Color,
+ disabledCheckedContentColor: Color,
+ disabledUncheckedContainerColor: Color,
+ disabledUncheckedContentColor: Color,
+ ): IconToggleButtonColors =
+ IconToggleButtonColors(
+ checkedContainerColor = checkedContainerColor.takeOrElse { this.checkedContainerColor },
+ checkedContentColor = checkedContentColor.takeOrElse { this.checkedContentColor },
+ uncheckedContainerColor =
+ uncheckedContainerColor.takeOrElse { this.uncheckedContainerColor },
+ uncheckedContentColor = uncheckedContentColor.takeOrElse { this.uncheckedContentColor },
+ disabledCheckedContainerColor =
+ disabledCheckedContainerColor.takeOrElse { this.disabledCheckedContainerColor },
+ disabledCheckedContentColor =
+ disabledCheckedContentColor.takeOrElse { this.disabledCheckedContentColor },
+ disabledUncheckedContainerColor =
+ disabledUncheckedContainerColor.takeOrElse { this.disabledUncheckedContainerColor },
+ disabledUncheckedContentColor =
+ disabledUncheckedContentColor.takeOrElse { this.disabledUncheckedContentColor },
+ )
+
+ /**
+ * Determines the container color based on whether the toggle button is [enabled] and [checked].
+ *
+ * @param enabled Whether the toggle button is enabled
+ * @param checked Whether the toggle button is checked
+ */
+ @Composable
+ internal fun containerColor(enabled: Boolean, checked: Boolean): State<Color> =
+ animateSelectionColor(
+ enabled = enabled,
+ checked = checked,
+ checkedColor = checkedContainerColor,
+ uncheckedColor = uncheckedContainerColor,
+ disabledCheckedColor = disabledCheckedContainerColor,
+ disabledUncheckedColor = disabledUncheckedContainerColor,
+ animationSpec = COLOR_ANIMATION_SPEC
+ )
+
+ /**
+ * Determines the content color based on whether the toggle button is [enabled] and [checked].
+ *
+ * @param enabled Whether the toggle button is enabled
+ * @param checked Whether the toggle button is checked
+ */
+ @Composable
+ internal fun contentColor(enabled: Boolean, checked: Boolean): State<Color> =
+ animateSelectionColor(
+ enabled = enabled,
+ checked = checked,
+ checkedColor = checkedContentColor,
+ uncheckedColor = uncheckedContentColor,
+ disabledCheckedColor = disabledCheckedContentColor,
+ disabledUncheckedColor = disabledUncheckedContentColor,
+ animationSpec = COLOR_ANIMATION_SPEC
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null) return false
+ if (this::class != other::class) return false
+
+ other as IconToggleButtonColors
+
+ if (checkedContainerColor != other.checkedContainerColor) return false
+ if (checkedContentColor != other.checkedContentColor) return false
+ if (uncheckedContainerColor != other.uncheckedContainerColor) return false
+ if (uncheckedContentColor != other.uncheckedContentColor) return false
+ if (disabledCheckedContainerColor != other.disabledCheckedContainerColor) return false
+ if (disabledCheckedContentColor != other.disabledCheckedContentColor) return false
+ if (disabledUncheckedContainerColor != other.disabledUncheckedContainerColor) return false
+ if (disabledUncheckedContentColor != other.disabledUncheckedContentColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = checkedContainerColor.hashCode()
+ result = 31 * result + checkedContentColor.hashCode()
+ result = 31 * result + uncheckedContainerColor.hashCode()
+ result = 31 * result + uncheckedContentColor.hashCode()
+ result = 31 * result + disabledCheckedContainerColor.hashCode()
+ result = 31 * result + disabledCheckedContentColor.hashCode()
+ result = 31 * result + disabledUncheckedContainerColor.hashCode()
+ result = 31 * result + disabledUncheckedContentColor.hashCode()
+ return result
+ }
+}
+
+private val COLOR_ANIMATION_SPEC: AnimationSpec<Color> =
+ tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/LevelIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/LevelIndicator.kt
new file mode 100644
index 0000000..6b112ae
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/LevelIndicator.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.tokens.ColorSchemeKeyTokens
+import kotlin.math.sin
+
+/**
+ * Creates a [LevelIndicator] for screens that that control a setting such as volume with either
+ * rotating side button, rotating bezel or a [Stepper].
+ *
+ * Example of [LevelIndicator] with a [Stepper]:
+ *
+ * @sample androidx.wear.compose.material3.samples.StepperSample
+ * @param value Value of the indicator in the [valueRange].
+ * @param modifier Modifier to be applied to the component
+ * @param valueRange range of values that [value] can take
+ * @param enabled Controls the enabled state of [LevelIndicator] - when false, disabled colors will
+ * be used.
+ * @param colors [LevelIndicatorColors] that will be used to resolve the indicator and track colors
+ * for this [LevelIndicator] in different states
+ * @param strokeWidth The stroke width for the indicator and track strokes
+ * @param sweepAngle The angle covered by the curved LevelIndicator
+ * @param reverseDirection Reverses direction of PositionIndicator if true
+ */
+@Composable
+fun LevelIndicator(
+ value: () -> Float,
+ modifier: Modifier = Modifier,
+ valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+ enabled: Boolean = true,
+ colors: LevelIndicatorColors = LevelIndicatorDefaults.colors(),
+ strokeWidth: Dp = LevelIndicatorDefaults.StrokeWidth,
+ sweepAngle: Float = LevelIndicatorDefaults.SweepAngle,
+ reverseDirection: Boolean = false,
+) {
+ val screenWidthDp = LocalConfiguration.current.screenWidthDp
+ val paddingHorizontal = LevelIndicatorDefaults.edgePadding
+ val radius = screenWidthDp / 2 - paddingHorizontal.value - strokeWidth.value / 2
+ // Calculate indicator height based on a triangle of the top half of the sweep angle
+ // and subtract the end caps
+ val indicatorHeight = 2f * sin((0.5f * sweepAngle).toRadians()) * radius - strokeWidth.value
+
+ IndicatorImpl(
+ state =
+ FractionPositionStateAdapter {
+ (value() - valueRange.start) / (valueRange.endInclusive - valueRange.start)
+ },
+ indicatorHeight = indicatorHeight.dp,
+ indicatorWidth = strokeWidth,
+ paddingHorizontal = paddingHorizontal,
+ background = colors.trackColor(enabled),
+ color = colors.indicatorColor(enabled),
+ modifier = modifier,
+ reverseDirection = reverseDirection,
+ rsbSide = false,
+ )
+}
+
+/**
+ * Creates a [LevelIndicator] for screens that that control a setting such as volume with either
+ * rotating side button, rotating bezel or a [Stepper].
+ *
+ * Example of [LevelIndicator] with a [Stepper] working on an [IntProgression]:
+ *
+ * @sample androidx.wear.compose.material3.samples.StepperWithIntegerSample
+ * @param value Current value of the Stepper. If outside of [valueProgression] provided, value will
+ * be coerced to this range.
+ * @param modifier Modifier to be applied to the component
+ * @param valueProgression Progression of values that [LevelIndicator] value can take. Consists of
+ * rangeStart, rangeEnd and step. Range will be equally divided by step size
+ * @param enabled Controls the enabled state of [LevelIndicator] - when false, disabled colors will
+ * be used.
+ * @param colors [LevelIndicatorColors] that will be used to resolve the indicator and track colors
+ * for this [LevelIndicator] in different states
+ * @param strokeWidth The stroke width for the indicator and track strokes
+ * @param sweepAngle The angle covered by the curved LevelIndicator
+ * @param reverseDirection Reverses direction of PositionIndicator if true
+ */
+@Composable
+fun LevelIndicator(
+ value: () -> Int,
+ valueProgression: IntProgression,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: LevelIndicatorColors = LevelIndicatorDefaults.colors(),
+ strokeWidth: Dp = LevelIndicatorDefaults.StrokeWidth,
+ sweepAngle: Float = LevelIndicatorDefaults.SweepAngle,
+ reverseDirection: Boolean = false,
+) {
+ LevelIndicator(
+ value = { value().toFloat() },
+ modifier = modifier,
+ valueRange = valueProgression.first.toFloat()..valueProgression.last.toFloat(),
+ enabled = enabled,
+ colors = colors,
+ strokeWidth = strokeWidth,
+ sweepAngle = sweepAngle,
+ reverseDirection = reverseDirection,
+ )
+}
+
+/** Contains the default values used for [LevelIndicator]. */
+object LevelIndicatorDefaults {
+ /**
+ * Creates a [LevelIndicatorColors] that represents the default colors used in a
+ * [LevelIndicator].
+ */
+ @Composable fun colors() = MaterialTheme.colorScheme.defaultLevelIndicatorColors
+
+ /**
+ * Creates a [LevelIndicatorColors] with modified colors used in [LevelIndicator].
+ *
+ * @param indicatorColor The indicator color.
+ * @param trackColor The track color.
+ * @param disabledIndicatorColor The disabled indicator color.
+ * @param disabledTrackColor The disabled track color.
+ */
+ @Composable
+ fun colors(
+ indicatorColor: Color = Color.Unspecified,
+ trackColor: Color = Color.Unspecified,
+ disabledIndicatorColor: Color = Color.Unspecified,
+ disabledTrackColor: Color = Color.Unspecified,
+ ) =
+ MaterialTheme.colorScheme.defaultLevelIndicatorColors.copy(
+ indicatorColor = indicatorColor,
+ trackColor = trackColor,
+ disabledIndicatorColor = disabledIndicatorColor,
+ disabledTrackColor = disabledTrackColor,
+ )
+
+ /** The sweep angle for the curved [LevelIndicator]. */
+ const val SweepAngle = 72f
+
+ /** The default stroke width for the indicator and track strokes */
+ val StrokeWidth = 6.dp
+
+ internal val edgePadding = PaddingDefaults.edgePadding
+
+ private val ColorScheme.defaultLevelIndicatorColors: LevelIndicatorColors
+ get() {
+ return defaultLevelIndicatorColorsCached
+ ?: LevelIndicatorColors(
+ indicatorColor = fromToken(ColorSchemeKeyTokens.SecondaryDim),
+ trackColor = fromToken(ColorSchemeKeyTokens.SurfaceContainer),
+ disabledIndicatorColor =
+ fromToken(ColorSchemeKeyTokens.OnSurface)
+ .toDisabledColor(disabledAlpha = DisabledContentAlpha),
+ disabledTrackColor =
+ fromToken(ColorSchemeKeyTokens.OnSurface)
+ .toDisabledColor(disabledAlpha = DisabledContainerAlpha),
+ )
+ .also { defaultLevelIndicatorColorsCached = it }
+ }
+}
+
+/**
+ * Represents the indicator and track colors used in [LevelIndicator].
+ *
+ * @param indicatorColor Color used to draw the indicator of [LevelIndicator].
+ * @param trackColor Color used to draw the track of [LevelIndicator].
+ * @param disabledIndicatorColor Color used to draw the indicator of [LevelIndicator] when it is not
+ * enabled.
+ * @param disabledTrackColor Color used to draw the track of [LevelIndicator] when it is not
+ * enabled.
+ */
+class LevelIndicatorColors(
+ val indicatorColor: Color,
+ val trackColor: Color,
+ val disabledIndicatorColor: Color,
+ val disabledTrackColor: Color
+) {
+ internal fun copy(
+ indicatorColor: Color? = null,
+ trackColor: Color? = null,
+ disabledIndicatorColor: Color? = null,
+ disabledTrackColor: Color? = null,
+ ) =
+ LevelIndicatorColors(
+ indicatorColor = indicatorColor ?: this.indicatorColor,
+ trackColor = trackColor ?: this.trackColor,
+ disabledIndicatorColor = disabledIndicatorColor ?: this.disabledIndicatorColor,
+ disabledTrackColor = disabledTrackColor ?: this.disabledTrackColor,
+ )
+
+ /**
+ * Represents the indicator color, depending on [enabled].
+ *
+ * @param enabled whether the component is enabled.
+ */
+ internal fun indicatorColor(enabled: Boolean): Color {
+ return if (enabled) indicatorColor else disabledIndicatorColor
+ }
+
+ /**
+ * Represents the track color, depending on [enabled].
+ *
+ * @param enabled whether the component is enabled.
+ */
+ internal fun trackColor(enabled: Boolean): Color {
+ return if (enabled) trackColor else disabledTrackColor
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is LevelIndicatorColors) return false
+
+ if (indicatorColor != other.indicatorColor) return false
+ if (trackColor != other.trackColor) return false
+ if (disabledIndicatorColor != other.disabledIndicatorColor) return false
+ if (disabledTrackColor != other.disabledTrackColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = indicatorColor.hashCode()
+ result = 31 * result + trackColor.hashCode()
+ result = 31 * result + disabledIndicatorColor.hashCode()
+ result = 31 * result + disabledTrackColor.hashCode()
+ return result
+ }
+}
+
+/**
+ * An implementation of [IndicatorState] to display the amount and position of a component
+ * implementing the [LevelIndicator].
+ *
+ * @param valueFraction the value fraction to adapt to a ScrollIndicatorState
+ * @VisibleForTesting
+ */
+internal class FractionPositionStateAdapter(
+ private val valueFraction: () -> Float,
+) : IndicatorState {
+
+ override val positionFraction = 1f // LevelIndicator always starts at the bottom
+
+ override val sizeFraction: Float
+ get() = valueFraction()
+
+ override fun equals(other: Any?): Boolean {
+ // Compare lambdas with referential equality
+ return (other as? FractionPositionStateAdapter)?.valueFraction === valueFraction
+ }
+
+ override fun hashCode(): Int = valueFraction.hashCode()
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
new file mode 100644
index 0000000..c7944ef
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+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.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.graphics.SolidColor
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalAccessibilityManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.DialogProperties
+import androidx.wear.compose.foundation.CurvedDirection
+import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
+import androidx.wear.compose.foundation.CurvedScope
+import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.padding
+import androidx.wear.compose.material3.tokens.ColorSchemeKeyTokens
+import androidx.wear.compose.materialcore.screenHeightDp
+import androidx.wear.compose.materialcore.screenWidthDp
+import kotlinx.coroutines.delay
+
+/**
+ * A full-screen dialog that displays an animated icon with a curved text at the bottom.
+ *
+ * The dialog will be showing a message to the user for [durationMillis]. After a specified timeout,
+ * the [onDismissRequest] callback will be invoked, where it's up to the caller to handle the
+ * dismissal. To hide the dialog, [show] parameter should be set to false.
+ *
+ * This dialog is typically used to indicate that an action has been initiated and will continue on
+ * the user's phone. Once this dialog is displayed, it's developer responsibility to establish the
+ * connection between the watch and the phone.
+ *
+ * Example of an [OpenOnPhoneDialog] usage:
+ *
+ * @sample androidx.wear.compose.material3.samples.OpenOnPhoneDialogSample
+ * @param show A boolean indicating whether the dialog should be displayed.
+ * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
+ * swiping right or when the [durationMillis] has passed.
+ * @param modifier Modifier to be applied to the dialog content.
+ * @param curvedText A slot for displaying curved text content which will be shown along the bottom
+ * edge of the dialog. Defaults to a localized open on phone message.
+ * @param colors [OpenOnPhoneDialogColors] that will be used to resolve the colors used for this
+ * [OpenOnPhoneDialog].
+ * @param properties An optional [DialogProperties] object for configuring the dialog's behavior.
+ * @param durationMillis The duration in milliseconds for which the dialog is displayed. Defaults to
+ * [OpenOnPhoneDialogDefaults.DurationMillis].
+ * @param content A slot for displaying an icon inside the open on phone dialog, which can be
+ * animated. Defaults to [OpenOnPhoneDialogDefaults.Icon].
+ */
+@Composable
+fun OpenOnPhoneDialog(
+ show: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ curvedText: (CurvedScope.() -> Unit)? = OpenOnPhoneDialogDefaults.curvedText(),
+ colors: OpenOnPhoneDialogColors = OpenOnPhoneDialogDefaults.colors(),
+ properties: DialogProperties = DialogProperties(),
+ durationMillis: Long = OpenOnPhoneDialogDefaults.DurationMillis,
+ content: @Composable BoxScope.() -> Unit = OpenOnPhoneDialogDefaults.Icon,
+) {
+ var progress by remember(show) { mutableFloatStateOf(0f) }
+ val animatable = remember { Animatable(0f) }
+
+ val a11yDurationMillis =
+ LocalAccessibilityManager.current?.calculateRecommendedTimeoutMillis(
+ originalTimeoutMillis = durationMillis,
+ containsIcons = true,
+ containsText = curvedText != null,
+ containsControls = false,
+ ) ?: durationMillis
+
+ LaunchedEffect(show, a11yDurationMillis) {
+ if (show) {
+ animatable.snapTo(0f)
+ animatable.animateTo(
+ targetValue = 1f,
+ animationSpec =
+ tween(durationMillis = a11yDurationMillis.toInt(), easing = LinearEasing),
+ ) {
+ progress = value
+ }
+ onDismissRequest()
+ }
+ }
+
+ Dialog(
+ showDialog = show,
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ val bottomPadding =
+ if (curvedText != null)
+ screenHeightDp() * OpenOnPhoneDialogDefaults.ExtraBottomPaddingFraction
+ else 0f
+ Box(
+ Modifier.fillMaxSize().padding(bottom = bottomPadding.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ iconContainer(
+ iconContainerColor = colors.iconContainerColor,
+ progressIndicatorColors =
+ ProgressIndicatorColors(
+ SolidColor(colors.progressIndicatorColor),
+ SolidColor(colors.progressTrackColor)
+ ),
+ progress = { progress }
+ )()
+ CompositionLocalProvider(LocalContentColor provides colors.iconColor) { content() }
+ }
+ CompositionLocalProvider(LocalContentColor provides colors.textColor) {
+ curvedText?.let { CurvedLayout(anchor = 90f, contentBuilder = curvedText) }
+ }
+ }
+ }
+}
+
+/** Contains the default values used by [OpenOnPhoneDialog]. */
+object OpenOnPhoneDialogDefaults {
+
+ /**
+ * A default composable used in [OpenOnPhoneDialog] that displays an open on phone icon with an
+ * animation.
+ */
+ @OptIn(ExperimentalAnimationGraphicsApi::class)
+ val Icon: @Composable BoxScope.() -> Unit = {
+ val animation =
+ AnimatedImageVector.animatedVectorResource(R.drawable.open_on_phone_animation)
+ var atEnd by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ delay(IconDelay)
+ atEnd = true
+ }
+ Icon(
+ painter = rememberAnimatedVectorPainter(animation, atEnd),
+ contentDescription = null,
+ modifier = Modifier.size(IconSize).align(Alignment.Center),
+ )
+ }
+
+ /**
+ * A default composable that displays text along a curved path, used in [OpenOnPhoneDialog].
+ *
+ * @param text The text to display. Defaults to an open on phone message.
+ * @param style The style to apply to the text. Defaults to
+ * CurvedTextStyle(MaterialTheme.typography.titleLarge).
+ */
+ @Composable
+ fun curvedText(
+ text: String = LocalContext.current.resources.getString(R.string.wear_m3c_open_on_phone),
+ style: CurvedTextStyle = CurvedTextStyle(MaterialTheme.typography.titleLarge)
+ ): CurvedScope.() -> Unit = {
+ curvedText(
+ text = text,
+ style = style,
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ modifier = CurvedModifier.padding(PaddingDefaults.edgePadding),
+ angularDirection = CurvedDirection.Angular.Reversed
+ )
+ }
+
+ /**
+ * Creates a [OpenOnPhoneDialogColors] that represents the default colors used in
+ * [OpenOnPhoneDialog].
+ */
+ @Composable fun colors() = MaterialTheme.colorScheme.defaultOpenOnPhoneDialogColors
+
+ /**
+ * Creates a [OpenOnPhoneDialogColors] with modified colors used in [OpenOnPhoneDialog].
+ *
+ * @param iconColor The icon color.
+ * @param iconContainerColor The icon container color.
+ * @param progressIndicatorColor The progress indicator color.
+ * @param progressTrackColor The progress track color.
+ * @param textColor The text color.
+ */
+ @Composable
+ fun colors(
+ iconColor: Color = Color.Unspecified,
+ iconContainerColor: Color = Color.Unspecified,
+ progressIndicatorColor: Color = Color.Unspecified,
+ progressTrackColor: Color = Color.Unspecified,
+ textColor: Color = Color.Unspecified
+ ) =
+ MaterialTheme.colorScheme.defaultOpenOnPhoneDialogColors.copy(
+ iconColor = iconColor,
+ iconContainerColor = iconContainerColor,
+ progressIndicatorColor = progressIndicatorColor,
+ progressTrackColor = progressTrackColor,
+ textColor = textColor
+ )
+
+ /** Default timeout for the [OpenOnPhoneDialog] dialog, in milliseconds. */
+ const val DurationMillis = 4000L
+
+ internal val IconDelay = 67L
+ internal val SizeFraction = 0.6f
+ internal val ExtraBottomPaddingFraction = 0.02f
+ internal val IconSize = 52.dp
+
+ internal val progressIndicatorStrokeWidth = 5.dp
+ internal val progressIndicatorPadding = 5.dp
+
+ private val ColorScheme.defaultOpenOnPhoneDialogColors: OpenOnPhoneDialogColors
+ get() {
+ return mDefaultOpenOnPhoneDialogColorsCached
+ ?: OpenOnPhoneDialogColors(
+ iconColor = fromToken(ColorSchemeKeyTokens.Primary),
+ iconContainerColor = fromToken(ColorSchemeKeyTokens.PrimaryContainer),
+ progressIndicatorColor = fromToken(ColorSchemeKeyTokens.Primary),
+ progressTrackColor = fromToken(ColorSchemeKeyTokens.OnPrimary),
+ textColor = fromToken(ColorSchemeKeyTokens.OnBackground)
+ )
+ .also { mDefaultOpenOnPhoneDialogColorsCached = it }
+ }
+}
+
+/**
+ * Represents the colors used in [OpenOnPhoneDialog].
+ *
+ * @param iconColor Color used to tint the icon.
+ * @param iconContainerColor The color of the container behind the icon.
+ * @param progressIndicatorColor Color used to draw the indicator arc of progress indicator.
+ * @param progressTrackColor Color used to draw the track of progress indicator.
+ * @param textColor Color used to draw the text.
+ */
+class OpenOnPhoneDialogColors(
+ val iconColor: Color,
+ val iconContainerColor: Color,
+ val progressIndicatorColor: Color,
+ val progressTrackColor: Color,
+ val textColor: Color
+) {
+ internal fun copy(
+ iconColor: Color? = null,
+ iconContainerColor: Color? = null,
+ progressIndicatorColor: Color? = null,
+ progressTrackColor: Color? = null,
+ textColor: Color? = null
+ ) =
+ OpenOnPhoneDialogColors(
+ iconColor = iconColor ?: this.iconColor,
+ iconContainerColor = iconContainerColor ?: this.iconContainerColor,
+ progressIndicatorColor = progressIndicatorColor ?: this.progressIndicatorColor,
+ progressTrackColor = progressTrackColor ?: this.progressTrackColor,
+ textColor = textColor ?: this.textColor
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is OpenOnPhoneDialogColors) return false
+
+ if (iconColor != other.iconColor) return false
+ if (iconContainerColor != other.iconContainerColor) return false
+ if (progressIndicatorColor != other.progressIndicatorColor) return false
+ if (progressTrackColor != other.progressTrackColor) return false
+ if (textColor != other.textColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = iconColor.hashCode()
+ result = 31 * result + iconContainerColor.hashCode()
+ result = 31 * result + progressIndicatorColor.hashCode()
+ result = 31 * result + progressTrackColor.hashCode()
+ result = 31 * result + textColor.hashCode()
+ return result
+ }
+}
+
+private fun iconContainer(
+ iconContainerColor: Color,
+ progressIndicatorColors: ProgressIndicatorColors,
+ progress: () -> Float
+): @Composable BoxScope.() -> Unit = {
+ val size = screenWidthDp() * OpenOnPhoneDialogDefaults.SizeFraction
+ Box(Modifier.size(size.dp)) {
+ Box(
+ Modifier.fillMaxSize()
+ .padding(
+ OpenOnPhoneDialogDefaults.progressIndicatorStrokeWidth +
+ OpenOnPhoneDialogDefaults.progressIndicatorPadding
+ )
+ .graphicsLayer {
+ shape = CircleShape
+ clip = true
+ }
+ .background(iconContainerColor)
+ )
+
+ CircularProgressIndicator(
+ progress = progress,
+ strokeWidth = OpenOnPhoneDialogDefaults.progressIndicatorStrokeWidth,
+ colors = progressIndicatorColors
+ )
+ }
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt
index e5c965c..1eb3d51 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Picker.kt
@@ -30,7 +30,10 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
@@ -59,6 +62,7 @@
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -546,4 +550,31 @@
get() = pickerState.selectedOption
}
+internal fun pickerTextOption(
+ textStyle: TextStyle,
+ indexToText: (Int) -> String,
+ optionHeight: Dp,
+ selectedContentColor: Color,
+ unselectedContentColor: Color,
+): (@Composable PickerScope.(optionIndex: Int, pickerSelected: Boolean) -> Unit) =
+ { value: Int, pickerSelected: Boolean ->
+ Box(
+ modifier = Modifier.fillMaxSize().height(optionHeight),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = indexToText(value),
+ maxLines = 1,
+ style = textStyle,
+ color =
+ if (pickerSelected) {
+ selectedContentColor
+ } else {
+ unselectedContentColor
+ },
+ modifier = Modifier.align(Alignment.Center).wrapContentSize(),
+ )
+ }
+ }
+
private const val LARGE_NUMBER_OF_ITEMS = 100_000_000
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollAway.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollAway.kt
index 7239dbe..bfc37da 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollAway.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollAway.kt
@@ -20,6 +20,7 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.tween
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.layout.Measurable
@@ -62,25 +63,38 @@
* [ScreenStage] represents the different stages for a screen, which affect visibility of scaffold
* components such as [TimeText] and [ScrollIndicator] with [scrollAway] and other animations.
*/
-enum class ScreenStage {
- /**
- * Initial stage for a screen when first displayed. It is expected that the [TimeText] and
- * [ScrollIndicator] are displayed when initially showing a screen.
- */
- New,
+@Immutable
+@JvmInline
+value class ScreenStage internal constructor(internal val value: Int) {
+ companion object {
+ /**
+ * Initial stage for a screen when first displayed. It is expected that the [TimeText] and
+ * [ScrollIndicator] are displayed when initially showing a screen.
+ */
+ val New = ScreenStage(0)
- /**
- * Stage when both the screen is not scrolling and some time has passed after the screen was
- * initially shown. At this stage, the [TimeText] is expected to be displayed and the
- * [ScrollIndicator] will be hidden.
- */
- Idle,
+ /**
+ * Stage when both the screen is not scrolling and some time has passed after the screen was
+ * initially shown. At this stage, the [TimeText] is expected to be displayed and the
+ * [ScrollIndicator] will be hidden.
+ */
+ val Idle = ScreenStage(1)
- /**
- * Stage when the screen is being scrolled. At this stage, it is expected that the
- * [ScrollIndicator] will be shown and [TimeText] will be scrolled away by the scroll operation.
- */
- Scrolling
+ /**
+ * Stage when the screen is being scrolled. At this stage, it is expected that the
+ * [ScrollIndicator] will be shown and [TimeText] will be scrolled away by the scroll
+ * operation.
+ */
+ val Scrolling = ScreenStage(2)
+ }
+
+ override fun toString() =
+ when (this) {
+ New -> "New"
+ Idle -> "Idle"
+ Scrolling -> "Scrolling"
+ else -> "Unknown"
+ }
}
private data class ScrollAwayModifierElement(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollIndicator.kt
index e091865..88c8e01 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScrollIndicator.kt
@@ -98,9 +98,8 @@
* `Modifier.align(Alignment.CenterEnd)`.
* @param reverseDirection Reverses direction of ScrollIndicator if true
* @param positionAnimationSpec [AnimationSpec] for position animation. The Position animation is
- * used for animating changes between state.positionFraction and state.sizeFraction of
- * [ScrollIndicatorState]. To disable this animation [snap] AnimationSpec should be passed
- * instead.
+ * used for animating changes to the scroll size and position. To disable this animation [snap]
+ * AnimationSpec should be passed instead.
*/
@Composable
fun ScrollIndicator(
@@ -111,7 +110,7 @@
) {
var containerSize by remember { mutableStateOf(IntSize.Zero) }
- ScrollIndicatorImpl(
+ IndicatorImpl(
remember { ScrollStateAdapter(state) { containerSize } },
indicatorHeight = ScrollIndicatorDefaults.indicatorHeight,
indicatorWidth = ScrollIndicatorDefaults.indicatorWidth,
@@ -147,9 +146,8 @@
* @param modifier The modifier to be applied to the component
* @param reverseDirection Reverses direction of ScrollIndicator if true
* @param positionAnimationSpec [AnimationSpec] for position animation. The Position animation is
- * used for animating changes between state.positionFraction and state.sizeFraction of
- * [ScrollIndicatorState]. To disable this animation [snap] AnimationSpec should be passed
- * instead.
+ * used for animating changes to the scroll size and position. To disable this animation [snap]
+ * AnimationSpec should be passed instead.
*/
@Composable
fun ScrollIndicator(
@@ -158,7 +156,7 @@
reverseDirection: Boolean = false,
positionAnimationSpec: AnimationSpec<Float> = ScrollIndicatorDefaults.PositionAnimationSpec
) =
- ScrollIndicatorImpl(
+ IndicatorImpl(
state = ScalingLazyColumnStateAdapter(state = state),
indicatorHeight = ScrollIndicatorDefaults.indicatorHeight,
indicatorWidth = ScrollIndicatorDefaults.indicatorWidth,
@@ -191,9 +189,8 @@
* @param modifier The modifier to be applied to the component
* @param reverseDirection Reverses direction of ScrollIndicator if true
* @param positionAnimationSpec [AnimationSpec] for position animation. The Position animation is
- * used for animating changes between state.positionFraction and state.sizeFraction of
- * [ScrollIndicatorState]. To disable this animation [snap] AnimationSpec should be passed
- * instead.
+ * used for animating changes to the scroll size and position. To disable this animation [snap]
+ * AnimationSpec should be passed instead.
*/
@Composable
fun ScrollIndicator(
@@ -202,7 +199,7 @@
reverseDirection: Boolean = false,
positionAnimationSpec: AnimationSpec<Float> = ScrollIndicatorDefaults.PositionAnimationSpec
) =
- ScrollIndicatorImpl(
+ IndicatorImpl(
state = LazyColumnStateAdapter(state = state),
indicatorHeight = ScrollIndicatorDefaults.indicatorHeight,
indicatorWidth = ScrollIndicatorDefaults.indicatorWidth,
@@ -241,7 +238,7 @@
* of 5 / 50 = 0.1f to indicate that 10% of the visible items are currently visible.
*/
@Stable
-internal interface ScrollIndicatorState {
+internal interface IndicatorState {
/**
* Position of the indicator in the range [0f,1f]. 0f means it is at the top|start, 1f means it
* is positioned at the bottom|end.
@@ -253,7 +250,7 @@
}
/**
- * An indicator on one side of the screen to show the current [ScrollIndicatorState].
+ * An indicator on one side of the screen to show the current [IndicatorState].
*
* Typically used with the [ScreenScaffold] but can be used to decorate any full screen situation.
*
@@ -270,7 +267,7 @@
* For more information, see the
* [Scroll indicators](https://developer.android.com/training/wearables/components/scroll) guide.
*
- * @param state the [ScrollIndicatorState] of the state we are displaying.
+ * @param state the [IndicatorState] of the state we are displaying.
* @param indicatorHeight the height of the position indicator in Dp.
* @param indicatorWidth the width of the position indicator in Dp.
* @param paddingHorizontal the padding to apply between the indicator and the border of the screen.
@@ -279,12 +276,12 @@
* @param color the color to draw the active part of the indicator in.
* @param reverseDirection Reverses direction of ScrollIndicator if true.
* @param positionAnimationSpec [AnimationSpec] for position animation. The Position animation is
- * used for animating changes between state.positionFraction and state.sizeFraction of
- * [ScrollIndicatorState]. To disable animation [snap] should be passed.
+ * used for animating changes to the scroll size and position. To disable this animation [snap]
+ * AnimationSpec should be passed instead.
*/
@Composable
-internal fun ScrollIndicatorImpl(
- state: ScrollIndicatorState,
+internal fun IndicatorImpl(
+ state: IndicatorState,
indicatorHeight: Dp,
indicatorWidth: Dp,
paddingHorizontal: Dp,
@@ -292,6 +289,7 @@
background: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f),
color: Color = MaterialTheme.colorScheme.onBackground,
reverseDirection: Boolean = false,
+ rsbSide: Boolean = true,
positionAnimationSpec: AnimationSpec<Float> = ScrollIndicatorDefaults.PositionAnimationSpec
) {
val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
@@ -302,7 +300,10 @@
val positionFractionAnimatable = remember { Animatable(0f) }
val sizeFractionAnimatable = remember { Animatable(0f) }
- val indicatorOnTheRight = layoutDirection == LayoutDirection.Ltr
+ // TODO(b/360358568) - consider taking lefty-mode into account for orientation.
+ val indicatorOnTheRight =
+ if (rsbSide) layoutDirection == LayoutDirection.Ltr
+ else layoutDirection == LayoutDirection.Rtl
val size: () -> DpSize = {
// radius is the distance from the center of the container to the arc we draw the
@@ -452,7 +453,7 @@
}
/**
- * An implementation of [ScrollIndicatorState] to display the amount and position of a component
+ * An implementation of [IndicatorState] to display the amount and position of a component
* implementing the [ScrollState] class such as a [Column] implementing [Modifier.verticalScroll].
*
* @param scrollState the [ScrollState] to adapt
@@ -461,7 +462,7 @@
internal class ScrollStateAdapter(
private val scrollState: ScrollState,
private val scrollableContainerSize: () -> IntSize
-) : ScrollIndicatorState {
+) : IndicatorState {
override val positionFraction: Float
get() {
@@ -493,8 +494,8 @@
}
/**
- * An implementation of [ScrollIndicatorState] to display the amount and position of a
- * [ScalingLazyColumn] component via its [ScalingLazyListState].
+ * An implementation of [IndicatorState] to display the amount and position of a [ScalingLazyColumn]
+ * component via its [ScalingLazyListState].
*
* Note that size and position calculations ignore spacing between list items both for determining
* the number and the number of visible items.
@@ -503,7 +504,7 @@
* @VisibleForTesting
*/
internal class ScalingLazyColumnStateAdapter(private val state: ScalingLazyListState) :
- ScrollIndicatorState {
+ IndicatorState {
private var currentSizeFraction: Float = 0f
private var previousItemsCount: Int = 0
override val positionFraction: Float
@@ -612,13 +613,13 @@
}
/**
- * An implementation of [ScrollIndicatorState] to display the amount and position of a [LazyColumn]
+ * An implementation of [IndicatorState] to display the amount and position of a [LazyColumn]
* component via its [LazyListState].
*
* @param state the [LazyListState] to adapt.
* @VisibleForTesting
*/
-internal class LazyColumnStateAdapter(private val state: LazyListState) : ScrollIndicatorState {
+internal class LazyColumnStateAdapter(private val state: LazyListState) : IndicatorState {
private var latestSizeFraction: Float = 0f
private var previousItemsCount: Int = 0
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Stepper.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Stepper.kt
index cc4ee4d..b64886a 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Stepper.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Stepper.kt
@@ -33,12 +33,12 @@
* 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. If [value] is not equal to any step value, then it will be
- * coerced to the closest step value. However, the [value] itself will not be changed and
- * [onValueChange] in this case will not be triggered. To add range semantics on Stepper, use
+ * [LevelIndicator] if required. If [value] is not equal to any step value, then it will be coerced
+ * to the closest step value. However, the [value] itself will not be changed and [onValueChange] in
+ * this case will not be triggered. To add range semantics on Stepper, use
* [Modifier.rangeSemantics].
*
- * Example of a simple [Stepper]:
+ * Example of a simple [Stepper] with [LevelIndicator]:
*
* @sample androidx.wear.compose.material3.samples.StepperSample
*
@@ -106,8 +106,7 @@
* either [Text] or [Button]) 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. To add range semantics on Stepper, use
- * [Modifier.rangeSemantics].
+ * [LevelIndicator] if required. To add range semantics on Stepper, use [Modifier.rangeSemantics].
*
* Example of a [Stepper] with integer values:
*
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
index c559aa5..20e8047 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
@@ -16,6 +16,8 @@
package androidx.wear.compose.material3
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
@@ -28,16 +30,19 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
import androidx.wear.compose.material3.tokens.FilledTextButtonTokens
import androidx.wear.compose.material3.tokens.FilledTonalTextButtonTokens
+import androidx.wear.compose.material3.tokens.MotionTokens
import androidx.wear.compose.material3.tokens.OutlinedTextButtonTokens
import androidx.wear.compose.material3.tokens.ShapeTokens
import androidx.wear.compose.material3.tokens.TextButtonTokens
import androidx.wear.compose.material3.tokens.TextToggleButtonTokens
+import androidx.wear.compose.materialcore.animateSelectionColor
/**
* Wear Material [TextButton] is a circular, text-only button with transparent background and no
@@ -170,7 +175,7 @@
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: ToggleButtonColors = TextButtonDefaults.textToggleButtonColors(),
+ colors: TextToggleButtonColors = TextButtonDefaults.textToggleButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = TextButtonDefaults.shape,
border: BorderStroke? = null,
@@ -398,7 +403,7 @@
)
/**
- * Creates a [ToggleButtonColors] for a [TextToggleButton]
+ * Creates a [TextToggleButtonColors] for a [TextToggleButton]
* - by default, a colored background with a contrasting content color. If the button is
* disabled, then the colors will have an alpha ([DisabledContainerAlpha] or
* [DisabledContentAlpha]) value applied.
@@ -407,7 +412,7 @@
fun textToggleButtonColors() = MaterialTheme.colorScheme.defaultTextToggleButtonColors
/**
- * Creates a [ToggleButtonColors] for a [TextToggleButton]
+ * Creates a [TextToggleButtonColors] for a [TextToggleButton]
* - by default, a colored background with a contrasting content color. If the button is
* disabled, then the colors will have an alpha ([DisabledContainerAlpha] or
* [DisabledContentAlpha]) value applied.
@@ -439,7 +444,7 @@
disabledCheckedContentColor: Color = Color.Unspecified,
disabledUncheckedContainerColor: Color = Color.Unspecified,
disabledUncheckedContentColor: Color = Color.Unspecified,
- ): ToggleButtonColors =
+ ): TextToggleButtonColors =
MaterialTheme.colorScheme.defaultTextToggleButtonColors.copy(
checkedContainerColor = checkedContainerColor,
checkedContentColor = checkedContentColor,
@@ -575,10 +580,10 @@
.also { defaultTextButtonColorsCached = it }
}
- private val ColorScheme.defaultTextToggleButtonColors: ToggleButtonColors
+ private val ColorScheme.defaultTextToggleButtonColors: TextToggleButtonColors
get() {
return defaultTextToggleButtonColorsCached
- ?: ToggleButtonColors(
+ ?: TextToggleButtonColors(
checkedContainerColor =
fromToken(TextToggleButtonTokens.CheckedContainerColor),
checkedContentColor = fromToken(TextToggleButtonTokens.CheckedContentColor),
@@ -691,3 +696,129 @@
return result
}
}
+
+/**
+ * Represents the different container and content colors used for [TextToggleButton] in various
+ * states, that are checked, unchecked, enabled and disabled.
+ *
+ * @param checkedContainerColor Container or background color when the toggle button is checked
+ * @param checkedContentColor Color of the content (text or icon) when the toggle button is checked
+ * @param uncheckedContainerColor Container or background color when the toggle button is unchecked
+ * @param uncheckedContentColor Color of the content (text or icon) when the toggle button is
+ * unchecked
+ * @param disabledCheckedContainerColor Container or background color when the toggle button is
+ * disabled and checked
+ * @param disabledCheckedContentColor Color of content (text or icon) when the toggle button is
+ * disabled and checked
+ * @param disabledUncheckedContainerColor Container or background color when the toggle button is
+ * disabled and unchecked
+ * @param disabledUncheckedContentColor Color of the content (text or icon) when the toggle button
+ * is disabled and unchecked
+ */
+@Immutable
+class TextToggleButtonColors(
+ val checkedContainerColor: Color,
+ val checkedContentColor: Color,
+ val uncheckedContainerColor: Color,
+ val uncheckedContentColor: Color,
+ val disabledCheckedContainerColor: Color,
+ val disabledCheckedContentColor: Color,
+ val disabledUncheckedContainerColor: Color,
+ val disabledUncheckedContentColor: Color,
+) {
+ internal fun copy(
+ checkedContainerColor: Color,
+ checkedContentColor: Color,
+ uncheckedContainerColor: Color,
+ uncheckedContentColor: Color,
+ disabledCheckedContainerColor: Color,
+ disabledCheckedContentColor: Color,
+ disabledUncheckedContainerColor: Color,
+ disabledUncheckedContentColor: Color,
+ ): TextToggleButtonColors =
+ TextToggleButtonColors(
+ checkedContainerColor = checkedContainerColor.takeOrElse { this.checkedContainerColor },
+ checkedContentColor = checkedContentColor.takeOrElse { this.checkedContentColor },
+ uncheckedContainerColor =
+ uncheckedContainerColor.takeOrElse { this.uncheckedContainerColor },
+ uncheckedContentColor = uncheckedContentColor.takeOrElse { this.uncheckedContentColor },
+ disabledCheckedContainerColor =
+ disabledCheckedContainerColor.takeOrElse { this.disabledCheckedContainerColor },
+ disabledCheckedContentColor =
+ disabledCheckedContentColor.takeOrElse { this.disabledCheckedContentColor },
+ disabledUncheckedContainerColor =
+ disabledUncheckedContainerColor.takeOrElse { this.disabledUncheckedContainerColor },
+ disabledUncheckedContentColor =
+ disabledUncheckedContentColor.takeOrElse { this.disabledUncheckedContentColor },
+ )
+
+ /**
+ * Determines the container color based on whether the toggle button is [enabled] and [checked].
+ *
+ * @param enabled Whether the toggle button is enabled
+ * @param checked Whether the toggle button is checked
+ */
+ @Composable
+ internal fun containerColor(enabled: Boolean, checked: Boolean): State<Color> =
+ animateSelectionColor(
+ enabled = enabled,
+ checked = checked,
+ checkedColor = checkedContainerColor,
+ uncheckedColor = uncheckedContainerColor,
+ disabledCheckedColor = disabledCheckedContainerColor,
+ disabledUncheckedColor = disabledUncheckedContainerColor,
+ animationSpec = COLOR_ANIMATION_SPEC
+ )
+
+ /**
+ * Determines the content color based on whether the toggle button is [enabled] and [checked].
+ *
+ * @param enabled Whether the toggle button is enabled
+ * @param checked Whether the toggle button is checked
+ */
+ @Composable
+ internal fun contentColor(enabled: Boolean, checked: Boolean): State<Color> =
+ animateSelectionColor(
+ enabled = enabled,
+ checked = checked,
+ checkedColor = checkedContentColor,
+ uncheckedColor = uncheckedContentColor,
+ disabledCheckedColor = disabledCheckedContentColor,
+ disabledUncheckedColor = disabledUncheckedContentColor,
+ animationSpec = COLOR_ANIMATION_SPEC
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null) return false
+ if (this::class != other::class) return false
+
+ other as TextToggleButtonColors
+
+ if (checkedContainerColor != other.checkedContainerColor) return false
+ if (checkedContentColor != other.checkedContentColor) return false
+ if (uncheckedContainerColor != other.uncheckedContainerColor) return false
+ if (uncheckedContentColor != other.uncheckedContentColor) return false
+ if (disabledCheckedContainerColor != other.disabledCheckedContainerColor) return false
+ if (disabledCheckedContentColor != other.disabledCheckedContentColor) return false
+ if (disabledUncheckedContainerColor != other.disabledUncheckedContainerColor) return false
+ if (disabledUncheckedContentColor != other.disabledUncheckedContentColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = checkedContainerColor.hashCode()
+ result = 31 * result + checkedContentColor.hashCode()
+ result = 31 * result + uncheckedContainerColor.hashCode()
+ result = 31 * result + uncheckedContentColor.hashCode()
+ result = 31 * result + disabledCheckedContainerColor.hashCode()
+ result = 31 * result + disabledCheckedContentColor.hashCode()
+ result = 31 * result + disabledUncheckedContainerColor.hashCode()
+ result = 31 * result + disabledUncheckedContentColor.hashCode()
+ return result
+ }
+}
+
+private val COLOR_ANIMATION_SPEC: AnimationSpec<Color> =
+ tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
index 9975891..a6c2b49 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
@@ -38,7 +38,6 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -60,7 +59,6 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
@@ -223,8 +221,9 @@
option =
pickerTextOption(
textStyle = styles.optionTextStyle,
- selectedPickerColor = colors.selectedPickerContentColor,
- unselectedPickerColor = colors.unselectedPickerContentColor,
+ selectedContentColor = colors.selectedPickerContentColor,
+ unselectedContentColor =
+ colors.unselectedPickerContentColor,
indexToText = {
"%02d".format(if (is12hour) it + 1 else it)
},
@@ -250,8 +249,9 @@
pickerTextOption(
textStyle = styles.optionTextStyle,
indexToText = { "%02d".format(it) },
- selectedPickerColor = colors.selectedPickerContentColor,
- unselectedPickerColor = colors.unselectedPickerContentColor,
+ selectedContentColor = colors.selectedPickerContentColor,
+ unselectedContentColor =
+ colors.unselectedPickerContentColor,
optionHeight = styles.optionHeight,
),
spacing = styles.optionSpacing
@@ -274,8 +274,9 @@
pickerTextOption(
textStyle = styles.optionTextStyle,
indexToText = thirdPicker.indexToText,
- selectedPickerColor = colors.selectedPickerContentColor,
- unselectedPickerColor = colors.unselectedPickerContentColor,
+ selectedContentColor = colors.selectedPickerContentColor,
+ unselectedContentColor =
+ colors.unselectedPickerContentColor,
optionHeight = styles.optionHeight,
),
spacing = styles.optionSpacing
@@ -690,33 +691,6 @@
}
}
-private fun pickerTextOption(
- textStyle: TextStyle,
- selectedPickerColor: Color,
- unselectedPickerColor: Color,
- indexToText: (Int) -> String,
- optionHeight: Dp,
-): (@Composable PickerScope.(optionIndex: Int, pickerSelected: Boolean) -> Unit) =
- { value: Int, pickerSelected: Boolean ->
- Box(
- modifier = Modifier.fillMaxSize().height(optionHeight),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = indexToText(value),
- maxLines = 1,
- style = textStyle,
- color =
- if (pickerSelected) {
- selectedPickerColor
- } else {
- unselectedPickerColor
- },
- modifier = Modifier.align(Alignment.Center).wrapContentSize(),
- )
- }
- }
-
@Composable
private fun createDescription(
pickerGroupState: PickerGroupState,
@@ -729,19 +703,6 @@
else -> getPlurals(plurals, selectedValue, selectedValue)
}
-@Composable
-private fun FontScaleIndependent(content: @Composable () -> Unit) {
- CompositionLocalProvider(
- value =
- LocalDensity provides
- Density(
- density = LocalDensity.current.density,
- fontScale = 1f,
- ),
- content = content
- )
-}
-
private enum class FocusableElementsTimePicker(val index: Int) {
HOURS(0),
MINUTES(1),
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
index 39436f5..5702c0e 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimeText.kt
@@ -22,12 +22,15 @@
import android.content.IntentFilter
import android.text.format.DateFormat
import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -42,6 +45,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
@@ -50,11 +54,13 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.wear.compose.foundation.ArcPaddingValues
+import androidx.wear.compose.foundation.CurvedAlignment
import androidx.wear.compose.foundation.CurvedDirection
import androidx.wear.compose.foundation.CurvedLayout
import androidx.wear.compose.foundation.CurvedModifier
import androidx.wear.compose.foundation.CurvedScope
import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.background
import androidx.wear.compose.foundation.curvedComposable
import androidx.wear.compose.foundation.curvedRow
import androidx.wear.compose.foundation.padding
@@ -91,10 +97,6 @@
* A [TimeText] with a short app status message shown:
*
* @sample androidx.wear.compose.material3.samples.TimeTextWithStatus
- *
- * An example of a [TimeText] with an icon along with the clock:
- *
- * @sample androidx.wear.compose.material3.samples.TimeTextWithIcon
* @param modifier The modifier to be applied to the component.
* @param curvedModifier The [CurvedModifier] used to restrict the arc in which [TimeText] is drawn.
* @param maxSweepAngle The default maximum sweep angle in degrees.
@@ -117,6 +119,7 @@
content: TimeTextScope.() -> Unit
) {
val timeText = timeSource.currentTime()
+ val backgroundColor = CurvedTextDefaults.backgroundColor()
if (isRoundDevice()) {
CurvedLayout(modifier = modifier) {
@@ -125,6 +128,8 @@
curvedModifier
.sizeIn(maxSweepDegrees = maxSweepAngle)
.padding(contentPadding.toArcPadding())
+ .background(backgroundColor, StrokeCap.Round),
+ radialAlignment = CurvedAlignment.Radial.Center
) {
CurvedTimeTextScope(timeText, timeTextStyle, maxSweepAngle, contentColor).apply {
content()
@@ -133,14 +138,19 @@
}
}
} else {
- Row(
- modifier = modifier.fillMaxSize().padding(contentPadding),
- verticalAlignment = Alignment.Top,
- horizontalArrangement = Arrangement.Center
- ) {
- LinearTimeTextScope(timeText, timeTextStyle, contentColor).apply {
- content()
- Show()
+ Box(modifier.fillMaxSize()) {
+ Row(
+ modifier =
+ Modifier.align(Alignment.TopCenter)
+ .background(backgroundColor, CircleShape)
+ .padding(contentPadding),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ LinearTimeTextScope(timeText, timeTextStyle, contentColor).apply {
+ content()
+ Show()
+ }
}
}
}
@@ -179,9 +189,6 @@
* Adds a composable in content of [TimeText]. This can be used to display non-text information
* such as an icon.
*
- * An example of a [TimeText] with an icon along with the clock:
- *
- * @sample androidx.wear.compose.material3.samples.TimeTextWithIcon
* @param content Slot for the [composable] to be displayed.
*/
abstract fun composable(content: @Composable () -> Unit)
@@ -387,11 +394,11 @@
overflow = TextOverflow.Ellipsis,
style = contentTextStyle.merge(style),
modifier =
- if (weight.isValidWeight()) Modifier.weight(weight)
+ if (weight.isValidWeight()) Modifier.weight(weight, fill = false)
// Note that we are creating a lambda here, but textCount is actually read
// later, during the call to Show, when the pending list is fully constructed.
else if (weight == TimeTextDefaults.AutoTextWeight && textCount <= 1)
- Modifier.weight(1f)
+ Modifier.weight(1f, fill = false)
else Modifier
)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
deleted file mode 100644
index cc9cc49..0000000
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ToggleButton.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.tween
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.State
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.takeOrElse
-import androidx.wear.compose.material3.tokens.MotionTokens
-import androidx.wear.compose.materialcore.animateSelectionColor
-
-/**
- * Represents the different container and content colors used for [IconToggleButton] and
- * [TextToggleButton]) in various states, that are checked, unchecked, enabled and disabled.
- *
- * @param checkedContainerColor Container or background color when the toggle button is checked
- * @param checkedContentColor Color of the content (text or icon) when the toggle button is checked
- * @param uncheckedContainerColor Container or background color when the toggle button is unchecked
- * @param uncheckedContentColor Color of the content (text or icon) when the toggle button is
- * unchecked
- * @param disabledCheckedContainerColor Container or background color when the toggle button is
- * disabled and checked
- * @param disabledCheckedContentColor Color of content (text or icon) when the toggle button is
- * disabled and checked
- * @param disabledUncheckedContainerColor Container or background color when the toggle button is
- * disabled and unchecked
- * @param disabledUncheckedContentColor Color of the content (text or icon) when the toggle button
- * is disabled and unchecked
- */
-@Immutable
-class ToggleButtonColors(
- val checkedContainerColor: Color,
- val checkedContentColor: Color,
- val uncheckedContainerColor: Color,
- val uncheckedContentColor: Color,
- val disabledCheckedContainerColor: Color,
- val disabledCheckedContentColor: Color,
- val disabledUncheckedContainerColor: Color,
- val disabledUncheckedContentColor: Color,
-) {
- internal fun copy(
- checkedContainerColor: Color,
- checkedContentColor: Color,
- uncheckedContainerColor: Color,
- uncheckedContentColor: Color,
- disabledCheckedContainerColor: Color,
- disabledCheckedContentColor: Color,
- disabledUncheckedContainerColor: Color,
- disabledUncheckedContentColor: Color,
- ): ToggleButtonColors =
- ToggleButtonColors(
- checkedContainerColor = checkedContainerColor.takeOrElse { this.checkedContainerColor },
- checkedContentColor = checkedContentColor.takeOrElse { this.checkedContentColor },
- uncheckedContainerColor =
- uncheckedContainerColor.takeOrElse { this.uncheckedContainerColor },
- uncheckedContentColor = uncheckedContentColor.takeOrElse { this.uncheckedContentColor },
- disabledCheckedContainerColor =
- disabledCheckedContainerColor.takeOrElse { this.disabledCheckedContainerColor },
- disabledCheckedContentColor =
- disabledCheckedContentColor.takeOrElse { this.disabledCheckedContentColor },
- disabledUncheckedContainerColor =
- disabledUncheckedContainerColor.takeOrElse { this.disabledUncheckedContainerColor },
- disabledUncheckedContentColor =
- disabledUncheckedContentColor.takeOrElse { this.disabledUncheckedContentColor },
- )
-
- /**
- * Determines the container color based on whether the toggle button is [enabled] and [checked].
- *
- * @param enabled Whether the toggle button is enabled
- * @param checked Whether the toggle button is checked
- */
- @Composable
- internal fun containerColor(enabled: Boolean, checked: Boolean): State<Color> =
- animateSelectionColor(
- enabled = enabled,
- checked = checked,
- checkedColor = checkedContainerColor,
- uncheckedColor = uncheckedContainerColor,
- disabledCheckedColor = disabledCheckedContainerColor,
- disabledUncheckedColor = disabledUncheckedContainerColor,
- animationSpec = COLOR_ANIMATION_SPEC
- )
-
- /**
- * Determines the content color based on whether the toggle button is [enabled] and [checked].
- *
- * @param enabled Whether the toggle button is enabled
- * @param checked Whether the toggle button is checked
- */
- @Composable
- internal fun contentColor(enabled: Boolean, checked: Boolean): State<Color> =
- animateSelectionColor(
- enabled = enabled,
- checked = checked,
- checkedColor = checkedContentColor,
- uncheckedColor = uncheckedContentColor,
- disabledCheckedColor = disabledCheckedContentColor,
- disabledUncheckedColor = disabledUncheckedContentColor,
- animationSpec = COLOR_ANIMATION_SPEC
- )
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null) return false
- if (this::class != other::class) return false
-
- other as ToggleButtonColors
-
- if (checkedContainerColor != other.checkedContainerColor) return false
- if (checkedContentColor != other.checkedContentColor) return false
- if (uncheckedContainerColor != other.uncheckedContainerColor) return false
- if (uncheckedContentColor != other.uncheckedContentColor) return false
- if (disabledCheckedContainerColor != other.disabledCheckedContainerColor) return false
- if (disabledCheckedContentColor != other.disabledCheckedContentColor) return false
- if (disabledUncheckedContainerColor != other.disabledUncheckedContainerColor) return false
- if (disabledUncheckedContentColor != other.disabledUncheckedContentColor) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = checkedContainerColor.hashCode()
- result = 31 * result + checkedContentColor.hashCode()
- result = 31 * result + uncheckedContainerColor.hashCode()
- result = 31 * result + uncheckedContentColor.hashCode()
- result = 31 * result + disabledCheckedContainerColor.hashCode()
- result = 31 * result + disabledCheckedContentColor.hashCode()
- result = 31 * result + disabledUncheckedContainerColor.hashCode()
- result = 31 * result + disabledUncheckedContentColor.hashCode()
- return result
- }
-}
-
-private val COLOR_ANIMATION_SPEC: AnimationSpec<Color> =
- tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt
index 4edf6fd..478d030 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/internal/Strings.kt
@@ -65,8 +65,20 @@
inline val TimePickerPeriod
get() = Strings(R.string.wear_m3c_time_picker_period)
+ inline val DatePickerYear
+ get() = Strings(R.string.wear_m3c_date_picker_year)
+
+ inline val DatePickerMonth
+ get() = Strings(R.string.wear_m3c_date_picker_month)
+
+ inline val DatePickerDay
+ get() = Strings(R.string.wear_m3c_date_picker_day)
+
inline val PickerConfirmButtonContentDescription
get() = Strings(R.string.wear_m3c_picker_confirm_button_content_description)
+
+ inline val PickerNextButtonContentDescription
+ get() = Strings(R.string.wear_m3c_picker_next_button_content_description)
}
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/DatePickerTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/DatePickerTokens.kt
new file mode 100644
index 0000000..9c53ae8
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/DatePickerTokens.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.tokens
+
+internal object DatePickerTokens {
+ val SelectedPickerContentColor = ColorSchemeKeyTokens.OnBackground
+ val UnselectedPickerContentColor = ColorSchemeKeyTokens.SecondaryDim
+ val PickerLabelColor = ColorSchemeKeyTokens.Primary
+ val NextButtonContentColor = ColorSchemeKeyTokens.Primary
+ val NextButtonContainerColor = ColorSchemeKeyTokens.SurfaceContainer
+ val ConfirmButtonContentColor = ColorSchemeKeyTokens.OnPrimary
+ val ConfirmButtonContainerColor = ColorSchemeKeyTokens.PrimaryDim
+
+ val PickerLabelLargeTypography = TypographyKeyTokens.TitleLarge
+ val PickerLabelTypography = TypographyKeyTokens.TitleMedium
+ val PickerContentLargeTypography = TypographyKeyTokens.NumeralMedium
+ val PickerContentTypography = TypographyKeyTokens.NumeralSmall
+}
diff --git a/wear/compose/compose-material3/src/main/res/drawable/check_animation.xml b/wear/compose/compose-material3/src/main/res/drawable/check_animation.xml
new file mode 100644
index 0000000..90fd02a
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/drawable/check_animation.xml
@@ -0,0 +1,17 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="300dp" android:width="400dp" android:viewportHeight="300" android:viewportWidth="400"><group android:name="_R_G"><group android:name="_R_G_L_0_G" android:translateX="200" android:translateY="176" android:scaleX="1.05507" android:scaleY="1.05556"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="34" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M-147.5 -20 C-147.5,-20 -51.5,76 -51.5,76 C-51.5,76 151.5,-127 151.5,-127 "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="250" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="10017" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/main/res/drawable/failure_animation.xml b/wear/compose/compose-material3/src/main/res/drawable/failure_animation.xml
new file mode 100644
index 0000000..2c5120b
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/drawable/failure_animation.xml
@@ -0,0 +1,17 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="800dp" android:width="800dp" android:viewportHeight="800" android:viewportWidth="800"><group android:name="_R_G"><group android:name="_R_G_L_2_G" android:translateX="414" android:translateY="398" android:scaleX="10.57" android:scaleY="10.57"><path android:name="_R_G_L_2_G_D_0_P_0" android:fillColor="#ff8986" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-14.49 17.77 C-14.49,17.77 -14.49,19.61 -14.49,19.61 C-14.49,19.72 -14.45,19.81 -14.38,19.88 C-14.31,19.95 -14.22,19.99 -14.11,19.99 C-14.11,19.99 7.78,19.99 7.78,19.99 C7.88,19.99 7.97,19.95 8.05,19.88 C8.12,19.81 8.15,19.72 8.15,19.61 C8.15,19.61 8.15,17.77 8.15,17.77 C8.15,17.77 -14.49,17.77 -14.49,17.77c M-14.49 -17.77 C-14.49,-17.77 8.15,-17.77 8.15,-17.77 C8.15,-17.77 8.15,-19.61 8.15,-19.61 C8.15,-19.72 8.12,-19.81 8.05,-19.88 C7.97,-19.95 7.88,-19.99 7.78,-19.99 C7.78,-19.99 -14.11,-19.99 -14.11,-19.99 C-14.22,-19.99 -14.31,-19.95 -14.38,-19.88 C-14.45,-19.81 -14.49,-19.72 -14.49,-19.61 C-14.49,-19.61 -14.49,-17.77 -14.49,-17.77c M-14.11 23.29 C-15.12,23.29 -15.99,22.93 -16.71,22.21 C-17.43,21.49 -17.79,20.62 -17.79,19.61 C-17.79,19.61 -17.79,-19.61 -17.79,-19.61 C-17.79,-20.62 -17.43,-21.49 -16.71,-22.21 C-15.99,-22.93 -15.12,-23.29 -14.11,-23.29 C-14.11,-23.29 7.78,-23.29 7.78,-23.29 C8.79,-23.29 9.65,-22.93 10.38,-22.21 C11.1,-21.49 11.46,-20.62 11.46,-19.61 C11.46,-19.61 11.46,-13.32 11.46,-13.32 C11.46,-12.86 11.3,-12.46 10.97,-12.13 C10.65,-11.81 10.25,-11.65 9.78,-11.65 C9.31,-11.65 8.91,-11.81 8.59,-12.13 C8.3,-12.46 8.15,-12.86 8.15,-13.32 C8.15,-13.32 8.15,-14.46 8.15,-14.46 C8.15,-14.46 -14.49,-14.46 -14.49,-14.46 C-14.49,-14.46 -14.49,14.46 -14.49,14.46 C-14.49,14.46 8.15,14.46 8.15,14.46 C8.15,14.46 8.15,13.33 8.15,13.33 C8.15,12.86 8.3,12.46 8.59,12.13 C8.91,11.81 9.31,11.65 9.78,11.65 C10.25,11.65 10.65,11.81 10.97,12.13 C11.3,12.46 11.46,12.86 11.46,13.33 C11.46,13.33 11.46,19.61 11.46,19.61 C11.46,20.62 11.1,21.49 10.38,22.21 C9.65,22.93 8.79,23.29 7.78,23.29 C7.78,23.29 -14.11,23.29 -14.11,23.29c "/></group><group android:name="_R_G_L_1_G" android:translateX="400" android:translateY="400"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#003352" android:fillAlpha="1" android:fillType="nonZero" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M52 61 C52,61 179,-67 179,-67 "/><path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#ff8986" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="34" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M52 61 C52,61 179,-67 179,-67 "/></group><group android:name="_R_G_L_0_G" android:translateX="400" android:translateY="400"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#003352" android:fillAlpha="1" android:fillType="nonZero" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M52 -66 C52,-66 177,60 177,60 "/><path android:name="_R_G_L_0_G_D_1_P_0" android:strokeColor="#ff8986" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="35" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M52 -66 C52,-66 177,60 177,60 "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="100" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="267" android:startOffset="100" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="100" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="267" android:startOffset="100" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="267" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="267" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="10017" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/main/res/drawable/open_on_phone_animation.xml b/wear/compose/compose-material3/src/main/res/drawable/open_on_phone_animation.xml
new file mode 100644
index 0000000..26c7f42
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/drawable/open_on_phone_animation.xml
@@ -0,0 +1,17 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="800dp" android:width="800dp" android:viewportHeight="800" android:viewportWidth="800"><group android:name="_R_G"><group android:name="_R_G_L_2_G" android:translateX="372" android:translateY="396" android:scaleX="9.86" android:scaleY="9.86"><path android:name="_R_G_L_2_G_D_0_P_0" android:fillColor="#d3e3fd" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-7.44 23.29 C-8.45,23.29 -9.32,22.93 -10.04,22.21 C-10.76,21.49 -11.13,20.62 -11.13,19.61 C-11.13,19.61 -11.13,8.18 -11.13,8.18 C-11.13,7.71 -10.96,7.31 -10.64,6.99 C-10.31,6.66 -9.91,6.5 -9.45,6.5 C-8.98,6.5 -8.6,6.66 -8.31,6.99 C-7.98,7.31 -7.82,7.71 -7.82,8.18 C-7.82,8.18 -7.82,14.46 -7.82,14.46 C-7.82,14.46 14.82,14.46 14.82,14.46 C14.82,14.46 14.82,-14.46 14.82,-14.46 C14.82,-14.46 -7.82,-14.46 -7.82,-14.46 C-7.82,-14.46 -7.82,-8.18 -7.82,-8.18 C-7.82,-7.71 -7.98,-7.31 -8.31,-6.99 C-8.6,-6.66 -8.98,-6.5 -9.45,-6.5 C-9.91,-6.5 -10.31,-6.66 -10.64,-6.99 C-10.96,-7.31 -11.13,-7.71 -11.13,-8.18 C-11.13,-8.18 -11.13,-19.61 -11.13,-19.61 C-11.13,-20.62 -10.76,-21.49 -10.04,-22.21 C-9.32,-22.93 -8.45,-23.29 -7.44,-23.29 C-7.44,-23.29 14.44,-23.29 14.44,-23.29 C15.45,-23.29 16.32,-22.93 17.04,-22.21 C17.76,-21.49 18.12,-20.62 18.12,-19.61 C18.12,-19.61 18.12,19.61 18.12,19.61 C18.12,20.62 17.76,21.49 17.04,22.21 C16.32,22.93 15.45,23.29 14.44,23.29 C14.44,23.29 -7.44,23.29 -7.44,23.29c M-7.82 17.77 C-7.82,17.77 -7.82,19.61 -7.82,19.61 C-7.82,19.72 -7.78,19.81 -7.71,19.88 C-7.64,19.95 -7.55,19.99 -7.44,19.99 C-7.44,19.99 14.44,19.99 14.44,19.99 C14.55,19.99 14.64,19.95 14.71,19.88 C14.78,19.81 14.82,19.72 14.82,19.61 C14.82,19.61 14.82,17.77 14.82,17.77 C14.82,17.77 -7.82,17.77 -7.82,17.77c M-7.82 -17.77 C-7.82,-17.77 14.82,-17.77 14.82,-17.77 C14.82,-17.77 14.82,-19.61 14.82,-19.61 C14.82,-19.72 14.78,-19.81 14.71,-19.88 C14.64,-19.95 14.55,-19.99 14.44,-19.99 C14.44,-19.99 -7.44,-19.99 -7.44,-19.99 C-7.55,-19.99 -7.64,-19.95 -7.71,-19.88 C-7.78,-19.81 -7.82,-19.72 -7.82,-19.61 C-7.82,-19.61 -7.82,-17.77 -7.82,-17.77c "/></group><group android:name="_R_G_L_1_G" android:translateX="370" android:translateY="428" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#003352" android:fillAlpha="1" android:fillType="nonZero" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M-189 -33 C-189,-33 20,-33 20,-33 "/><path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="30" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M-189 -33 C-189,-33 20,-33 20,-33 "/></group><group android:name="_R_G_L_0_G_T_1" android:translateX="180" android:translateY="395" android:scaleY="0"><group android:name="_R_G_L_0_G" android:translateX="-20" android:translateY="33"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="32" android:strokeAlpha="1" android:trimPathStart="0.49" android:trimPathEnd="0.51" android:trimPathOffset="0" android:pathData=" M-36 -90 C-36,-90 21,-33 21,-33 C21,-33 -36,24 -36,24 "/></group></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="217" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="217" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateXY" android:duration="67" android:startOffset="0" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 370,428C 370,428 370,428 370,428"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="217" android:startOffset="67" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 370,428C 370,428 412,428 412,428"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="200" android:startOffset="283" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 412,428C 412,428 400,428 400,428"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.44,0 0.55,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathStart" android:duration="133" android:startOffset="0" android:valueFrom="0.49" android:valueTo="0.49" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathStart" android:duration="100" android:startOffset="133" android:valueFrom="0.49" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="133" android:startOffset="0" android:valueFrom="0.51" android:valueTo="0.51" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="100" android:startOffset="133" android:valueFrom="0.51" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_T_1"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateXY" android:duration="67" android:startOffset="0" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 180,395C 180,395 180,395 180,395"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="217" android:startOffset="67" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 180,395C 180,395 432,395 432,395"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.5,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="translateXY" android:duration="200" android:startOffset="283" android:propertyXName="translateX" android:propertyYName="translateY" android:pathData="M 432,395C 432,395 420,395 420,395"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.44,0 0.55,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_T_1"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="10017" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/main/res/values-af/strings.xml b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
new file mode 100644
index 0000000..4951307
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Uur"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekonde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d uur</item>
+ <item quantity="one">%d uur</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minute</item>
+ <item quantity="one">%d minuut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d sekondes</item>
+ <item quantity="one">%d sekonde</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Tydperk"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestig"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-am/strings.xml b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000..da5ab9c
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ሰዓት"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"ደቂቃ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ሰከንድ"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ሰዓት</item>
+ <item quantity="other">%d ሰዓታት</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d ደቂቃ</item>
+ <item quantity="other">%d ደቂቃዎች</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d ሰከንድ</item>
+ <item quantity="other">%d ሰከንዶች</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ክፍለ ጊዜ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"አረጋግጥ"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..450227f
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ساعة"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"دقيقة"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ثانية"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="zero">%d ساعة</item>
+ <item quantity="two">ساعتان</item>
+ <item quantity="few">%d ساعات</item>
+ <item quantity="many">%d ساعة</item>
+ <item quantity="other">%d ساعة</item>
+ <item quantity="one">ساعة واحدة</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="zero">%d دقيقة</item>
+ <item quantity="two">دقيقتان</item>
+ <item quantity="few">%d دقائق</item>
+ <item quantity="many">%d دقيقة</item>
+ <item quantity="other">%d دقيقة</item>
+ <item quantity="one">دقيقة واحدة</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="zero">%d ثانية</item>
+ <item quantity="two">ثانيتان</item>
+ <item quantity="few">%d ثوانٍ</item>
+ <item quantity="many">%d ثانية</item>
+ <item quantity="other">%d ثانية</item>
+ <item quantity="one">ثانية واحدة</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"فترة"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأكيد"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-as/strings.xml b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..178eaa4
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ঘণ্টা"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"মিনিট"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"দ্বিতীয়"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ঘণ্টা</item>
+ <item quantity="other">%d ঘণ্টা</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d মিনিট</item>
+ <item quantity="other">%d মিনিট</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d ছেকেণ্ড</item>
+ <item quantity="other">%d ছেকেণ্ড</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"পিৰিয়ড"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"নিশ্চিত কৰক"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-az/strings.xml b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000..de43d4f
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Saat"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Dəqiqə"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Saniyə"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d saat</item>
+ <item quantity="one">%d saat</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d dəqiqə</item>
+ <item quantity="one">%d dəqiqə</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d saniyə</item>
+ <item quantity="one">%d saniyə</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Müddət"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Təsdiq edin"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..eca919a
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Sat"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minut"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d sat</item>
+ <item quantity="few">%d sata</item>
+ <item quantity="other">%d sati</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minut</item>
+ <item quantity="few">%d minuta</item>
+ <item quantity="other">%d minuta</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekunda</item>
+ <item quantity="few">%d sekunde</item>
+ <item quantity="other">%d sekundi</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-be/strings.xml b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000..dc5e832
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Гадзіны"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Хвіліны"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунды"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d гадзіна</item>
+ <item quantity="few">%d гадзіны</item>
+ <item quantity="many">%d гадзін</item>
+ <item quantity="other">%d гадзіны</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d хвіліна</item>
+ <item quantity="few">%d хвіліны</item>
+ <item quantity="many">%d хвілін</item>
+ <item quantity="other">%d хвіліны</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d секунда</item>
+ <item quantity="few">%d секунды</item>
+ <item quantity="many">%d секунд</item>
+ <item quantity="other">%d секунды</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Перыяд"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Пацвердзіць"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000..2866f08
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Час"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Минута"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунда"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d часа</item>
+ <item quantity="one">%d час</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d минути</item>
+ <item quantity="one">%d минута</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d секунди</item>
+ <item quantity="one">%d секунда</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Точка"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ден"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месец"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потвърждаване"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Напред"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000..34a09f1
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ঘণ্টা"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"মিনিট"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"সেকেন্ড"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ঘণ্টা</item>
+ <item quantity="other">%d ঘণ্টা</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d মিনিট</item>
+ <item quantity="other">%d মিনিট</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d সেকেন্ড</item>
+ <item quantity="other">%d সেকেন্ড</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"সময়সীমা"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"কনফার্ম করুন"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000..9e53fc8
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Sat"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d sat</item>
+ <item quantity="few">%d sata</item>
+ <item quantity="other">%d sati</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minuta</item>
+ <item quantity="few">%d minute</item>
+ <item quantity="other">%d minuta</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekunda</item>
+ <item quantity="few">%d sekunde</item>
+ <item quantity="other">%d sekundi</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrđivanje"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000..0c19945
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minut"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segon"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="many">%d d\'hores</item>
+ <item quantity="other">%d hores</item>
+ <item quantity="one">%d hora</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="many">%d de minuts</item>
+ <item quantity="other">%d minuts</item>
+ <item quantity="one">%d minut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="many">%d de segons</item>
+ <item quantity="other">%d segons</item>
+ <item quantity="one">%d segon</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Període"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dia"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Any"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirma"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Següent"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000..98dc3ea
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hodiny"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuty"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekundy"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="few">%1$d hodiny</item>
+ <item quantity="many">%1$d hodiny</item>
+ <item quantity="other">%1$d hodin</item>
+ <item quantity="one">%d hodina</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="few">%1$d minuty</item>
+ <item quantity="many">%1$d minuty</item>
+ <item quantity="other">%1$d minut</item>
+ <item quantity="one">%d minuta</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="few">%d sekundy</item>
+ <item quantity="many">%d sekundy</item>
+ <item quantity="other">%d sekund</item>
+ <item quantity="one">%d sekunda</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Období"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdit"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-da/strings.xml b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000..d1bd436
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Time"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minut"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekund"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d time</item>
+ <item quantity="other">%d timer</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minut</item>
+ <item quantity="other">%d minutter</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekund</item>
+ <item quantity="other">%d sekunder</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Format"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekræft"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-de/strings.xml b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000..9fdd438
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Stunde"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d Stunden</item>
+ <item quantity="one">%d Stunde</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d Minuten</item>
+ <item quantity="one">%d Minute</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d Sekunden</item>
+ <item quantity="one">%d Sekunde</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Zeitraum"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bestätigen"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-el/strings.xml b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000..20374c9
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Ώρα"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Λεπτό"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Δευτ."</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d ώρες</item>
+ <item quantity="one">%d ώρα</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d λεπτά</item>
+ <item quantity="one">%d λεπτό</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d δευτερόλεπτα</item>
+ <item quantity="one">%d δευτερόλεπτο</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Περίοδος"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Επιβεβαίωση"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..58982b2
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hour"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Second"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d hours</item>
+ <item quantity="one">%d hour</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutes</item>
+ <item quantity="one">%d minute</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d seconds</item>
+ <item quantity="one">%d second</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..fb5c42c
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hour"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Second"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d Hours</item>
+ <item quantity="one">%d Hour</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d Minutes</item>
+ <item quantity="one">%d Minute</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d Seconds</item>
+ <item quantity="one">%d Second</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..58982b2
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hour"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Second"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d hours</item>
+ <item quantity="one">%d hour</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutes</item>
+ <item quantity="one">%d minute</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d seconds</item>
+ <item quantity="one">%d second</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..58982b2
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hour"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Second"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d hours</item>
+ <item quantity="one">%d hour</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutes</item>
+ <item quantity="one">%d minute</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d seconds</item>
+ <item quantity="one">%d second</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..0bff2a4
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hour"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Second"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d Hours</item>
+ <item quantity="one">%d Hour</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d Minutes</item>
+ <item quantity="one">%d Minute</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d Seconds</item>
+ <item quantity="one">%d Second</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..5dac2b5
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="many">%d de horas</item>
+ <item quantity="other">%d horas</item>
+ <item quantity="one">%d hora</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="many">%d de minutos</item>
+ <item quantity="other">%d minutos</item>
+ <item quantity="one">%d minuto</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="many">%d de segundos</item>
+ <item quantity="other">%d segundos</item>
+ <item quantity="one">%d segundo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-es/strings.xml b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000..7ed6eed
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="many">%d horas</item>
+ <item quantity="other">%d horas</item>
+ <item quantity="one">%d hora</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="many">%d minutos</item>
+ <item quantity="other">%d minutos</item>
+ <item quantity="one">%d minuto</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="many">%d segundos</item>
+ <item quantity="other">%d segundos</item>
+ <item quantity="one">%d segundo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periodo"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-et/strings.xml b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000..898d4f1
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Tunnid"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minutid"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekund"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d tundi</item>
+ <item quantity="one">%d tund</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutit</item>
+ <item quantity="one">%d minut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d sekundit</item>
+ <item quantity="one">%d sekund</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periood"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kinnita"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000..202e7ee
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Ordua"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minutua"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundoa"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d ordu</item>
+ <item quantity="one">%d ordu</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutu</item>
+ <item quantity="one">%d minutu</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d segundo</item>
+ <item quantity="one">%d segundo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Epea"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Berretsi"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000..8f9c137
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ساعت"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"دقیقه"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ثانیه"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ساعت</item>
+ <item quantity="other">%d ساعت</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d دقیقه</item>
+ <item quantity="other">%d دقیقه</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d ثانیه</item>
+ <item quantity="other">%d ثانیه</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"مدت زمان"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأیید کردن"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..0f0b50b
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Tunti"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuutti"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Toinen"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d tuntia</item>
+ <item quantity="one">%d tunti</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minuuttia</item>
+ <item quantity="one">%d minuutti</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d sekuntia</item>
+ <item quantity="one">%d sekunti</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Jakso"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Vahvista"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..cdc3ad5
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Heure"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Seconde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d heure</item>
+ <item quantity="many">%d d\'heures</item>
+ <item quantity="other">%d heures</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minute</item>
+ <item quantity="many">%d de minutes</item>
+ <item quantity="other">%d minutes</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d seconde</item>
+ <item quantity="many">%d de secondes</item>
+ <item quantity="other">%d secondes</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Période"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000..bdb1f1b
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Heure"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minute"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Seconde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d heure</item>
+ <item quantity="many">%d d\'heures</item>
+ <item quantity="other">%d heures</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minute</item>
+ <item quantity="many">%d de minutes</item>
+ <item quantity="other">%d minutes</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d seconde</item>
+ <item quantity="many">%d de secondes</item>
+ <item quantity="other">%d secondes</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Période"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000..ad5ca33
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d horas</item>
+ <item quantity="one">%d hora</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutos</item>
+ <item quantity="one">%d minuto</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d segundos</item>
+ <item quantity="one">%d segundo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
new file mode 100644
index 0000000..b01e9be
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"કલાક"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"મિનિટ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"સેકન્ડ"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d કલાક</item>
+ <item quantity="other">%d કલાક</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d મિનિટ</item>
+ <item quantity="other">%d મિનિટ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d સેકન્ડ</item>
+ <item quantity="other">%d સેકન્ડ</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"અવધિ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"કન્ફર્મ કરો"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000..c861b3af
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"घंटा"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"मिनट"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"दूसरा"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d घंटा</item>
+ <item quantity="other">%d घंटे</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d मिनट</item>
+ <item quantity="other">%d मिनट</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d सेकंड</item>
+ <item quantity="other">%d सेकंड</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"समयअवधि"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"दिन"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"महीना"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि करें"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अगला"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000..af212a5
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Sat"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d sat</item>
+ <item quantity="few">%d sata</item>
+ <item quantity="other">%d sati</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minuta</item>
+ <item quantity="few">%d minute</item>
+ <item quantity="other">%d minuta</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekunda</item>
+ <item quantity="few">%d sekunde</item>
+ <item quantity="other">%d sekundi</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Razdoblje"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000..2cce16e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Óra"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Perc"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Mp."</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d óra</item>
+ <item quantity="one">%d óra</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d perc</item>
+ <item quantity="one">%d perc</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d másodperc</item>
+ <item quantity="one">%d másodperc</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Időszak"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Megerősítés"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
new file mode 100644
index 0000000..cda99db
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Ժամ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Րոպե"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Վայրկյան"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ժամ</item>
+ <item quantity="other">%d ժամ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d րոպե</item>
+ <item quantity="other">%d րոպե</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d վայրկյան</item>
+ <item quantity="other">%d վայրկյան</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Ժամանակահատված"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Հաստատել"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-in/strings.xml b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000..66a973e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Jam"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Menit"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Detik"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d Jam</item>
+ <item quantity="one">%d Jam</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d Menit</item>
+ <item quantity="one">%d Menit</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d Detik</item>
+ <item quantity="one">%d Detik</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Jangka waktu"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmasi"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-is/strings.xml b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000..8983c3f
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Klukkustund"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Mínúta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekúnda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d klukkustund</item>
+ <item quantity="other">%d klukkustundir</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d mínúta</item>
+ <item quantity="other">%d mínútur</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekúnda</item>
+ <item quantity="other">%d sekúndur</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Punktur"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Staðfesta"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-it/strings.xml b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..a9566b7
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Ora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Secondo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="many">%d di ore</item>
+ <item quantity="other">%d ore</item>
+ <item quantity="one">%d ora</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="many">%d di minuti</item>
+ <item quantity="other">%d minuti</item>
+ <item quantity="one">%d minuto</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="many">%d di secondi</item>
+ <item quantity="other">%d secondi</item>
+ <item quantity="one">%d secondo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periodo"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Conferma"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000..91f1e27
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"שעות"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"דקות"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"שניות"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d שעות</item>
+ <item quantity="two">שעתיים (%d)</item>
+ <item quantity="other">%d שעות</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d דקות</item>
+ <item quantity="two">%d דקות</item>
+ <item quantity="other">%d דקות</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d שניות</item>
+ <item quantity="two">%d שניות</item>
+ <item quantity="other">%d שניות</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"תקופת זמן"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"אישור"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000..65e9cc7
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"時間"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"分"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"秒"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d 時間</item>
+ <item quantity="one">%d 時間</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d 分</item>
+ <item quantity="one">%d 分</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d 秒</item>
+ <item quantity="one">%d 秒</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"期間"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"日"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"月"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"次へ"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000..4f4734e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"საათი"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"წუთი"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"წამი"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d საათი</item>
+ <item quantity="one">%d საათი</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d წუთი</item>
+ <item quantity="one">%d წუთი</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d წამი</item>
+ <item quantity="one">%d წამი</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"პერიოდი"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"დღე"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"თვე"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"წელი"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"დადასტურება"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"შემდეგი"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000..e0d8b3d
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Сағат"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Mинут"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунд"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d сағат</item>
+ <item quantity="one">%d сағат</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d минут</item>
+ <item quantity="one">%d минут</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d секунд</item>
+ <item quantity="one">%d секунд</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Кезең"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Растау"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-km/strings.xml b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
new file mode 100644
index 0000000..410a6f2
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ម៉ោង"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"នាទី"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"វិនាទី"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d ម៉ោង</item>
+ <item quantity="one">%d ម៉ោង</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d នាទី</item>
+ <item quantity="one">%d នាទី</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d វិនាទី</item>
+ <item quantity="one">%d វិនាទី</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"រយៈពេល"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"បញ្ជាក់"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000..6ae6994
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ಗಂಟೆ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"ನಿಮಿಷ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ಸೆಕೆಂಡ್"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ಗಂಟೆಗಳು</item>
+ <item quantity="other">%d ಗಂಟೆಗಳು</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d ನಿಮಿಷಗಳು</item>
+ <item quantity="other">%d ನಿಮಿಷಗಳು</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d ಸೆಕೆಂಡ್ಗಳು</item>
+ <item quantity="other">%d ಸೆಕೆಂಡ್ಗಳು</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ಅವಧಿ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ದೃಢೀಕರಿಸಿ"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000..45f8446
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"시간"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"분"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"초"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d시간</item>
+ <item quantity="one">%d시간</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d분</item>
+ <item quantity="one">%d분</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d초</item>
+ <item quantity="one">%d초</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"기간"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"확인"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
new file mode 100644
index 0000000..692726e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Саат"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Мүнөт"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунд"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d саат</item>
+ <item quantity="one">%d саат</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d мүнөт</item>
+ <item quantity="one">%d мүнөт</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d секунд</item>
+ <item quantity="one">%d секунд</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Чекит"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Ырастоо"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000..59d2d94
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ຊົ່ວໂມງ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"ນາທີ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ວິນາທີ"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d ຊົ່ວໂມງ</item>
+ <item quantity="one">%d ຊົ່ວໂມງ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d ນາທີ</item>
+ <item quantity="one">%d ນາທີ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d ວິນາທີ</item>
+ <item quantity="one">%d ວິນາທີ</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ໄລຍະເວລາ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ຢືນຢັນ"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000..615054e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Valanda"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minutė"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekundė"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d valanda</item>
+ <item quantity="few">%d valandos</item>
+ <item quantity="many">%d valandos</item>
+ <item quantity="other">%d valandų</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minutė</item>
+ <item quantity="few">%d minutės</item>
+ <item quantity="many">%d minutės</item>
+ <item quantity="other">%d minučių</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekundė</item>
+ <item quantity="few">%d sekundės</item>
+ <item quantity="many">%d sekundės</item>
+ <item quantity="other">%d sekundžių</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Laikotarpis"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Patvirtinti"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
new file mode 100644
index 0000000..08f2d2c
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Stunda"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minūte"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="zero">%d stundu</item>
+ <item quantity="one">%d stunda</item>
+ <item quantity="other">%d stundas</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="zero">%d minūšu</item>
+ <item quantity="one">%d minūte</item>
+ <item quantity="other">%d minūtes</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="zero">%d sekunžu</item>
+ <item quantity="one">%d sekunde</item>
+ <item quantity="other">%d sekundes</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periods"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Apstiprināt"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
new file mode 100644
index 0000000..a093425
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Час"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Минута"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунда"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d час</item>
+ <item quantity="other">%d часа</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d минута</item>
+ <item quantity="other">%d минути</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d секунда</item>
+ <item quantity="other">%d секунди</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000..36d8b54
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"മണിക്കൂർ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"മിനിറ്റ്"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"സെക്കൻഡ്"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d മണിക്കൂർ</item>
+ <item quantity="one">%d മണിക്കൂർ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d മിനിറ്റ്</item>
+ <item quantity="one">%d മിനിറ്റ്</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d സെക്കൻഡ്</item>
+ <item quantity="one">%d സെക്കൻഡ്</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"കാലയളവ്"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"സ്ഥിരീകരിക്കുക"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
new file mode 100644
index 0000000..629e16a
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Цаг"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Минут"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунд"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d цаг</item>
+ <item quantity="one">%d цаг</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d минут</item>
+ <item quantity="one">%d минут</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d секунд</item>
+ <item quantity="one">%d секунд</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Хугацаа"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Баталгаажуулах"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000..904ad4f
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"तास"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"मिनिट"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"सेकंद"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d तास</item>
+ <item quantity="one">%d तास</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d मिनिटे</item>
+ <item quantity="one">%d मिनिट</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d सेकंद</item>
+ <item quantity="one">%d सेकंद</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"कालावधी"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"कन्फर्म करा"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
new file mode 100644
index 0000000..df4b8c9
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Jam"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minit"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Kedua"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d Jam</item>
+ <item quantity="one">%d Jam</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d Minit</item>
+ <item quantity="one">%d Minit</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d Saat</item>
+ <item quantity="one">%d Saat</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Tempoh"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Sahkan"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-my/strings.xml b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000..aa85494
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"နာရီ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"မိနစ်"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"စက္ကန့်"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d နာရီ</item>
+ <item quantity="one">%d နာရီ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d မိနစ်</item>
+ <item quantity="one">%d မိနစ်</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d စက္ကန့်</item>
+ <item quantity="one">%d စက္ကန့်</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"အချိန်ကာလ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"အတည်ပြုရန်"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
new file mode 100644
index 0000000..0d9dbd3
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Time"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minutt"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekund"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d timer</item>
+ <item quantity="one">%d time</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minutter</item>
+ <item quantity="one">%d minutt</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d sekunder</item>
+ <item quantity="one">%d sekund</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periode"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekreft"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
new file mode 100644
index 0000000..4f5d479
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"घण्टा"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"मिनेट"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"सेकेन्ड"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d घण्टा</item>
+ <item quantity="one">%d घण्टा</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d मिनेट</item>
+ <item quantity="one">%d मिनेट</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d सेकेन्ड</item>
+ <item quantity="one">%d सेकेन्ड</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"अवधि"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि गर्नुहोस्"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..8561545
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Uur"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuut"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Seconde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d uur</item>
+ <item quantity="one">%d uur</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minuten</item>
+ <item quantity="one">%d minuut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d seconden</item>
+ <item quantity="one">%d seconde</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periode"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestigen"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-or/strings.xml b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..7701b598
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ଘଣ୍ଟା"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"ମିନିଟ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ସେକେଣ୍ଡ"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d ଘଣ୍ଟା</item>
+ <item quantity="one">%d ଘଣ୍ଟା</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d ମିନିଟ</item>
+ <item quantity="one">%d ମିନିଟ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d ସେକେଣ୍ଡ</item>
+ <item quantity="one">%d ସେକେଣ୍ଡ</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ଅବଧି"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
new file mode 100644
index 0000000..1000cde
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ਘੰਟੇ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"ਮਿੰਟ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"ਸਕਿੰਟ"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ਘੰਟਾ</item>
+ <item quantity="other">%d ਘੰਟੇ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d ਮਿੰਟ</item>
+ <item quantity="other">%d ਮਿੰਟ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d ਸਕਿੰਟ</item>
+ <item quantity="other">%d ਸਕਿੰਟ</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ਮਿਆਦ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ਤਸਦੀਕ ਕਰੋ"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000..5d56d82
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Godzina"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="few">%d godziny</item>
+ <item quantity="many">%d godzin</item>
+ <item quantity="other">%d godziny</item>
+ <item quantity="one">%d godzina</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="few">%d minuty</item>
+ <item quantity="many">%d minut</item>
+ <item quantity="other">%d minuty</item>
+ <item quantity="one">%d minuta</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="few">%d sekundy</item>
+ <item quantity="many">%d sekund</item>
+ <item quantity="other">%d sekundy</item>
+ <item quantity="one">%d sekunda</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Kropka"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potwierdź"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..77c3710
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d hora</item>
+ <item quantity="many">%d de horas</item>
+ <item quantity="other">%d horas</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minuto</item>
+ <item quantity="many">%d de minutos</item>
+ <item quantity="other">%d minutos</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d segundo</item>
+ <item quantity="many">%d de segundos</item>
+ <item quantity="other">%d segundos</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..416cbc0
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="many">%d horas</item>
+ <item quantity="other">%d horas</item>
+ <item quantity="one">%d hora</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="many">%d minutos</item>
+ <item quantity="other">%d minutos</item>
+ <item quantity="one">%d minuto</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="many">%d segundos</item>
+ <item quantity="other">%d segundos</item>
+ <item quantity="one">%d segundo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dia"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mês"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..77c3710
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d hora</item>
+ <item quantity="many">%d de horas</item>
+ <item quantity="other">%d horas</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minuto</item>
+ <item quantity="many">%d de minutos</item>
+ <item quantity="other">%d minutos</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d segundo</item>
+ <item quantity="many">%d de segundos</item>
+ <item quantity="other">%d segundos</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000..f9723df
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Oră"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minut"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Secundă"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="few">%d ore</item>
+ <item quantity="other">%d de ore</item>
+ <item quantity="one">%d oră</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="few">%d minute</item>
+ <item quantity="other">%d de minute</item>
+ <item quantity="one">%d minut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="few">%d secunde</item>
+ <item quantity="other">%d de secunde</item>
+ <item quantity="one">%d secundă</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Perioada"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmă"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..e01e3c3
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Часы"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Минуты"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунды"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d час</item>
+ <item quantity="few">%d часа</item>
+ <item quantity="many">%d часов</item>
+ <item quantity="other">%d часа</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d минута</item>
+ <item quantity="few">%d минуты</item>
+ <item quantity="many">%d минут</item>
+ <item quantity="other">%d минуты</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d секунда</item>
+ <item quantity="few">%d секунды</item>
+ <item quantity="many">%d секунд</item>
+ <item quantity="other">%d секунды</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Подтвердить"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-si/strings.xml b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000..6f3d4c6
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"පැය"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"විනාඩි"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"තත්පර"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">පැය %d</item>
+ <item quantity="other">පැය %d</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">මිනිත්තු %d</item>
+ <item quantity="other">මිනිත්තු %d</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">තත්පර %d</item>
+ <item quantity="other">තත්පර %d</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"කාල පරිච්ඡේදය"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"තහවුරු කරන්න"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000..8840530
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Hodina"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minúta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="few">%d hodiny</item>
+ <item quantity="many">%d hodiny</item>
+ <item quantity="other">%d hodín</item>
+ <item quantity="one">%d hodina</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="few">%d minúty</item>
+ <item quantity="many">%d minúty</item>
+ <item quantity="other">%d minút</item>
+ <item quantity="one">%d minúta</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="few">%d sekundy</item>
+ <item quantity="many">%d sekundy</item>
+ <item quantity="other">%d sekúnd</item>
+ <item quantity="one">%d sekunda</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Obdobie"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdiť"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000..cc79d76
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Ura"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d ura</item>
+ <item quantity="two">%d uri</item>
+ <item quantity="few">%d ure</item>
+ <item quantity="other">%d ur</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d minuta</item>
+ <item quantity="two">%d minuti</item>
+ <item quantity="few">%d minute</item>
+ <item quantity="other">%d minut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d sekunda</item>
+ <item quantity="two">%d sekundi</item>
+ <item quantity="few">%d sekunde</item>
+ <item quantity="other">%d sekund</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Pika"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Leto"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potrdi"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprej"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000..411d53e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Orë"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuta"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekonda"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d orë</item>
+ <item quantity="one">%d orë</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minuta</item>
+ <item quantity="one">%d minutë</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d sekonda</item>
+ <item quantity="one">%d sekondë</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periudha"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmo"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000..661b6d2
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Сат"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Минут"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунда"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d сат</item>
+ <item quantity="few">%d сата</item>
+ <item quantity="other">%d сати</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d минут</item>
+ <item quantity="few">%d минута</item>
+ <item quantity="other">%d минута</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d секунда</item>
+ <item quantity="few">%d секунде</item>
+ <item quantity="other">%d секунди</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
new file mode 100644
index 0000000..b395ba1d
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Timme"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minut"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekund"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d timmar</item>
+ <item quantity="one">%d timme</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d minuter</item>
+ <item quantity="one">%d minut</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d sekunder</item>
+ <item quantity="one">%d sekund</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Punkt"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekräfta"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
new file mode 100644
index 0000000..aad1959
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Saa"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Dakika"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Sekunde"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">Saa %d</item>
+ <item quantity="one">Saa %d</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">Dakika %d</item>
+ <item quantity="one">Dakika %d</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">Sekunde %d</item>
+ <item quantity="one">Sekunde %d</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Kipindi"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Thibitisha"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000..aacd3cd
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"மணிநேரம்"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"நிமிடம்"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"வினாடி"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d மணிநேரம்</item>
+ <item quantity="one">%d மணிநேரம்</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d நிமிடங்கள்</item>
+ <item quantity="one">%d நிமிடம்</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d வினாடிகள்</item>
+ <item quantity="one">%d வினாடி</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"கால இடைவெளி"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"உறுதிசெய்யும்"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-te/strings.xml b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000..2374063
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"గంట"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"నిమిషం"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"సెకను"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d గంటలు</item>
+ <item quantity="one">%d గంట</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d నిమిషాలు</item>
+ <item quantity="one">%d నిమిషం</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d సెకన్లు</item>
+ <item quantity="one">%d సెకను</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"వ్యవధి"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"రోజు"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"నెల"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"సంవత్సరం"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"నిర్ధారించండి"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"తర్వాత"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-th/strings.xml b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000..d1ad793
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"ชั่วโมง"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"นาที"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"วินาที"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d ชั่วโมง</item>
+ <item quantity="one">%d ชั่วโมง</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d นาที</item>
+ <item quantity="one">%d นาที</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d วินาที</item>
+ <item quantity="one">%d วินาที</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ระยะเวลา"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"วัน"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"เดือน"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ปี"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ยืนยัน"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ถัดไป"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000..fe20f09
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Oras"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Minuto"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Segundo"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d Oras</item>
+ <item quantity="other">%d na Oras</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d Minuto</item>
+ <item quantity="other">%d na Minuto</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d Segundo</item>
+ <item quantity="other">%d na Segundo</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Panahon"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Araw"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Buwan"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Taon"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kumpirmahin"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Susunod"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000..3ec35be
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Saat"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Dakika"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Saniye"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d Saat</item>
+ <item quantity="one">%d Saat</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d Dakika</item>
+ <item quantity="one">%d Dakika</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d Saniye</item>
+ <item quantity="one">%d Saniye</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Aralık"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Onayla"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000..bb037ea
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Година"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Хвилина"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Секунда"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">%d година</item>
+ <item quantity="few">%d години</item>
+ <item quantity="many">%d годин</item>
+ <item quantity="other">%d години</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">%d хвилина</item>
+ <item quantity="few">%d хвилини</item>
+ <item quantity="many">%d хвилин</item>
+ <item quantity="other">%d хвилини</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">%d секунда</item>
+ <item quantity="few">%d секунди</item>
+ <item quantity="many">%d секунд</item>
+ <item quantity="other">%d секунди</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Період"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Підтвердити"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000..6f902ab
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"گھنٹہ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"منٹ"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"دوسرا"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d گھنٹے</item>
+ <item quantity="one">%d گھنٹہ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d منٹ</item>
+ <item quantity="one">%d منٹ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d سیکنڈ</item>
+ <item quantity="one">%d سیکنڈ</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"وقفہ"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تصدیق کریں"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000..4b1121e
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Soat"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Daqiqa"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Soniya"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d soat</item>
+ <item quantity="one">%d soat</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d daqiqa</item>
+ <item quantity="one">%d daqiqa</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d soniya</item>
+ <item quantity="one">%d soniya</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Oraliq"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Kun"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Oy"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yil"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Tasdiqlash"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Keyingisi"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000..3b40f28
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Giờ"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Phút"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Giây"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d giờ</item>
+ <item quantity="one">%d giờ</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d phút</item>
+ <item quantity="one">%d phút</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d giây</item>
+ <item quantity="one">%d giây</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Khoảng thời gian"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Xác nhận"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..14ce25a
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"小时"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"分钟"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"秒"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d 小时</item>
+ <item quantity="one">%d 小时</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d 分钟</item>
+ <item quantity="one">%d 分钟</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d 秒</item>
+ <item quantity="one">%d 秒</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"时段"</string>
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"日"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"月"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"确认"</string>
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一个"</string>
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..0d30e02
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"小時"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"分鐘"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"秒"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d 個鐘</item>
+ <item quantity="one">%d 個鐘</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d 分鐘</item>
+ <item quantity="one">%d 分鐘</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d 秒</item>
+ <item quantity="one">%d 秒</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"時段"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..3e569a9
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"小時"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"分鐘"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"秒"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="other">%d 小時</item>
+ <item quantity="one">%d 小時</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="other">%d 分鐘</item>
+ <item quantity="one">%d 分鐘</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="other">%d 秒</item>
+ <item quantity="one">%d 秒</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"期間"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
new file mode 100644
index 0000000..43167ed
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wear_m3c_time_picker_hour" msgid="5670838450714030035">"Ihora"</string>
+ <string name="wear_m3c_time_picker_minute" msgid="2847700380677127030">"Umzuzu"</string>
+ <string name="wear_m3c_time_picker_second" msgid="5551916170669814925">"Umzuzwana"</string>
+ <plurals name="wear_m3c_time_picker_hours_content_description" formatted="false" msgid="7688673698789346225">
+ <item quantity="one">Amahora angu-%d</item>
+ <item quantity="other">Amahora angu-%d</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_minutes_content_description" formatted="false" msgid="8268405448590438607">
+ <item quantity="one">Imizuzu engu-%d</item>
+ <item quantity="other">Imizuzu engu-%d</item>
+ </plurals>
+ <plurals name="wear_m3c_time_picker_seconds_content_description" formatted="false" msgid="1073969431850983434">
+ <item quantity="one">Imizuzwana engu-%d</item>
+ <item quantity="other">Imizuzwana engu-%d</item>
+ </plurals>
+ <string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Isikhathi"</string>
+ <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
+ <skip />
+ <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
+ <skip />
+ <string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Qinisekisa"</string>
+ <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
+ <skip />
+</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values/strings.xml b/wear/compose/compose-material3/src/main/res/values/strings.xml
index 35e6bd7..868ca2b 100644
--- a/wear/compose/compose-material3/src/main/res/values/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values/strings.xml
@@ -31,5 +31,13 @@
<item quantity="other">%d Seconds</item>
</plurals>
<string description="Content description of the period picker in TimePickerWith12HourClock. It lets the user select the period for 12H time format. [CHAR_LIMIT=NONE]" name="wear_m3c_time_picker_period">Period</string>
+ <string description="Lets the user know that this picker is to change the value of the day date unit. Appears on the DatePicker component, on top of the day picker. [CHAR_LIMIT=8]" name="wear_m3c_date_picker_day">Day</string>
+ <string description="Lets the user know that this picker is to change the value of the month date unit. Appears on the DatePicker component, on top of the month picker. [CHAR_LIMIT=8]" name="wear_m3c_date_picker_month">Month</string>
+ <string description="Lets the user know that this picker is to change the value of the year date unit. Appears on the DatePicker component, on top of the year picker. [CHAR_LIMIT=8]" name="wear_m3c_date_picker_year">Year</string>
<string description="Content description of the confirm button of DatePicker and TimePicker components. It lets the user confirm the date or time selected. [CHAR_LIMIT=NONE]" name="wear_m3c_picker_confirm_button_content_description">Confirm</string>
-</resources>
\ No newline at end of file
+ <string description="Content description of the next button of DatePicker and TimePicker components. It lets the user to move to the next picker. [CHAR_LIMIT=NONE]" name="wear_m3c_picker_next_button_content_description">Next</string>
+
+ <string description="A message which is used to indicate that an action failed in a FailureConfirmation [CHAR_LIMIT=12]" name="wear_m3c_confirmation_failure_message">Failed</string>
+ <string description="A message which is used to indicate that an action succeeded in a SuccessConfirmation [CHAR_LIMIT=12]" name="wear_m3c_confirmation_success_message">Success</string>
+ <string description="A message which is used to indicate than the user should continue their action on their phone, used in OpenOnPhone component [CHAR_LIMIT=12]" name="wear_m3c_open_on_phone">Open on phone</string>
+</resources>
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 2c17d38..bb35fa2 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -31,8 +31,8 @@
}
dependencies {
- api("androidx.compose.ui:ui:1.7.0-beta02")
- api("androidx.compose.runtime:runtime:1.6.0")
+ api("androidx.compose.ui:ui:1.7.0")
+ api("androidx.compose.runtime:runtime:1.7.0")
api("androidx.navigation:navigation-runtime:2.6.0")
api(project(":wear:compose:compose-material"))
api("androidx.activity:activity-compose:1.7.0")
diff --git a/wear/compose/compose-ui-tooling/build.gradle b/wear/compose/compose-ui-tooling/build.gradle
index 9c73539..5dcfeea 100644
--- a/wear/compose/compose-ui-tooling/build.gradle
+++ b/wear/compose/compose-ui-tooling/build.gradle
@@ -32,7 +32,7 @@
dependencies {
api("androidx.annotation:annotation:1.8.1")
- api("androidx.compose.ui:ui-tooling-preview:1.7.0-beta02")
+ api("androidx.compose.ui:ui-tooling-preview:1.7.0")
implementation(libs.kotlinStdlib)
implementation("androidx.wear:wear-tooling-preview:1.0.0")
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index b0421b4..8886921 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 34
- versionName "1.34"
+ versionCode 37
+ versionName "1.37"
}
buildTypes {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 650517cd..a674c88 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.integration.demos
import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -25,8 +26,10 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.SwipeToDismissBoxState
import androidx.wear.compose.foundation.SwipeToDismissKeys
import androidx.wear.compose.foundation.SwipeToDismissValue
@@ -123,6 +126,8 @@
modifier = Modifier.fillMaxWidth().testTag(DemoListTag),
state = state,
autoCentering = AutoCenteringParams(itemIndex = if (category.demos.size >= 2) 2 else 1),
+ contentPadding =
+ PaddingValues(horizontal = LocalConfiguration.current.screenWidthDp.dp * 0.052f),
) {
item {
ListHeader {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index c879bf4..9f3ead4 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -6,6 +6,18 @@
method @UiThread public void startEvaluation();
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface DynamicTypeAnimator {
+ method public void advanceToAnimationTime(long);
+ method public Object? getCurrentValue();
+ method public long getDurationMs();
+ method public Object? getEndValue();
+ method public long getStartDelayMs();
+ method public Object? getStartValue();
+ method public android.animation.TypeEvaluator<? extends java.lang.Object!> getTypeEvaluator();
+ method public void setFloatValues(float...);
+ method public void setIntValues(int...);
+ }
+
public abstract class DynamicTypeBindingRequest {
method public static androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest forDynamicBool(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Boolean!>);
method public static androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest forDynamicColor(androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeAnimator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeAnimator.java
new file mode 100644
index 0000000..eb56683
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeAnimator.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import android.animation.TypeEvaluator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * DynamicTypeAnimator interface defines the methods and properties of ProtoLayout animation.
+ *
+ * <p>The following classes implement the DynamicTypeAnimator interface:
+ *
+ * <ul>
+ * <li>{@link QuotaAwareAnimator}
+ * <li>{@link QuotaAwareAnimatorWithAux}
+ * </ul>
+ *
+ * <p>This interface allows to inspect animation and modify it. It can set new float and int values,
+ * and set a timeframe for animation. This class is intended to be used by Ui-tooling in Android
+ * Studio
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public interface DynamicTypeAnimator {
+
+ /**
+ * Gets the type evaluator used for interpolating values in this animation.
+ *
+ * @return The type evaluator used for interpolation.
+ */
+ @NonNull
+ TypeEvaluator<?> getTypeEvaluator();
+
+ /**
+ * Sets the float values that this animation will animate between.
+ *
+ * @param values The float values to animate between.
+ * @throws IllegalArgumentException if this {@link DynamicTypeAnimator} is not configured with a
+ * suitable {@link TypeEvaluator} for float values (e.g., {@link FloatEvaluator}).
+ */
+ void setFloatValues(@NonNull float... values);
+
+ /**
+ * Sets the integer values that this animation will animate between.
+ *
+ * @param values The integer values to animate between.
+ * @throws IllegalArgumentException if this {@link DynamicTypeAnimator} is not configured with a
+ * suitable {@link TypeEvaluator} for integer values (e.g., {@link IntEvaluator} or {@link
+ * ArgbEvaluator}).
+ */
+ void setIntValues(@NonNull int... values);
+
+ /**
+ * Advances the animation to the specified time.
+ *
+ * @param newTime The new time in milliseconds from animation start.
+ */
+ void advanceToAnimationTime(long newTime);
+
+ /**
+ * Gets the start value of the animation.
+ *
+ * @return The start value of the animation or null if value wasn't set.
+ */
+ @Nullable
+ Object getStartValue();
+
+ /**
+ * Gets the end value of the animation.
+ *
+ * @return The end value of the animation.
+ */
+ @Nullable
+ Object getEndValue();
+
+ /**
+ * Gets the last value of the animated property at the current time in the animation.
+ *
+ * @return The last calculated animated value or null if value wasn't set.
+ */
+ @Nullable
+ Object getCurrentValue();
+
+ /**
+ * Gets the duration of the animation, in milliseconds.
+ *
+ * @return The duration of the animation.
+ */
+ long getDurationMs();
+
+ /**
+ * Gets the start delay of the animation, in milliseconds.
+ *
+ * @return The start delay of the animation.
+ */
+ long getStartDelayMs();
+}
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
index fa7fdbd..b9671b6 100644
--- 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
@@ -30,6 +30,7 @@
import android.os.Looper;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.os.HandlerCompat;
import androidx.wear.protolayout.expression.pipeline.AnimationsHelper.RepeatDelays;
@@ -42,14 +43,18 @@
* quota manager allows. If not, non infinite animation will jump to an end. Any existing listeners
* on wrapped {@link Animator} will be replaced.
*/
-class QuotaAwareAnimator {
+class QuotaAwareAnimator implements DynamicTypeAnimator {
@NonNull protected final ValueAnimator mAnimator;
@NonNull protected final QuotaManager mQuotaManager;
@NonNull protected final QuotaReleasingAnimatorListener mListener;
@NonNull protected final Handler mUiHandler;
- private long mStartDelay = 0;
+ private final long mStartDelay;
protected Runnable mAcquireQuotaAndAnimateRunnable = this::acquireQuotaAndAnimate;
@NonNull protected final TypeEvaluator<?> mEvaluator;
+ @Nullable protected Object mLastAnimatedValue;
+
+ @Nullable private Object mStartValue = null; // To cache the start value
+ @Nullable private Object mEndValue = null; // To cache the end value
interface UpdateCallback {
void onUpdate(@NonNull Object animatedValue);
@@ -96,6 +101,12 @@
mEvaluator = evaluator;
}
+ @NonNull
+ @Override
+ public TypeEvaluator<?> getTypeEvaluator() {
+ return mEvaluator;
+ }
+
/**
* Adds a listener that is sent update events through the life of the animation. This method is
* called on every frame of the animation after the values of the animation have been
@@ -103,7 +114,10 @@
*/
void addUpdateCallback(@NonNull UpdateCallback updateCallback) {
mAnimator.addUpdateListener(
- animation -> updateCallback.onUpdate(animation.getAnimatedValue()));
+ animation -> {
+ mLastAnimatedValue = animation.getAnimatedValue();
+ updateCallback.onUpdate(mLastAnimatedValue);
+ });
}
/**
@@ -111,8 +125,11 @@
*
* @param values A set of values that the animation will animate between over time.
*/
- void setFloatValues(float... values) {
+ @Override
+ public void setFloatValues(@NonNull float... values) {
setFloatValues(mAnimator, mEvaluator, values);
+ mStartValue = values[0];
+ mEndValue = values[values.length - 1];
}
protected static void setFloatValues(
@@ -134,8 +151,33 @@
*
* @param values A set of values that the animation will animate between over time.
*/
- void setIntValues(int... values) {
+ @Override
+ public void setIntValues(@NonNull int... values) {
setIntValues(mAnimator, mEvaluator, values);
+ mStartValue = values[0];
+ mEndValue = values[values.length - 1];
+ }
+
+ /**
+ * Gets the start value of the animation.
+ *
+ * @return The start value of the animation or null if value wasn't set.
+ */
+ @Override
+ @Nullable
+ public Object getStartValue() {
+ return mStartValue;
+ }
+
+ /**
+ * Gets the end value of the animation.
+ *
+ * @return The end value of the animation.
+ */
+ @Override
+ @Nullable
+ public Object getEndValue() {
+ return mEndValue;
}
protected static void setIntValues(
@@ -251,6 +293,28 @@
mAnimator.end();
}
+ @Override
+ public void advanceToAnimationTime(long newTime) {
+ long adjustedTime = newTime - mStartDelay;
+ mAnimator.setCurrentPlayTime(adjustedTime);
+ }
+
+ @Nullable
+ @Override
+ public Object getCurrentValue() {
+ return mLastAnimatedValue;
+ }
+
+ @Override
+ public long getDurationMs() {
+ return mAnimator.getDuration();
+ }
+
+ @Override
+ public long getStartDelayMs() {
+ return mStartDelay;
+ }
+
/** Returns whether the animator in this class has an infinite duration. */
protected boolean isInfiniteAnimator() {
return mAnimator.getTotalDuration() == Animator.DURATION_INFINITE;
@@ -276,23 +340,20 @@
* animation.
*/
protected static final class QuotaReleasingAnimatorListener extends AnimatorListenerAdapter {
- @NonNull
- private final QuotaManager mQuotaManager;
+ @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 it is skipped by calling {@link android.animation
// .Animator#end()} because no quota is available.
- @NonNull
- final AtomicBoolean mIsUsingQuota = new AtomicBoolean(false);
+ @NonNull final AtomicBoolean mIsUsingQuota = new AtomicBoolean(false);
private final int mRepeatMode;
private final long mForwardRepeatDelay;
private final long mReverseRepeatDelay;
- @NonNull
- private final Handler mHandler;
- @NonNull
- Runnable mResumeRepeatRunnable;
+ @NonNull private final Handler mHandler;
+ @NonNull Runnable mResumeRepeatRunnable;
private boolean mIsReverse;
+
/**
* Only intended to be true with {@link QuotaAwareAnimatorWithAux} to play main and aux
* animators alternately, the pause and resume is still required to swap animators even
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAux.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAux.java
index 3d050a9..fda3478 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAux.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAux.java
@@ -80,44 +80,52 @@
mAnimator.addUpdateListener(
animation -> {
if (!mSuppressForwardUpdate && !mAnimator.isPaused()) {
- updateCallback.onUpdate(animation.getAnimatedValue());
+ mLastAnimatedValue = animation.getAnimatedValue();
+ updateCallback.onUpdate(mLastAnimatedValue);
}
});
mAuxAnimator.addUpdateListener(
animation -> {
if (!mSuppressReverseUpdate && !mAuxAnimator.isPaused()) {
- updateCallback.onUpdate(animation.getAnimatedValue());
+ mLastAnimatedValue = animation.getAnimatedValue();
+ updateCallback.onUpdate(mLastAnimatedValue);
}
});
}
@Override
- void setFloatValues(float... values) {
+ public void setFloatValues(@NonNull float... values) {
super.setFloatValues(values);
- // reverse the value array
+ // Create a copy of the values array before reversing it
+ float[] reversedValues = values.clone();
+
+ // reverse the copied array
float temp;
- for (int i = 0; i < values.length / 2; i++) {
- temp = values[i];
- values[i] = values[values.length - 1 - i];
- values[values.length - 1 - i] = temp;
+ for (int i = 0; i < reversedValues.length / 2; i++) {
+ temp = reversedValues[i];
+ reversedValues[i] = reversedValues[reversedValues.length - 1 - i];
+ reversedValues[reversedValues.length - 1 - i] = temp;
}
- setFloatValues(mAuxAnimator, mEvaluator, values);
+ setFloatValues(mAuxAnimator, mEvaluator, reversedValues);
}
@Override
- void setIntValues(int... values) {
+ public void setIntValues(@NonNull int... values) {
super.setIntValues(values);
- // reverse the value array
+ // Create a copy of the values array before reversing it
+ int[] reversedValues = values.clone();
+
+ // reverse the copied array
int temp;
- for (int i = 0; i < values.length / 2; i++) {
- temp = values[i];
- values[i] = values[values.length - 1 - i];
- values[values.length - 1 - i] = temp;
+ for (int i = 0; i < reversedValues.length / 2; i++) {
+ temp = reversedValues[i];
+ reversedValues[i] = reversedValues[reversedValues.length - 1 - i];
+ reversedValues[reversedValues.length - 1 - i] = temp;
}
- setIntValues(mAuxAnimator, mEvaluator, values);
+ setIntValues(mAuxAnimator, mEvaluator, reversedValues);
}
@Override
@@ -182,4 +190,20 @@
&& mAuxAnimator.isPaused()
&& !HandlerCompat.hasCallbacks(mUiHandler, mAuxListener.mResumeRepeatRunnable);
}
+
+ @Override
+ public void advanceToAnimationTime(long newTime) {
+ if (newTime < mAuxAnimator.getStartDelay()) {
+ super.advanceToAnimationTime(newTime);
+ } else {
+ // Adjust time for the auxiliary animator
+ long adjustedTime = newTime - mAuxAnimator.getStartDelay();
+ mAuxAnimator.setCurrentPlayTime(adjustedTime);
+ }
+ }
+
+ @Override
+ public long getDurationMs() {
+ return mAnimator.getDuration() + mAuxAnimator.getDuration();
+ }
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorTest.java
new file mode 100644
index 0000000..6843fac
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ArgbEvaluator;
+import android.animation.FloatEvaluator;
+import android.animation.IntEvaluator;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class QuotaAwareAnimatorTest {
+
+ private static final int ANIMATION_DURATION = 500;
+ private static final int ANIMATION_START_DELAY = 100;
+ private static final float[] FLOAT_VALUES = {0f, 5f, 10f};
+ private static final int[] INT_VALUES = {0, 50, 100};
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Mock private QuotaManager mMockQuotaManager;
+ private AnimationSpec mDefaultAnimationSpec;
+ private AnimationSpec mAnimationSpecWithDuration;
+
+ @Before
+ public void setUp() {
+ // Create AnimationSpec instances
+ mDefaultAnimationSpec = AnimationSpec.newBuilder().build();
+ mAnimationSpecWithDuration =
+ AnimationSpec.newBuilder()
+ .setDurationMillis(ANIMATION_DURATION)
+ .setStartDelayMillis(ANIMATION_START_DELAY)
+ .build();
+ }
+
+ @Test
+ public void getTypeEvaluator_returnsCorrectEvaluator() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+
+ assertThat(animator.getTypeEvaluator()).isInstanceOf(FloatEvaluator.class);
+ }
+
+ @Test
+ public void setFloatValues_updatesAnimatorValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+
+ animator.setFloatValues(FLOAT_VALUES);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ // Check the animated value at the beginning of the animation
+ animator.advanceToAnimationTime(0);
+ assertThat(animator.getCurrentValue()).isEqualTo(FLOAT_VALUES[0]);
+
+ // Check the animated value at the end of the animation
+ animator.advanceToAnimationTime(ANIMATION_DURATION); // Assuming default 300ms duration
+ assertThat(animator.getCurrentValue()).isEqualTo(FLOAT_VALUES[FLOAT_VALUES.length - 1]);
+ }
+
+ @Test
+ public void setFloatValues_cancelsAnimator() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+ animator.mAnimator.start(); // Simulate running animator
+
+ animator.setFloatValues(FLOAT_VALUES);
+
+ assertThat(animator.mAnimator.isStarted()).isFalse();
+ }
+
+ @Test
+ public void setFloatValues_throwsExceptionWithIncorrectEvaluator() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new IntEvaluator());
+
+ assertThrows(IllegalArgumentException.class, () -> animator.setFloatValues(FLOAT_VALUES));
+ }
+
+ @Test
+ public void setIntValues_updatesAnimatorValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new IntEvaluator());
+
+ animator.setIntValues(INT_VALUES);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ // Check the animated value at the beginning of the animation
+ animator.advanceToAnimationTime(0);
+ assertThat(animator.getCurrentValue()).isEqualTo(INT_VALUES[0]);
+
+ // Check the animated value at the end of the animation
+ animator.advanceToAnimationTime(ANIMATION_DURATION);
+ assertThat(animator.getCurrentValue()).isEqualTo(INT_VALUES[INT_VALUES.length - 1]);
+ }
+
+ @Test
+ public void setIntValues_cancelsAnimator() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new IntEvaluator());
+ animator.mAnimator.start(); // Simulate running animator
+
+ animator.setIntValues(INT_VALUES);
+
+ assertThat(animator.mAnimator.isStarted()).isFalse();
+ }
+
+ @Test
+ public void setIntValues_throwsExceptionWithIncorrectEvaluator() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+
+ assertThrows(IllegalArgumentException.class, () -> animator.setIntValues(INT_VALUES));
+ }
+
+ @Test
+ public void setIntValues_worksWithArgbEvaluator() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new ArgbEvaluator());
+
+ animator.setIntValues(INT_VALUES); // Should not throw an exception
+ }
+
+ @Test
+ public void advanceToAnimationTime_setsCurrentPlayTime() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new FloatEvaluator());
+ long newTime = 250L;
+
+ animator.advanceToAnimationTime(newTime);
+
+ assertThat(animator.mAnimator.getCurrentPlayTime())
+ .isEqualTo(newTime - ANIMATION_START_DELAY);
+ }
+
+ @Test
+ public void getPropertyValuesHolders_returnsAnimatorValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+ animator.setFloatValues(FLOAT_VALUES);
+
+ Object startValue = animator.getStartValue();
+ Object endValue = animator.getEndValue();
+
+ assertThat(startValue).isEqualTo(FLOAT_VALUES[0]);
+ assertThat(endValue).isEqualTo(FLOAT_VALUES[FLOAT_VALUES.length - 1]);
+ }
+
+ @Test
+ public void getCurrentValue_returnsCorrectValue() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+ animator.setFloatValues(0f, 10f);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+ animator.mAnimator.setCurrentPlayTime(150); // Halfway through the default 300ms duration
+
+ Object lastValue = animator.getCurrentValue();
+
+ assertThat(lastValue).isEqualTo(5f); // Expected interpolated value
+ }
+
+ @Test
+ public void getDuration_returnsAnimatorDurationMs() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new FloatEvaluator());
+
+ long duration = animator.getDurationMs();
+
+ assertThat(duration).isEqualTo(ANIMATION_DURATION);
+ }
+
+ @Test
+ public void getStartDelay_returnsAnimatorStartDelayMs() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new FloatEvaluator());
+
+ long startDelay = animator.getStartDelayMs();
+
+ assertThat(startDelay).isEqualTo(ANIMATION_START_DELAY);
+ }
+
+ @Test
+ public void tryStartAnimation_acquiresQuotaAndStarts_whenQuotaAvailable() {
+ when(mMockQuotaManager.tryAcquireQuota(anyInt()))
+ .thenReturn(true); // Simulate quota available
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mDefaultAnimationSpec, new FloatEvaluator());
+ animator.setFloatValues(FLOAT_VALUES);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ animator.tryStartAnimation();
+
+ verify(mMockQuotaManager).tryAcquireQuota(1); // Verify quota acquisition
+ assertThat(animator.mAnimator.isStarted()).isTrue(); // Verify animator started
+ assertThat(animator.mListener.mIsUsingQuota.get()).isTrue(); // Verify quota flag is set
+ }
+
+ @Test
+ public void testStartAndEndValueCaching_FloatValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new FloatEvaluator());
+ float[] values = {10f, 20f, 30f};
+ animator.setFloatValues(values);
+
+ assertEquals(10f, animator.getStartValue());
+ assertEquals(30f, animator.getEndValue());
+ }
+
+ @Test
+ public void testStartAndEndValueCaching_IntValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new IntEvaluator());
+ int[] values = {5, 15, 25};
+ animator.setIntValues(values);
+
+ assertEquals(5, animator.getStartValue());
+ assertEquals(25, animator.getEndValue());
+ }
+
+ @Test
+ public void testStartAndEndValue_BeforeSettingValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new FloatEvaluator());
+ assertNull(animator.getStartValue());
+ assertNull(animator.getEndValue());
+ }
+
+ @Test
+ public void testStartAndEndValue_AfterResettingValues() {
+ QuotaAwareAnimator animator =
+ new QuotaAwareAnimator(
+ mMockQuotaManager, mAnimationSpecWithDuration, new IntEvaluator());
+ animator.setIntValues(1, 2);
+ animator.setIntValues(3, 4);
+
+ assertEquals(3, animator.getStartValue());
+ assertEquals(4, animator.getEndValue());
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAuxTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAuxTest.java
new file mode 100644
index 0000000..c1dccf6
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/QuotaAwareAnimatorWithAuxTest.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.animation.FloatEvaluator;
+import android.animation.IntEvaluator;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class QuotaAwareAnimatorWithAuxTest {
+ private static final int ANIMATION_DURATION = 500;
+ private static final int ANIMATION_START_DELAY = 100;
+ private static final float[] FLOAT_VALUES = {0f, 5f, 10f};
+ private static final int[] INT_VALUES = {0, 50, 100};
+
+ @Mock private QuotaManager mMockQuotaManager;
+
+ private AnimationSpec mDefaultAnimationSpec;
+ private AnimationSpec mAnimationSpecWithDuration;
+
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Before
+ public void setUp() {
+ // Create AnimationSpec instances
+ mDefaultAnimationSpec = AnimationSpec.newBuilder().build();
+ mAnimationSpecWithDuration =
+ AnimationSpec.newBuilder()
+ .setDurationMillis(ANIMATION_DURATION)
+ .setStartDelayMillis(ANIMATION_START_DELAY)
+ .build();
+ }
+
+ @Test
+ public void getTypeEvaluator_returnsCorrectEvaluator_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mDefaultAnimationSpec, auxSpec, new FloatEvaluator());
+
+ assertThat(animator.getTypeEvaluator()).isInstanceOf(FloatEvaluator.class);
+ }
+
+ @Test
+ public void setFloatValues_updatesBothAnimators_withAux() {
+ long mainDuration = 300L;
+ long auxDuration = 400L;
+ long auxStartDelay = 300L;
+
+ AnimationSpec mainSpec =
+ AnimationSpec.newBuilder().setDurationMillis((int) mainDuration).build();
+ AnimationSpec auxSpec =
+ AnimationSpec.newBuilder()
+ .setDurationMillis((int) auxDuration)
+ .setStartDelayMillis((int) auxStartDelay)
+ .build();
+
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mainSpec, auxSpec, new FloatEvaluator());
+
+ animator.setFloatValues(FLOAT_VALUES);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ // Check main animator at the beginning
+ animator.advanceToAnimationTime(0);
+ assertThat(animator.getCurrentValue()).isEqualTo(FLOAT_VALUES[0]);
+
+ // Check main animator at the end
+ animator.advanceToAnimationTime(mainDuration);
+ assertThat(animator.getCurrentValue()).isEqualTo(FLOAT_VALUES[FLOAT_VALUES.length - 1]);
+
+ // Check aux animator at the beginning (should be the reversed end value of the main
+ // animator)
+ animator.advanceToAnimationTime(auxStartDelay);
+ assertThat(animator.getCurrentValue()).isEqualTo(FLOAT_VALUES[FLOAT_VALUES.length - 1]);
+
+ // Check aux animator at the end (should be the reversed start value of the main animator)
+ animator.advanceToAnimationTime(auxStartDelay + auxDuration);
+ assertThat(animator.getCurrentValue()).isEqualTo(FLOAT_VALUES[0]);
+ }
+
+ @Test
+ public void setFloatValues_cancelsBothAnimators_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mDefaultAnimationSpec, auxSpec, new FloatEvaluator());
+ animator.tryStartAnimation(); // Simulate running animators
+
+ animator.setFloatValues(FLOAT_VALUES);
+
+ assertThat(animator.isRunning()).isFalse();
+ }
+
+ @Test
+ public void setFloatValues_throwsExceptionWithIncorrectEvaluator_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mDefaultAnimationSpec, auxSpec, new IntEvaluator());
+
+ assertThrows(IllegalArgumentException.class, () -> animator.setFloatValues(FLOAT_VALUES));
+ }
+
+ @Test
+ public void setIntValues_updatesBothAnimators_withAux() {
+ long mainDuration = 300L;
+ long auxDuration = 400L;
+ long auxStartDelay = 300L;
+
+ AnimationSpec mainSpec =
+ AnimationSpec.newBuilder().setDurationMillis((int) mainDuration).build();
+ AnimationSpec auxSpec =
+ AnimationSpec.newBuilder()
+ .setDurationMillis((int) auxDuration)
+ .setStartDelayMillis((int) auxStartDelay)
+ .build();
+
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mainSpec, auxSpec, new IntEvaluator());
+
+ animator.setIntValues(INT_VALUES);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ // Check main animator at the beginning
+ animator.advanceToAnimationTime(0);
+ assertThat(animator.getCurrentValue()).isEqualTo(INT_VALUES[0]);
+
+ // Check main animator at the end
+ animator.advanceToAnimationTime(mainDuration);
+ assertThat(animator.getCurrentValue()).isEqualTo(INT_VALUES[INT_VALUES.length - 1]);
+
+ // Check aux animator at the beginning (should be the reversed end value of the main
+ // animator)
+ animator.advanceToAnimationTime(auxStartDelay);
+ assertThat(animator.getCurrentValue()).isEqualTo(INT_VALUES[INT_VALUES.length - 1]);
+
+ // Check aux animator at the end (should be the reversed start value of the main animator)
+ animator.advanceToAnimationTime(auxStartDelay + auxDuration);
+ assertThat(animator.getCurrentValue()).isEqualTo(INT_VALUES[0]);
+ }
+
+ @Test
+ public void setIntValues_throwsExceptionWithIncorrectEvaluator_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mDefaultAnimationSpec, auxSpec, new FloatEvaluator());
+
+ assertThrows(IllegalArgumentException.class, () -> animator.setIntValues(INT_VALUES));
+ }
+
+ @Test
+ public void setIntValues_worksWithArgbEvaluator_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager,
+ mDefaultAnimationSpec,
+ auxSpec,
+ AnimatableNode.ARGB_EVALUATOR);
+
+ animator.setIntValues(INT_VALUES); // Should not throw an exception
+ }
+
+ @Test
+ public void getPropertyValuesHolders_returnsMainAnimatorValues_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mDefaultAnimationSpec, auxSpec, new FloatEvaluator());
+ animator.setFloatValues(FLOAT_VALUES);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ Object startValue = animator.getStartValue();
+ Object endValue = animator.getEndValue();
+
+ assertThat(startValue).isEqualTo(FLOAT_VALUES[0]);
+ assertThat(endValue).isEqualTo(FLOAT_VALUES[FLOAT_VALUES.length - 1]);
+ }
+
+ @Test
+ public void getCurrentValue_returnsCorrectValue_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager, mDefaultAnimationSpec, auxSpec, new FloatEvaluator());
+ animator.setFloatValues(0f, 10f);
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+ animator.mAnimator.setCurrentPlayTime(150); // Halfway through the default 300ms duration
+
+ Object lastValue = animator.getCurrentValue();
+
+ assertThat(lastValue).isEqualTo(5f); // Expected interpolated value from main animator
+ }
+
+ @Test
+ public void getDuration_returnsCombinedDuration_Ms_withAux() {
+ long auxDuration = 400L;
+ AnimationSpec auxSpec =
+ AnimationSpec.newBuilder().setDurationMillis((int) auxDuration).build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager,
+ mAnimationSpecWithDuration,
+ auxSpec,
+ new FloatEvaluator());
+
+ long duration = animator.getDurationMs();
+
+ assertThat(duration).isEqualTo(ANIMATION_DURATION + auxDuration);
+ }
+
+ @Test
+ public void getStartDelay_returnsMainAnimatorStartDelay_Ms_withAux() {
+ AnimationSpec auxSpec = AnimationSpec.newBuilder().build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager,
+ mAnimationSpecWithDuration,
+ auxSpec,
+ new FloatEvaluator());
+
+ long startDelay = animator.getStartDelayMs();
+
+ assertThat(startDelay)
+ .isEqualTo(ANIMATION_START_DELAY); // Should return main animator's start delay
+ }
+
+ @Test
+ public void advanceToAnimationTime_setsCurrentPlayTime_onMainAnimator() {
+ long auxStartDelay = 300L;
+ AnimationSpec auxSpec =
+ AnimationSpec.newBuilder().setStartDelayMillis((int) auxStartDelay).build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager,
+ mAnimationSpecWithDuration,
+ auxSpec,
+ new FloatEvaluator());
+ animator.setFloatValues(0f, 10f); // Set values for both animators
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ long newTime = 200L; // Less than aux animator's start delay
+
+ animator.advanceToAnimationTime(newTime);
+
+ // Indirectly verify main animator's current play time
+ assertThat(animator.getCurrentValue())
+ .isEqualTo(
+ new FloatEvaluator()
+ .evaluate(
+ (newTime - ANIMATION_START_DELAY)
+ / (float) ANIMATION_DURATION,
+ 0f,
+ 10f));
+ }
+
+ @Test
+ public void advanceToAnimationTime_setsCurrentPlayTime_onAuxAnimator() {
+ long auxStartDelay = 300L;
+ long auxDuration = 400L;
+ AnimationSpec auxSpec =
+ AnimationSpec.newBuilder()
+ .setStartDelayMillis((int) auxStartDelay)
+ .setDurationMillis((int) auxDuration)
+ .build();
+ QuotaAwareAnimatorWithAux animator =
+ new QuotaAwareAnimatorWithAux(
+ mMockQuotaManager,
+ mAnimationSpecWithDuration,
+ auxSpec,
+ new FloatEvaluator());
+ animator.setFloatValues(0f, 10f); // Set values for both animators
+ animator.addUpdateCallback(value -> {}); // Add an empty update callback
+
+ long newTime = 400L; // Greater than or equal to aux animator's start delay
+
+ animator.advanceToAnimationTime(newTime);
+
+ // Indirectly verify auxiliary animator's current play time
+ assertThat(animator.getCurrentValue())
+ .isEqualTo(
+ new FloatEvaluator()
+ .evaluate(
+ (newTime - auxStartDelay) / (float) auxDuration,
+ 10f,
+ 0f)); // Reversed values in aux animator
+ }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index f76e662..7c4af90 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -743,13 +743,16 @@
open = true)
public @interface FontFamilyName {}
- /** Font family name that uses Roboto font. Supported in renderers supporting 1.4. */
+ /**
+ * Font family name that uses Roboto font. Supported in renderers supporting 1.4, but the
+ * actual availability of this font is dependent on the devices.
+ */
@RequiresSchemaVersion(major = 1, minor = 400)
public static final String ROBOTO_FONT = "roboto";
/**
- * Font family name that uses Roboto Flex variable font. Supported in renderers
- * supporting 1.4.
+ * Font family name that uses Roboto Flex variable font. Supported in renderers supporting
+ * 1.4, but the actual availability of this font is dependent on the devices.
*/
@RequiresSchemaVersion(major = 1, minor = 400)
public static final String ROBOTO_FLEX_FONT = "roboto-flex";
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
index f37b66a..98d2264 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
@@ -253,6 +253,6 @@
@SuppressWarnings("KotlinInternal")
private android.support.wearable.complications.ComplicationData asWireComplicationData(
ComplicationDataTimeline timeline) {
- return timeline.asWireComplicationData$watchface_complications_data_source_debug();
+ return timeline.asWireComplicationData$watchface_complications_data_source_release();
}
}
diff --git a/wear/watchface/watchface-style/build.gradle b/wear/watchface/watchface-style/build.gradle
index 91d5360..333ba3b7 100644
--- a/wear/watchface/watchface-style/build.gradle
+++ b/wear/watchface/watchface-style/build.gradle
@@ -87,7 +87,7 @@
// It makes sure that the apks are generated before the assets are packed.
afterEvaluate {
- tasks.named("generateDebugAndroidTestAssets").configure { it.dependsOn(copyApkTaskProvider) }
+ tasks.named("generateReleaseAndroidTestAssets").configure { it.dependsOn(copyApkTaskProvider) }
}
android {
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
index 52f34d67..4a7305a 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
@@ -39,7 +39,7 @@
public fun resource() {
val wireSizeAndDimensions = testIcon.getWireSizeAndDimensions(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Truth.assertThat(wireSizeAndDimensions.wireSizeBytes).isEqualTo(673)
+ Truth.assertThat(wireSizeAndDimensions.wireSizeBytes).isEqualTo(547)
} else {
Truth.assertThat(wireSizeAndDimensions.wireSizeBytes).isNull()
}
@@ -71,7 +71,7 @@
val estimate = setting.estimateWireSizeInBytesAndValidateIconDimensions(context, 100, 100)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Truth.assertThat(estimate).isEqualTo(708)
+ Truth.assertThat(estimate).isEqualTo(582)
} else {
Truth.assertThat(estimate).isEqualTo(35)
}
@@ -160,7 +160,7 @@
val estimate = setting.estimateWireSizeInBytesAndValidateIconDimensions(context, 100, 100)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Truth.assertThat(estimate).isEqualTo(2800)
+ Truth.assertThat(estimate).isEqualTo(2296)
} else {
Truth.assertThat(estimate).isEqualTo(108)
}
@@ -182,7 +182,7 @@
val estimate = setting.estimateWireSizeInBytesAndValidateIconDimensions(context, 100, 100)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Truth.assertThat(estimate).isEqualTo(767)
+ Truth.assertThat(estimate).isEqualTo(641)
} else {
Truth.assertThat(estimate).isEqualTo(94)
}
@@ -259,7 +259,7 @@
val estimate = setting.estimateWireSizeInBytesAndValidateIconDimensions(context, 100, 100)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Truth.assertThat(estimate).isEqualTo(3592)
+ Truth.assertThat(estimate).isEqualTo(2962)
} else {
Truth.assertThat(estimate).isEqualTo(227)
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 9764305..c065779 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -999,7 +999,6 @@
}
if (watchState.isHeadless) {
headlessWatchFaceImpl!!.release()
- } else {
[email protected]()
}
}
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 80ca709..28b26e9 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
@@ -7756,14 +7756,6 @@
) {
// Intentionally empty.
}
-
- var destroyed: Boolean = false
-
- override fun onDestroy() {
- super.onDestroy()
- assert(!destroyed) { "onDestroy already called!!" }
- destroyed = true
- }
}
)
diff --git a/wear/wear/src/main/java/androidx/wear/widget/drawer/WearableActionDrawerView.java b/wear/wear/src/main/java/androidx/wear/widget/drawer/WearableActionDrawerView.java
index bded2fb..cb595c4 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/drawer/WearableActionDrawerView.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/drawer/WearableActionDrawerView.java
@@ -401,6 +401,7 @@
}
};
+ @SuppressWarnings("UnusedVariable")
ActionListAdapter(Menu menu) {
mActionMenu = getMenu();
}
diff --git a/wear/wear_sdk/README.txt b/wear/wear_sdk/README.txt
index 5f080c8..26122f9 100644
--- a/wear/wear_sdk/README.txt
+++ b/wear/wear_sdk/README.txt
@@ -4,6 +4,6 @@
"The implementation associated with this version containing are"
"preinstalled on WearOS devices."
gerrit source: "vendor/google_clockwork/sdk/lib"
-API version: 34.1
-Build ID: 11505490
-Last updated: Wed Feb 28 02:47:27 AM UTC 2024
+API version: 35.1
+Build ID: 12239970
+Last updated: Fri Aug 16 06:57:41 PM UTC 2024
diff --git a/wear/wear_sdk/wear-sdk.jar b/wear/wear_sdk/wear-sdk.jar
index 16fe240..36f61d4 100644
--- a/wear/wear_sdk/wear-sdk.jar
+++ b/wear/wear_sdk/wear-sdk.jar
Binary files differ
diff --git a/window/extensions/extensions/api/1.4.0-beta01.txt b/window/extensions/extensions/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..8c45b062
--- /dev/null
+++ b/window/extensions/extensions/api/1.4.0-beta01.txt
@@ -0,0 +1,381 @@
+// Signature format: 4.0
+package androidx.window.extensions {
+
+ public interface WindowExtensions {
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
+ method public default int getVendorApiLevel();
+ method public default androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
+ method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
+ }
+
+ public class WindowExtensionsProvider {
+ method public static androidx.window.extensions.WindowExtensions getWindowExtensions();
+ }
+
+}
+
+package androidx.window.extensions.area {
+
+ public interface ExtensionWindowAreaPresentation {
+ method public android.content.Context getPresentationContext();
+ method public default android.view.Window getWindow();
+ method public void setPresentationView(android.view.View);
+ }
+
+ public interface ExtensionWindowAreaStatus {
+ method public android.util.DisplayMetrics getWindowAreaDisplayMetrics();
+ method public int getWindowAreaStatus();
+ }
+
+ public interface WindowAreaComponent {
+ method public default void addRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
+ method public void addRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void endRearDisplayPresentationSession();
+ method public void endRearDisplaySession();
+ method public default android.util.DisplayMetrics getRearDisplayMetrics();
+ method public default androidx.window.extensions.area.ExtensionWindowAreaPresentation? getRearDisplayPresentation();
+ method public default void removeRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
+ method public void removeRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void startRearDisplayPresentationSession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public void startRearDisplaySession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+ field public static final int SESSION_STATE_CONTENT_VISIBLE = 2; // 0x2
+ field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+ field public static final int STATUS_ACTIVE = 3; // 0x3
+ field public static final int STATUS_AVAILABLE = 2; // 0x2
+ field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+ field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+ }
+
+}
+
+package androidx.window.extensions.embedding {
+
+ public interface ActivityEmbeddingComponent {
+ method public default void clearActivityStackAttributesCalculator();
+ method public default void clearEmbeddedActivityWindowInfoCallback();
+ method public void clearSplitAttributesCalculator();
+ method public void clearSplitInfoCallback();
+ method @Deprecated public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
+ method public default void finishActivityStacksWithTokens(java.util.Set<androidx.window.extensions.embedding.ActivityStack.Token!>);
+ method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
+ method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
+ method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
+ method public default void invalidateTopVisibleSplitAttributes();
+ method public boolean isActivityEmbedded(android.app.Activity);
+ method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
+ method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams!,androidx.window.extensions.embedding.ActivityStackAttributes!>);
+ method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.embedding.EmbeddedActivityWindowInfo!>);
+ method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
+ method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
+ method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method public default void unpinTopActivityStack(int);
+ method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
+ method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
+ method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
+ }
+
+ public class ActivityEmbeddingOptionsProperties {
+ field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
+ field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
+ }
+
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ method public boolean shouldAlwaysExpand();
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(androidx.window.extensions.core.util.function.Predicate<android.app.Activity!>, androidx.window.extensions.core.util.function.Predicate<android.content.Intent!>);
+ ctor @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.N) public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+ method public androidx.window.extensions.embedding.ActivityRule build();
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setTag(String);
+ }
+
+ public class ActivityStack {
+ method public java.util.List<android.app.Activity!> getActivities();
+ method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
+ method public String? getTag();
+ method public boolean isEmpty();
+ }
+
+ public static final class ActivityStack.Token {
+ method public static androidx.window.extensions.embedding.ActivityStack.Token createFromBinder(android.os.IBinder);
+ method public static androidx.window.extensions.embedding.ActivityStack.Token readFromBundle(android.os.Bundle);
+ method public android.os.Bundle toBundle();
+ field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
+ }
+
+ public final class ActivityStackAttributes {
+ method public android.graphics.Rect getRelativeBounds();
+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+ }
+
+ public static final class ActivityStackAttributes.Builder {
+ ctor public ActivityStackAttributes.Builder();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes build();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+ }
+
+ public class ActivityStackAttributesCalculatorParams {
+ method public String getActivityStackTag();
+ method public android.os.Bundle getLaunchOptions();
+ method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
+ }
+
+ public abstract class AnimationBackground {
+ method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
+ field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
+ }
+
+ public static class AnimationBackground.ColorBackground extends androidx.window.extensions.embedding.AnimationBackground {
+ method @ColorInt public int getColor();
+ }
+
+ public final class DividerAttributes {
+ method @ColorInt public int getDividerColor();
+ method public int getDividerType();
+ method public float getPrimaryMaxRatio();
+ method public float getPrimaryMinRatio();
+ method @Dimension public int getWidthDp();
+ field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
+ field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
+ field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+ }
+
+ public static final class DividerAttributes.Builder {
+ ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
+ ctor public DividerAttributes.Builder(int);
+ method public androidx.window.extensions.embedding.DividerAttributes build();
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
+ }
+
+ public class EmbeddedActivityWindowInfo {
+ method public android.app.Activity getActivity();
+ method public android.graphics.Rect getActivityStackBounds();
+ method public android.graphics.Rect getTaskBounds();
+ method public boolean isEmbedded();
+ }
+
+ public abstract class EmbeddingRule {
+ method public String? getTag();
+ }
+
+ public class ParentContainerInfo {
+ method public android.content.res.Configuration getConfiguration();
+ method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
+ method public android.view.WindowMetrics getWindowMetrics();
+ }
+
+ public class SplitAttributes {
+ method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
+ method public int getLayoutDirection();
+ method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+ }
+
+ public static final class SplitAttributes.Builder {
+ ctor public SplitAttributes.Builder();
+ ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
+ method public androidx.window.extensions.embedding.SplitAttributes build();
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+ }
+
+ public static final class SplitAttributes.LayoutDirection {
+ field public static final int BOTTOM_TO_TOP = 5; // 0x5
+ field public static final int LEFT_TO_RIGHT = 0; // 0x0
+ field public static final int LOCALE = 3; // 0x3
+ field public static final int RIGHT_TO_LEFT = 1; // 0x1
+ field public static final int TOP_TO_BOTTOM = 4; // 0x4
+ }
+
+ public static class SplitAttributes.SplitType {
+ }
+
+ public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+ ctor public SplitAttributes.SplitType.ExpandContainersSplitType();
+ }
+
+ public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+ ctor public SplitAttributes.SplitType.HingeSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+ method public androidx.window.extensions.embedding.SplitAttributes.SplitType getFallbackSplitType();
+ }
+
+ public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+ ctor public SplitAttributes.SplitType.RatioSplitType(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float);
+ method @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) public float getRatio();
+ method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+ }
+
+ public class SplitAttributesCalculatorParams {
+ method public boolean areDefaultConstraintsSatisfied();
+ method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+ method public android.content.res.Configuration getParentConfiguration();
+ method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+ method public android.view.WindowMetrics getParentWindowMetrics();
+ method public String? getSplitRuleTag();
+ }
+
+ public class SplitInfo {
+ method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+ method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
+ method public androidx.window.extensions.embedding.SplitInfo.Token getSplitInfoToken();
+ method @Deprecated public float getSplitRatio();
+ method @Deprecated public android.os.IBinder getToken();
+ }
+
+ public static final class SplitInfo.Token {
+ method public static androidx.window.extensions.embedding.SplitInfo.Token createFromBinder(android.os.IBinder);
+ }
+
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+ method public boolean shouldClearTop();
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(androidx.window.extensions.core.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, androidx.window.extensions.core.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, androidx.window.extensions.core.util.function.Predicate<android.view.WindowMetrics!>);
+ ctor @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.N) public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPairRule build();
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
+ }
+
+ public class SplitPinRule extends androidx.window.extensions.embedding.SplitRule {
+ method public boolean isSticky();
+ }
+
+ public static final class SplitPinRule.Builder {
+ ctor public SplitPinRule.Builder(androidx.window.extensions.embedding.SplitAttributes, androidx.window.extensions.core.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPinRule build();
+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setSticky(boolean);
+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setTag(String);
+ }
+
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithPlaceholder();
+ method @Deprecated public int getFinishPrimaryWithSecondary();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(android.content.Intent, androidx.window.extensions.core.util.function.Predicate<android.app.Activity!>, androidx.window.extensions.core.util.function.Predicate<android.content.Intent!>, androidx.window.extensions.core.util.function.Predicate<android.view.WindowMetrics!>);
+ ctor @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.N) public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setTag(String);
+ }
+
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+ method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+ method @Deprecated public int getLayoutDirection();
+ method @Deprecated public float getSplitRatio();
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+ public final class WindowAttributes {
+ ctor public WindowAttributes(int);
+ method public int getDimAreaBehavior();
+ field public static final int DIM_AREA_ON_ACTIVITY_STACK = 1; // 0x1
+ field public static final int DIM_AREA_ON_TASK = 2; // 0x2
+ }
+
+}
+
+package androidx.window.extensions.layout {
+
+ public interface DisplayFeature {
+ method public android.graphics.Rect getBounds();
+ }
+
+ public final class DisplayFoldFeature {
+ method public int getType();
+ method public boolean hasProperties(int...);
+ method public boolean hasProperty(int);
+ field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
+ field public static final int TYPE_HINGE = 1; // 0x1
+ field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class DisplayFoldFeature.Builder {
+ ctor public DisplayFoldFeature.Builder(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature build();
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
+ }
+
+ public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
+ ctor public FoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
+ method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
+ field public static final int TYPE_FOLD = 1; // 0x1
+ field public static final int TYPE_HINGE = 2; // 0x2
+ }
+
+ public final class SupportedWindowFeatures {
+ method public java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!> getDisplayFoldFeatures();
+ }
+
+ public static final class SupportedWindowFeatures.Builder {
+ ctor public SupportedWindowFeatures.Builder(java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!>);
+ method public androidx.window.extensions.layout.SupportedWindowFeatures build();
+ }
+
+ public interface WindowLayoutComponent {
+ method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
+ method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ }
+
+ public class WindowLayoutInfo {
+ ctor public WindowLayoutInfo(java.util.List<androidx.window.extensions.layout.DisplayFeature!>);
+ method public java.util.List<androidx.window.extensions.layout.DisplayFeature!> getDisplayFeatures();
+ }
+
+}
+
diff --git a/activity/activity-compose/api/res-1.10.0-beta01.txt b/window/extensions/extensions/api/res-1.4.0-beta01.txt
similarity index 100%
copy from activity/activity-compose/api/res-1.10.0-beta01.txt
copy to window/extensions/extensions/api/res-1.4.0-beta01.txt
diff --git a/window/extensions/extensions/api/restricted_1.4.0-beta01.txt b/window/extensions/extensions/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..6811a24
--- /dev/null
+++ b/window/extensions/extensions/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,382 @@
+// Signature format: 4.0
+package androidx.window.extensions {
+
+ public interface WindowExtensions {
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
+ method public default int getVendorApiLevel();
+ method public default androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
+ method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
+ }
+
+ public class WindowExtensionsProvider {
+ method public static androidx.window.extensions.WindowExtensions getWindowExtensions();
+ }
+
+}
+
+package androidx.window.extensions.area {
+
+ public interface ExtensionWindowAreaPresentation {
+ method public android.content.Context getPresentationContext();
+ method public default android.view.Window getWindow();
+ method public void setPresentationView(android.view.View);
+ }
+
+ public interface ExtensionWindowAreaStatus {
+ method public android.util.DisplayMetrics getWindowAreaDisplayMetrics();
+ method public int getWindowAreaStatus();
+ }
+
+ public interface WindowAreaComponent {
+ method public default void addRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
+ method public void addRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void endRearDisplayPresentationSession();
+ method public void endRearDisplaySession();
+ method public default android.util.DisplayMetrics getRearDisplayMetrics();
+ method public default androidx.window.extensions.area.ExtensionWindowAreaPresentation? getRearDisplayPresentation();
+ method public default void removeRearDisplayPresentationStatusListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.area.ExtensionWindowAreaStatus!>);
+ method public void removeRearDisplayStatusListener(androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public default void startRearDisplayPresentationSession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ method public void startRearDisplaySession(android.app.Activity, androidx.window.extensions.core.util.function.Consumer<java.lang.Integer!>);
+ field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+ field public static final int SESSION_STATE_CONTENT_VISIBLE = 2; // 0x2
+ field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+ field public static final int STATUS_ACTIVE = 3; // 0x3
+ field public static final int STATUS_AVAILABLE = 2; // 0x2
+ field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+ field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+ }
+
+}
+
+package androidx.window.extensions.embedding {
+
+ public interface ActivityEmbeddingComponent {
+ method public default void clearActivityStackAttributesCalculator();
+ method public default void clearEmbeddedActivityWindowInfoCallback();
+ method public void clearSplitAttributesCalculator();
+ method public void clearSplitInfoCallback();
+ method @Deprecated public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
+ method public default void finishActivityStacksWithTokens(java.util.Set<androidx.window.extensions.embedding.ActivityStack.Token!>);
+ method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
+ method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
+ method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
+ method public default void invalidateTopVisibleSplitAttributes();
+ method public boolean isActivityEmbedded(android.app.Activity);
+ method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
+ method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams!,androidx.window.extensions.embedding.ActivityStackAttributes!>);
+ method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.embedding.EmbeddedActivityWindowInfo!>);
+ method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
+ method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
+ method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ method public default void unpinTopActivityStack(int);
+ method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+ method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
+ method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
+ method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
+ }
+
+ public class ActivityEmbeddingOptionsProperties {
+ field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
+ field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
+ }
+
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ method public boolean shouldAlwaysExpand();
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(androidx.window.extensions.core.util.function.Predicate<android.app.Activity!>, androidx.window.extensions.core.util.function.Predicate<android.content.Intent!>);
+ ctor @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.N) public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+ method public androidx.window.extensions.embedding.ActivityRule build();
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setTag(String);
+ }
+
+ public class ActivityStack {
+ method public java.util.List<android.app.Activity!> getActivities();
+ method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
+ method public String? getTag();
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.os.IBinder getToken();
+ method public boolean isEmpty();
+ }
+
+ public static final class ActivityStack.Token {
+ method public static androidx.window.extensions.embedding.ActivityStack.Token createFromBinder(android.os.IBinder);
+ method public static androidx.window.extensions.embedding.ActivityStack.Token readFromBundle(android.os.Bundle);
+ method public android.os.Bundle toBundle();
+ field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
+ }
+
+ public final class ActivityStackAttributes {
+ method public android.graphics.Rect getRelativeBounds();
+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+ }
+
+ public static final class ActivityStackAttributes.Builder {
+ ctor public ActivityStackAttributes.Builder();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes build();
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+ }
+
+ public class ActivityStackAttributesCalculatorParams {
+ method public String getActivityStackTag();
+ method public android.os.Bundle getLaunchOptions();
+ method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
+ }
+
+ public abstract class AnimationBackground {
+ method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
+ field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
+ }
+
+ public static class AnimationBackground.ColorBackground extends androidx.window.extensions.embedding.AnimationBackground {
+ method @ColorInt public int getColor();
+ }
+
+ public final class DividerAttributes {
+ method @ColorInt public int getDividerColor();
+ method public int getDividerType();
+ method public float getPrimaryMaxRatio();
+ method public float getPrimaryMinRatio();
+ method @Dimension public int getWidthDp();
+ field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
+ field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
+ field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+ }
+
+ public static final class DividerAttributes.Builder {
+ ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
+ ctor public DividerAttributes.Builder(int);
+ method public androidx.window.extensions.embedding.DividerAttributes build();
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
+ }
+
+ public class EmbeddedActivityWindowInfo {
+ method public android.app.Activity getActivity();
+ method public android.graphics.Rect getActivityStackBounds();
+ method public android.graphics.Rect getTaskBounds();
+ method public boolean isEmbedded();
+ }
+
+ public abstract class EmbeddingRule {
+ method public String? getTag();
+ }
+
+ public class ParentContainerInfo {
+ method public android.content.res.Configuration getConfiguration();
+ method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
+ method public android.view.WindowMetrics getWindowMetrics();
+ }
+
+ public class SplitAttributes {
+ method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+ method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
+ method public int getLayoutDirection();
+ method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+ }
+
+ public static final class SplitAttributes.Builder {
+ ctor public SplitAttributes.Builder();
+ ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
+ method public androidx.window.extensions.embedding.SplitAttributes build();
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+ }
+
+ public static final class SplitAttributes.LayoutDirection {
+ field public static final int BOTTOM_TO_TOP = 5; // 0x5
+ field public static final int LEFT_TO_RIGHT = 0; // 0x0
+ field public static final int LOCALE = 3; // 0x3
+ field public static final int RIGHT_TO_LEFT = 1; // 0x1
+ field public static final int TOP_TO_BOTTOM = 4; // 0x4
+ }
+
+ public static class SplitAttributes.SplitType {
+ }
+
+ public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+ ctor public SplitAttributes.SplitType.ExpandContainersSplitType();
+ }
+
+ public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+ ctor public SplitAttributes.SplitType.HingeSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+ method public androidx.window.extensions.embedding.SplitAttributes.SplitType getFallbackSplitType();
+ }
+
+ public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+ ctor public SplitAttributes.SplitType.RatioSplitType(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float);
+ method @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) public float getRatio();
+ method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+ }
+
+ public class SplitAttributesCalculatorParams {
+ method public boolean areDefaultConstraintsSatisfied();
+ method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+ method public android.content.res.Configuration getParentConfiguration();
+ method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+ method public android.view.WindowMetrics getParentWindowMetrics();
+ method public String? getSplitRuleTag();
+ }
+
+ public class SplitInfo {
+ method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+ method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
+ method public androidx.window.extensions.embedding.SplitInfo.Token getSplitInfoToken();
+ method @Deprecated public float getSplitRatio();
+ method @Deprecated public android.os.IBinder getToken();
+ }
+
+ public static final class SplitInfo.Token {
+ method public static androidx.window.extensions.embedding.SplitInfo.Token createFromBinder(android.os.IBinder);
+ }
+
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+ method public boolean shouldClearTop();
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(androidx.window.extensions.core.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, androidx.window.extensions.core.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, androidx.window.extensions.core.util.function.Predicate<android.view.WindowMetrics!>);
+ ctor @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.N) public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPairRule build();
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
+ }
+
+ public class SplitPinRule extends androidx.window.extensions.embedding.SplitRule {
+ method public boolean isSticky();
+ }
+
+ public static final class SplitPinRule.Builder {
+ ctor public SplitPinRule.Builder(androidx.window.extensions.embedding.SplitAttributes, androidx.window.extensions.core.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPinRule build();
+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setSticky(boolean);
+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setTag(String);
+ }
+
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithPlaceholder();
+ method @Deprecated public int getFinishPrimaryWithSecondary();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(android.content.Intent, androidx.window.extensions.core.util.function.Predicate<android.app.Activity!>, androidx.window.extensions.core.util.function.Predicate<android.content.Intent!>, androidx.window.extensions.core.util.function.Predicate<android.view.WindowMetrics!>);
+ ctor @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.N) public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setTag(String);
+ }
+
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+ method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+ method @Deprecated public int getLayoutDirection();
+ method @Deprecated public float getSplitRatio();
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+ public final class WindowAttributes {
+ ctor public WindowAttributes(int);
+ method public int getDimAreaBehavior();
+ field public static final int DIM_AREA_ON_ACTIVITY_STACK = 1; // 0x1
+ field public static final int DIM_AREA_ON_TASK = 2; // 0x2
+ }
+
+}
+
+package androidx.window.extensions.layout {
+
+ public interface DisplayFeature {
+ method public android.graphics.Rect getBounds();
+ }
+
+ public final class DisplayFoldFeature {
+ method public int getType();
+ method public boolean hasProperties(int...);
+ method public boolean hasProperty(int);
+ field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
+ field public static final int TYPE_HINGE = 1; // 0x1
+ field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
+ field public static final int TYPE_UNKNOWN = 0; // 0x0
+ }
+
+ public static final class DisplayFoldFeature.Builder {
+ ctor public DisplayFoldFeature.Builder(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
+ method public androidx.window.extensions.layout.DisplayFoldFeature build();
+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
+ }
+
+ public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
+ ctor public FoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
+ method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
+ field public static final int TYPE_FOLD = 1; // 0x1
+ field public static final int TYPE_HINGE = 2; // 0x2
+ }
+
+ public final class SupportedWindowFeatures {
+ method public java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!> getDisplayFoldFeatures();
+ }
+
+ public static final class SupportedWindowFeatures.Builder {
+ ctor public SupportedWindowFeatures.Builder(java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!>);
+ method public androidx.window.extensions.layout.SupportedWindowFeatures build();
+ }
+
+ public interface WindowLayoutComponent {
+ method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
+ method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+ }
+
+ public class WindowLayoutInfo {
+ ctor public WindowLayoutInfo(java.util.List<androidx.window.extensions.layout.DisplayFeature!>);
+ method public java.util.List<androidx.window.extensions.layout.DisplayFeature!> getDisplayFeatures();
+ }
+
+}
+
diff --git a/window/window-core/api/current.txt b/window/window-core/api/current.txt
index aa7ee82..2c8a4d2 100644
--- a/window/window-core/api/current.txt
+++ b/window/window-core/api/current.txt
@@ -8,39 +8,68 @@
package androidx.window.core.layout {
- public final class WindowHeightSizeClass {
- field public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
- field public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
- field public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
- field public static final androidx.window.core.layout.WindowHeightSizeClass MEDIUM;
+ @Deprecated public final class WindowHeightSizeClass {
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass MEDIUM;
}
- public static final class WindowHeightSizeClass.Companion {
+ @Deprecated public static final class WindowHeightSizeClass.Companion {
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getCOMPACT();
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getEXPANDED();
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getMEDIUM();
+ property @Deprecated public androidx.window.core.layout.WindowHeightSizeClass COMPACT;
+ property @Deprecated public androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
+ property @Deprecated public androidx.window.core.layout.WindowHeightSizeClass MEDIUM;
}
public final class WindowSizeClass {
- method public static androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowCoreApi public static androidx.window.core.layout.WindowSizeClass compute(int widthPx, int heightPx, float density);
- method public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
- method public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- property public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
- property public final androidx.window.core.layout.WindowWidthSizeClass windowWidthSizeClass;
+ ctor public WindowSizeClass(float widthDp, float heightDp);
+ ctor public WindowSizeClass(int minWidthDp, int minHeightDp);
+ method @Deprecated public static androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
+ method public int getMinHeightDp();
+ method public int getMinWidthDp();
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
+ method public boolean isAtLeast(int widthDp, int heightDp);
+ method public boolean isHeightAtLeast(int heightDp);
+ method public boolean isWidthAtLeast(int widthDp);
+ property public final int minHeightDp;
+ property public final int minWidthDp;
+ property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
+ property @Deprecated public final androidx.window.core.layout.WindowWidthSizeClass windowWidthSizeClass;
+ field public static final java.util.Set<androidx.window.core.layout.WindowSizeClass> BREAKPOINTS_V1;
field public static final androidx.window.core.layout.WindowSizeClass.Companion Companion;
+ field public static final int HEIGHT_DP_EXPANDED_LOWER_BOUND = 900; // 0x384
+ field public static final int HEIGHT_DP_MEDIUM_LOWER_BOUND = 480; // 0x1e0
+ field public static final int WIDTH_DP_EXPANDED_LOWER_BOUND = 840; // 0x348
+ field public static final int WIDTH_DP_MEDIUM_LOWER_BOUND = 600; // 0x258
}
public static final class WindowSizeClass.Companion {
- method public androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowCoreApi public androidx.window.core.layout.WindowSizeClass compute(int widthPx, int heightPx, float density);
+ method @Deprecated public androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
}
- public final class WindowWidthSizeClass {
- field public static final androidx.window.core.layout.WindowWidthSizeClass COMPACT;
- field public static final androidx.window.core.layout.WindowWidthSizeClass.Companion Companion;
- field public static final androidx.window.core.layout.WindowWidthSizeClass EXPANDED;
- field public static final androidx.window.core.layout.WindowWidthSizeClass MEDIUM;
+ public final class WindowSizeClassSelectors {
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClassPreferHeight(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
}
- public static final class WindowWidthSizeClass.Companion {
+ @Deprecated public final class WindowWidthSizeClass {
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass COMPACT;
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass.Companion Companion;
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass EXPANDED;
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass MEDIUM;
+ }
+
+ @Deprecated public static final class WindowWidthSizeClass.Companion {
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getCOMPACT();
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getEXPANDED();
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getMEDIUM();
+ property @Deprecated public androidx.window.core.layout.WindowWidthSizeClass COMPACT;
+ property @Deprecated public androidx.window.core.layout.WindowWidthSizeClass EXPANDED;
+ property @Deprecated public androidx.window.core.layout.WindowWidthSizeClass MEDIUM;
}
}
diff --git a/window/window-core/api/restricted_current.txt b/window/window-core/api/restricted_current.txt
index aa7ee82..2c8a4d2 100644
--- a/window/window-core/api/restricted_current.txt
+++ b/window/window-core/api/restricted_current.txt
@@ -8,39 +8,68 @@
package androidx.window.core.layout {
- public final class WindowHeightSizeClass {
- field public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
- field public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
- field public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
- field public static final androidx.window.core.layout.WindowHeightSizeClass MEDIUM;
+ @Deprecated public final class WindowHeightSizeClass {
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
+ field @Deprecated public static final androidx.window.core.layout.WindowHeightSizeClass MEDIUM;
}
- public static final class WindowHeightSizeClass.Companion {
+ @Deprecated public static final class WindowHeightSizeClass.Companion {
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getCOMPACT();
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getEXPANDED();
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getMEDIUM();
+ property @Deprecated public androidx.window.core.layout.WindowHeightSizeClass COMPACT;
+ property @Deprecated public androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
+ property @Deprecated public androidx.window.core.layout.WindowHeightSizeClass MEDIUM;
}
public final class WindowSizeClass {
- method public static androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowCoreApi public static androidx.window.core.layout.WindowSizeClass compute(int widthPx, int heightPx, float density);
- method public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
- method public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- property public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
- property public final androidx.window.core.layout.WindowWidthSizeClass windowWidthSizeClass;
+ ctor public WindowSizeClass(float widthDp, float heightDp);
+ ctor public WindowSizeClass(int minWidthDp, int minHeightDp);
+ method @Deprecated public static androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
+ method public int getMinHeightDp();
+ method public int getMinWidthDp();
+ method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
+ method public boolean isAtLeast(int widthDp, int heightDp);
+ method public boolean isHeightAtLeast(int heightDp);
+ method public boolean isWidthAtLeast(int widthDp);
+ property public final int minHeightDp;
+ property public final int minWidthDp;
+ property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
+ property @Deprecated public final androidx.window.core.layout.WindowWidthSizeClass windowWidthSizeClass;
+ field public static final java.util.Set<androidx.window.core.layout.WindowSizeClass> BREAKPOINTS_V1;
field public static final androidx.window.core.layout.WindowSizeClass.Companion Companion;
+ field public static final int HEIGHT_DP_EXPANDED_LOWER_BOUND = 900; // 0x384
+ field public static final int HEIGHT_DP_MEDIUM_LOWER_BOUND = 480; // 0x1e0
+ field public static final int WIDTH_DP_EXPANDED_LOWER_BOUND = 840; // 0x348
+ field public static final int WIDTH_DP_MEDIUM_LOWER_BOUND = 600; // 0x258
}
public static final class WindowSizeClass.Companion {
- method public androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowCoreApi public androidx.window.core.layout.WindowSizeClass compute(int widthPx, int heightPx, float density);
+ method @Deprecated public androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
}
- public final class WindowWidthSizeClass {
- field public static final androidx.window.core.layout.WindowWidthSizeClass COMPACT;
- field public static final androidx.window.core.layout.WindowWidthSizeClass.Companion Companion;
- field public static final androidx.window.core.layout.WindowWidthSizeClass EXPANDED;
- field public static final androidx.window.core.layout.WindowWidthSizeClass MEDIUM;
+ public final class WindowSizeClassSelectors {
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClassPreferHeight(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
}
- public static final class WindowWidthSizeClass.Companion {
+ @Deprecated public final class WindowWidthSizeClass {
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass COMPACT;
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass.Companion Companion;
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass EXPANDED;
+ field @Deprecated public static final androidx.window.core.layout.WindowWidthSizeClass MEDIUM;
+ }
+
+ @Deprecated public static final class WindowWidthSizeClass.Companion {
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getCOMPACT();
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getEXPANDED();
+ method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getMEDIUM();
+ property @Deprecated public androidx.window.core.layout.WindowWidthSizeClass COMPACT;
+ property @Deprecated public androidx.window.core.layout.WindowWidthSizeClass EXPANDED;
+ property @Deprecated public androidx.window.core.layout.WindowWidthSizeClass MEDIUM;
}
}
diff --git a/window/window-core/build.gradle b/window/window-core/build.gradle
index a484af0..926941d 100644
--- a/window/window-core/build.gradle
+++ b/window/window-core/build.gradle
@@ -26,12 +26,18 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
}
androidXMultiplatform {
jvm()
- android()
+ androidLibrary {
+ namespace = "androidx.window.core"
+ withAndroidTestOnDeviceBuilder {
+ it.compilationName = "instrumentedTest"
+ it.defaultSourceSetName = "androidInstrumentedTest"
+ it.sourceSetTreeName = "test"
+ }
+ }
mac()
linux()
ios()
@@ -69,10 +75,6 @@
enableBinaryCompatibilityValidator = false
}
-android {
- namespace "androidx.window.core"
-}
-
androidx {
name = "WindowManager Core"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowHeightSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowHeightSizeClass.kt
index e69a63f..1eccac7 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowHeightSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowHeightSizeClass.kt
@@ -16,9 +16,6 @@
package androidx.window.core.layout
-import androidx.window.core.layout.WindowHeightSizeClass.Companion.COMPACT
-import androidx.window.core.layout.WindowHeightSizeClass.Companion.EXPANDED
-import androidx.window.core.layout.WindowHeightSizeClass.Companion.MEDIUM
import kotlin.jvm.JvmField
/**
@@ -27,6 +24,8 @@
* type. It is possible to have resizeable windows in different device types. The viewport might
* change from a [COMPACT] all the way to an [EXPANDED] size class.
*/
+@Suppress("DEPRECATION")
+@Deprecated("WindowHeightSizeClass will not be developed further, use WindowSizeClass instead.")
class WindowHeightSizeClass private constructor(private val rawValue: Int) {
override fun toString(): String {
@@ -56,16 +55,22 @@
companion object {
/** A bucket to represent a compact height, typical for a phone that is in landscape. */
- @JvmField val COMPACT: WindowHeightSizeClass = WindowHeightSizeClass(0)
+ @Deprecated("WindowHeightSizeClass not be developed further.")
+ @JvmField
+ val COMPACT: WindowHeightSizeClass = WindowHeightSizeClass(0)
/** A bucket to represent a medium height, typical for a phone in portrait or a tablet. */
- @JvmField val MEDIUM: WindowHeightSizeClass = WindowHeightSizeClass(1)
+ @Deprecated("WindowHeightSizeClass not be developed further.")
+ @JvmField
+ val MEDIUM: WindowHeightSizeClass = WindowHeightSizeClass(1)
/**
* A bucket to represent an expanded height window, typical for a large tablet or a desktop
* form-factor.
*/
- @JvmField val EXPANDED: WindowHeightSizeClass = WindowHeightSizeClass(2)
+ @Deprecated("WindowHeightSizeClass not be developed further.")
+ @JvmField
+ val EXPANDED: WindowHeightSizeClass = WindowHeightSizeClass(2)
/**
* Returns a recommended [WindowHeightSizeClass] for the height of a window given the height
@@ -75,6 +80,7 @@
* @return A recommended size class for the height
* @throws IllegalArgumentException if the height is negative
*/
+ @Deprecated("WindowHeightSizeClass not be developed further.")
internal fun compute(dpHeight: Float): WindowHeightSizeClass {
require(dpHeight >= 0) { "Height must be positive, received $dpHeight" }
return when {
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
index 9df0361..43bef20 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
@@ -16,30 +16,28 @@
package androidx.window.core.layout
-import androidx.window.core.ExperimentalWindowCoreApi
+import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic
/**
- * [WindowSizeClass] represents breakpoints for a viewport. The recommended width and height break
- * points are presented through [windowWidthSizeClass] and [windowHeightSizeClass]. Designers should
- * design around the different combinations of width and height buckets. Developers should use the
- * different buckets to specify the layouts. Ideally apps will work well in each bucket and by
- * extension work well across multiple devices. If two devices are in similar buckets they should
- * behave similarly.
+ * [WindowSizeClass] represents breakpoints for a viewport. Designers should design around the
+ * different combinations of width and height buckets. Developers should use the different buckets
+ * to specify the layouts. Ideally apps will work well in each bucket and by extension work well
+ * across multiple devices. If two devices are in similar buckets they should behave similarly.
*
* This class is meant to be a common definition that can be shared across different device types.
- * Application developers can use WindowSizeClass to have standard window buckets and design the UI
- * around those buckets. Library developers can use these buckets to create different UI with
+ * Application developers can use [WindowSizeClass] to have standard window buckets and design the
+ * UI around those buckets. Library developers can use these buckets to create different UI with
* respect to each bucket. This will help with consistency across multiple device types.
*
* A library developer use-case can be creating some navigation UI library. For a size class with
- * the [WindowWidthSizeClass.EXPANDED] width it might be more reasonable to have a side navigation.
- * For a [WindowWidthSizeClass.COMPACT] width, a bottom navigation might be a better fit.
+ * the [WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND] width it might be more reasonable to have a
+ * side navigation.
*
* An application use-case can be applied for apps that use a list-detail pattern. The app can use
- * the [WindowWidthSizeClass.MEDIUM] to determine if there is enough space to show the list and the
- * detail side by side. If all apps follow this guidance then it will present a very consistent user
- * experience.
+ * the [WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND] to determine if there is enough space to show
+ * the list and the detail side by side. If all apps follow this guidance then it will present a
+ * very consistent user experience.
*
* In some cases developers or UI systems may decide to create their own break points. A developer
* might optimize for a window that is smaller than the supported break points or larger. A UI
@@ -50,13 +48,58 @@
* @see WindowWidthSizeClass
* @see WindowHeightSizeClass
*/
-class WindowSizeClass
-private constructor(
+class WindowSizeClass(
+ /** Returns the lower bound for the width of the size class in dp. */
+ val minWidthDp: Int,
+ /** Returns the lower bound for the height of the size class in dp. */
+ val minHeightDp: Int
+) {
+
+ /** A convenience constructor that will truncate to ints. */
+ constructor(widthDp: Float, heightDp: Float) : this(widthDp.toInt(), heightDp.toInt())
+
+ init {
+ require(minWidthDp >= 0) {
+ "Expected minWidthDp to be at least 0, minWidthDp: $minWidthDp."
+ }
+ require(minHeightDp >= 0) {
+ "Expected minHeightDp to be at least 0, minHeightDp: $minHeightDp."
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ @Deprecated("Use either isWidthAtLeast or isAtLeast to check matching bounds.")
/** Returns the [WindowWidthSizeClass] that corresponds to the widthDp of the window. */
- val windowWidthSizeClass: WindowWidthSizeClass,
+ val windowWidthSizeClass: WindowWidthSizeClass
+ get() = WindowWidthSizeClass.compute(minWidthDp.toFloat())
+
+ @Suppress("DEPRECATION")
+ @Deprecated("Use either isHeightAtLeast or isAtLeast to check matching bounds.")
/** Returns the [WindowHeightSizeClass] that corresponds to the heightDp of the window. */
val windowHeightSizeClass: WindowHeightSizeClass
-) {
+ get() = WindowHeightSizeClass.compute(minHeightDp.toFloat())
+
+ /**
+ * Returns `true` when [widthDp] is greater than or equal to [minWidthDp], `false` otherwise.
+ */
+ fun isWidthAtLeast(widthDp: Int): Boolean {
+ return widthDp >= minWidthDp
+ }
+
+ /**
+ * Returns `true` when [heightDp] is greater than or equal to [minHeightDp], `false` otherwise.
+ */
+ fun isHeightAtLeast(heightDp: Int): Boolean {
+ return heightDp >= minHeightDp
+ }
+
+ /**
+ * Returns `true` when [widthDp] is greater than or equal to [minWidthDp] and [heightDp] is
+ * greater than or equal to [minHeightDp], `false` otherwise.
+ */
+ fun isAtLeast(widthDp: Int, heightDp: Int): Boolean {
+ return isWidthAtLeast(widthDp) && isHeightAtLeast(heightDp)
+ }
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -64,25 +107,49 @@
other as WindowSizeClass
- if (windowWidthSizeClass != other.windowWidthSizeClass) return false
- if (windowHeightSizeClass != other.windowHeightSizeClass) return false
+ if (minWidthDp != other.minWidthDp) return false
+ if (minHeightDp != other.minHeightDp) return false
return true
}
override fun hashCode(): Int {
- var result = windowWidthSizeClass.hashCode()
- result = 31 * result + windowHeightSizeClass.hashCode()
+ var result = minWidthDp
+ result = 31 * result + minHeightDp
return result
}
override fun toString(): String {
- return "WindowSizeClass {" +
- "windowWidthSizeClass=$windowWidthSizeClass, " +
- "windowHeightSizeClass=$windowHeightSizeClass }"
+ return "WindowSizeClass(minWidthDp=$minWidthDp, minHeightDp=$minHeightDp)"
}
companion object {
+ /** A lower bound for a size class with Medium width in dp. */
+ const val WIDTH_DP_MEDIUM_LOWER_BOUND = 600
+
+ /** A lower bound for a size class with Expanded width in dp. */
+ const val WIDTH_DP_EXPANDED_LOWER_BOUND = 840
+
+ /** A lower bound for a size class with Medium height in dp. */
+ const val HEIGHT_DP_MEDIUM_LOWER_BOUND = 480
+
+ /** A lower bound for a size class with Expanded height in dp. */
+ const val HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
+
+ private val WIDTH_DP_BREAKPOINTS_V1 =
+ listOf(WIDTH_DP_MEDIUM_LOWER_BOUND, WIDTH_DP_EXPANDED_LOWER_BOUND)
+
+ private val HEIGHT_DP_BREAKPOINTS_V1 =
+ listOf(HEIGHT_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+
+ @JvmField
+ val BREAKPOINTS_V1 =
+ WIDTH_DP_BREAKPOINTS_V1.flatMap { widthBp ->
+ HEIGHT_DP_BREAKPOINTS_V1.map { heightBp ->
+ WindowSizeClass(minWidthDp = widthBp, minHeightDp = heightBp)
+ }
+ }
+ .toSet()
/**
* Computes the recommended [WindowSizeClass] for the given width and height in DP.
@@ -93,28 +160,21 @@
* @throws IllegalArgumentException if [dpWidth] or [dpHeight] is negative.
*/
@JvmStatic
+ @Deprecated("Use the constructor instead.")
fun compute(dpWidth: Float, dpHeight: Float): WindowSizeClass {
- return WindowSizeClass(
- WindowWidthSizeClass.compute(dpWidth),
- WindowHeightSizeClass.compute(dpHeight)
- )
- }
-
- /**
- * Computes the [WindowSizeClass] for the given width and height in pixels with density.
- *
- * @param widthPx width of a window in PX.
- * @param heightPx height of a window in PX.
- * @param density density of the display where the window is shown.
- * @return [WindowSizeClass] that is recommended for the given dimensions.
- * @throws IllegalArgumentException if [widthPx], [heightPx], or [density] is negative.
- */
- @JvmStatic
- @ExperimentalWindowCoreApi
- fun compute(widthPx: Int, heightPx: Int, density: Float): WindowSizeClass {
- val widthDp = widthPx / density
- val heightDp = heightPx / density
- return compute(widthDp, heightDp)
+ val widthDp =
+ when {
+ dpWidth >= WIDTH_DP_EXPANDED_LOWER_BOUND -> WIDTH_DP_EXPANDED_LOWER_BOUND
+ dpWidth >= WIDTH_DP_MEDIUM_LOWER_BOUND -> WIDTH_DP_MEDIUM_LOWER_BOUND
+ else -> 0
+ }
+ val heightDp =
+ when {
+ dpHeight >= HEIGHT_DP_EXPANDED_LOWER_BOUND -> HEIGHT_DP_EXPANDED_LOWER_BOUND
+ dpHeight >= HEIGHT_DP_MEDIUM_LOWER_BOUND -> HEIGHT_DP_MEDIUM_LOWER_BOUND
+ else -> 0
+ }
+ return WindowSizeClass(widthDp, heightDp)
}
}
}
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt
new file mode 100644
index 0000000..25ef7c0
--- /dev/null
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("WindowSizeClassSelectors")
+
+package androidx.window.core.layout
+
+import kotlin.jvm.JvmName
+
+/**
+ * Returns the largest [WindowSizeClass] that is within the bounds of ([widthDp], [heightDp]). This
+ * method prefers width and uses max height to break ties. If there is no match a default of
+ * `WindowSizeClass(0,0)` is returned. Examples: Input: Set: `setOf(WindowSizeClass(300, 300),
+ * WindowSizeClass(300, 600)` widthDp: `300` heightDp: `800` Output: `WindowSizeClass(300, 600)`
+ * Input: Set: `setOf(WindowSizeClass(300, 300), WindowSizeClass(300, 600)` widthDp: `300` heightDp:
+ * `400` Output: `WindowSizeClass(300, 300)`
+ *
+ * @param widthDp the width of the window to match a [WindowSizeClass] to.
+ * @param heightDp the height of the window to match a [WindowSizeClass] to.
+ */
+fun Set<WindowSizeClass>.computeWindowSizeClass(widthDp: Int, heightDp: Int): WindowSizeClass {
+ var maxWidth = 0
+ forEach { bucket ->
+ if (bucket.minWidthDp <= widthDp && bucket.minWidthDp > maxWidth) {
+ maxWidth = bucket.minWidthDp
+ }
+ }
+ var match = WindowSizeClass(0, 0)
+ forEach { bucket ->
+ if (
+ bucket.minWidthDp == maxWidth &&
+ bucket.minHeightDp <= heightDp &&
+ match.minHeightDp < bucket.minHeightDp
+ ) {
+ match = bucket
+ }
+ }
+ return match
+}
+
+/**
+ * Returns the largest [WindowSizeClass] that is within the bounds of ([widthDp], [heightDp]). This
+ * method prefers height and uses max width to break ties. If there is no match a default of
+ * `WindowSizeClass(0,0)` is returned. Examples: Input: Set: `setOf(WindowSizeClass(300, 300),
+ * WindowSizeClass(600, 300)` widthDp: `800` heightDp: `300` Output: `WindowSizeClass(600, 300)`
+ * Input: Set: `setOf(WindowSizeClass(300, 300), WindowSizeClass(600, 300)` widthDp: `400` heightDp:
+ * `300` Output: `WindowSizeClass(300, 300)`
+ *
+ * @param widthDp the width of the window to match a [WindowSizeClass] to.
+ * @param heightDp the height of the window to match a [WindowSizeClass] to.
+ */
+fun Set<WindowSizeClass>.computeWindowSizeClassPreferHeight(
+ widthDp: Int,
+ heightDp: Int
+): WindowSizeClass {
+ var maxHeight = 0
+ forEach { bucket ->
+ if (bucket.minHeightDp <= heightDp && bucket.minHeightDp > maxHeight) {
+ maxHeight = bucket.minHeightDp
+ }
+ }
+ var match = WindowSizeClass(0, 0)
+ forEach { bucket ->
+ if (
+ bucket.minHeightDp == maxHeight &&
+ bucket.minWidthDp <= widthDp &&
+ match.minWidthDp < bucket.minWidthDp
+ ) {
+ match = bucket
+ }
+ }
+ return match
+}
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowWidthSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowWidthSizeClass.kt
index 11af3a0..3097d8f 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowWidthSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowWidthSizeClass.kt
@@ -24,7 +24,10 @@
* type. It is possible to have resizeable windows in different device types. The viewport might
* change from a [COMPACT] all the way to an [EXPANDED] size class.
*/
+@Suppress("DEPRECATION")
+@Deprecated("WindowWidthSizeClass will not be developed further, use WindowSizeClass instead.")
class WindowWidthSizeClass private constructor(private val rawValue: Int) {
+
override fun toString(): String {
val name =
when (this) {
@@ -52,19 +55,25 @@
companion object {
/** A bucket to represent a compact width window, typical for a phone in portrait. */
- @JvmField val COMPACT: WindowWidthSizeClass = WindowWidthSizeClass(0)
+ @Deprecated("WindowWidthSizeClass not be developed further.")
+ @JvmField
+ val COMPACT: WindowWidthSizeClass = WindowWidthSizeClass(0)
/**
* A bucket to represent a medium width window, typical for a phone in landscape or a
* tablet.
*/
- @JvmField val MEDIUM: WindowWidthSizeClass = WindowWidthSizeClass(1)
+ @Deprecated("WindowWidthSizeClass not be developed further.")
+ @JvmField
+ val MEDIUM: WindowWidthSizeClass = WindowWidthSizeClass(1)
/**
* A bucket to represent an expanded width window, typical for a large tablet or desktop
* form-factor.
*/
- @JvmField val EXPANDED: WindowWidthSizeClass = WindowWidthSizeClass(2)
+ @Deprecated("WindowWidthSizeClass not be developed further.")
+ @JvmField
+ val EXPANDED: WindowWidthSizeClass = WindowWidthSizeClass(2)
/**
* Returns a recommended [WindowWidthSizeClass] for the width of a window given the width in
@@ -74,6 +83,7 @@
* @return A recommended size class for the width
* @throws IllegalArgumentException if the width is negative
*/
+ @Deprecated("WindowWidthSizeClass not be developed further.")
internal fun compute(dpWidth: Float): WindowWidthSizeClass {
require(dpWidth >= 0) { "Width must be positive, received $dpWidth" }
return when {
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowHeightSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowHeightSizeClassTest.kt
index 80868cf..7183670 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowHeightSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowHeightSizeClassTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
+
package androidx.window.core.layout
import androidx.window.core.layout.WindowHeightSizeClass.Companion.COMPACT
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt
new file mode 100644
index 0000000..f72e378
--- /dev/null
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.core.layout
+
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class WindowSizeClassSelectorsTest {
+
+ val coreSet = WindowSizeClass.BREAKPOINTS_V1
+
+ @Test
+ fun compute_window_size_class_returns_zero_for_default() {
+ // coreSet does not contain 10, 10
+ val actual = coreSet.computeWindowSizeClass(10, 10)
+
+ assertEquals(WindowSizeClass(0, 0), actual)
+ }
+
+ @Test
+ fun compute_window_size_class_returns_exact_match() {
+ val expected = WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+
+ // coreSet contains WindowSizeClass(MEDIUM, MEDIUM)
+ val actual =
+ coreSet.computeWindowSizeClass(
+ WIDTH_DP_MEDIUM_LOWER_BOUND,
+ HEIGHT_DP_MEDIUM_LOWER_BOUND
+ )
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_returns_bounded_match() {
+ val expected = WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+
+ // coreSet contains WindowSizeClass(MEDIUM, MEDIUM)
+ val actual =
+ coreSet.computeWindowSizeClass(
+ WIDTH_DP_MEDIUM_LOWER_BOUND + 1,
+ HEIGHT_DP_MEDIUM_LOWER_BOUND + 1
+ )
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_prefers_width() {
+ val expected = WindowSizeClass(minWidthDp = 100, minHeightDp = 50)
+
+ val actual =
+ setOf(
+ WindowSizeClass(minWidthDp = 100, minHeightDp = 50),
+ WindowSizeClass(minWidthDp = 50, minHeightDp = 100)
+ )
+ .computeWindowSizeClass(100, 100)
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_breaks_tie_with_height() {
+ val expected = WindowSizeClass(minWidthDp = 100, minHeightDp = 100)
+
+ val actual =
+ setOf(
+ WindowSizeClass(minWidthDp = 100, minHeightDp = 50),
+ WindowSizeClass(minWidthDp = 100, minHeightDp = 100)
+ )
+ .computeWindowSizeClass(200, 200)
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_preferring_height_returns_zero_for_default() {
+ // coreSet does not contain 10, 10
+ val actual = coreSet.computeWindowSizeClassPreferHeight(10, 10)
+
+ assertEquals(WindowSizeClass(0, 0), actual)
+ }
+
+ @Test
+ fun compute_window_size_class_preferring_height_returns_exact_match() {
+ val expected = WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+
+ // coreSet contains WindowSizeClass(MEDIUM, MEDIUM)
+ val actual =
+ coreSet.computeWindowSizeClassPreferHeight(
+ WIDTH_DP_MEDIUM_LOWER_BOUND,
+ HEIGHT_DP_MEDIUM_LOWER_BOUND
+ )
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_preferring_height_returns_bounded_match() {
+ val expected = WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+
+ // coreSet contains WindowSizeClass(MEDIUM, MEDIUM)
+ val actual =
+ coreSet.computeWindowSizeClassPreferHeight(
+ WIDTH_DP_MEDIUM_LOWER_BOUND + 1,
+ HEIGHT_DP_MEDIUM_LOWER_BOUND + 1
+ )
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_preferring_height_prefers_height() {
+ val expected = WindowSizeClass(minWidthDp = 50, minHeightDp = 100)
+
+ val actual =
+ setOf(
+ WindowSizeClass(minWidthDp = 100, minHeightDp = 50),
+ WindowSizeClass(minWidthDp = 50, minHeightDp = 100)
+ )
+ .computeWindowSizeClassPreferHeight(100, 100)
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun compute_window_size_class_preferring_height_breaks_tie_with_width() {
+ val expected = WindowSizeClass(minWidthDp = 100, minHeightDp = 100)
+
+ val actual =
+ setOf(
+ WindowSizeClass(minWidthDp = 50, minHeightDp = 100),
+ WindowSizeClass(minWidthDp = 100, minHeightDp = 100)
+ )
+ .computeWindowSizeClass(200, 200)
+
+ assertEquals(expected, actual)
+ }
+}
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
index 46607ab..816e40c 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
@@ -16,16 +16,18 @@
package androidx.window.core.layout
-import androidx.window.core.ExperimentalWindowCoreApi
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
/** Tests for [WindowSizeClass] that verify construction. */
class WindowSizeClassTest {
+ @Suppress("DEPRECATION")
@Test
- fun testWidthSizeClass_construction() {
+ fun testWindowWidthSizeClass_compatibility() {
val expected =
listOf(
WindowWidthSizeClass.COMPACT,
@@ -41,6 +43,7 @@
assertEquals(expected, actual)
}
+ @Suppress("DEPRECATION")
@Test
fun testWindowSizeClass_computeRounds() {
val expected = WindowSizeClass.compute(0f, 0f)
@@ -50,18 +53,9 @@
assertEquals(expected, actual)
}
- @OptIn(ExperimentalWindowCoreApi::class)
+ @Suppress("DEPRECATION")
@Test
- fun testConstruction_usingPx() {
- val expected = WindowSizeClass.compute(600f, 600f)
-
- val actual = WindowSizeClass.compute(600, 600, 1f)
-
- assertEquals(expected, actual)
- }
-
- @Test
- fun testHeightSizeClass_construction() {
+ fun testWindowHeightSizeClass_compatibility() {
val expected =
listOf(
WindowHeightSizeClass.COMPACT,
@@ -79,16 +73,17 @@
@Test
fun testEqualsImpliesHashCode() {
- val first = WindowSizeClass.compute(100f, 500f)
- val second = WindowSizeClass.compute(100f, 500f)
+ val first = WindowSizeClass(100, 500)
+ val second = WindowSizeClass(100, 500)
assertEquals(first, second)
assertEquals(first.hashCode(), second.hashCode())
}
+ @Suppress("DEPRECATION")
@Test
fun truncated_float_does_not_throw() {
- val sizeClass = WindowSizeClass.compute(0.5f, 0.5f)
+ val sizeClass = WindowSizeClass(0.5f, 0.5f)
val widthSizeClass = sizeClass.windowWidthSizeClass
val heightSizeClass = sizeClass.windowHeightSizeClass
@@ -97,9 +92,10 @@
assertEquals(WindowHeightSizeClass.COMPACT, heightSizeClass)
}
+ @Suppress("DEPRECATION")
@Test
fun zero_size_class_does_not_throw() {
- val sizeClass = WindowSizeClass.compute(0f, 0f)
+ val sizeClass = WindowSizeClass(0, 0)
val widthSizeClass = sizeClass.windowWidthSizeClass
val heightSizeClass = sizeClass.windowHeightSizeClass
@@ -110,11 +106,94 @@
@Test
fun negative_width_throws() {
- assertFailsWith(IllegalArgumentException::class) { WindowSizeClass.compute(-1f, 0f) }
+ assertFailsWith(IllegalArgumentException::class) { WindowSizeClass(-1, 0) }
}
@Test
fun negative_height_throws() {
- assertFailsWith(IllegalArgumentException::class) { WindowSizeClass.compute(0f, -1f) }
+ assertFailsWith(IllegalArgumentException::class) { WindowSizeClass(0, -1) }
+ }
+
+ @Test
+ fun is_width_at_least_returns_true_when_input_is_greater() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertTrue(sizeClass.isWidthAtLeast(width + 1))
+ }
+
+ @Test
+ fun is_width_at_least_returns_true_when_input_is_equal() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertTrue(sizeClass.isWidthAtLeast(width))
+ }
+
+ @Test
+ fun is_width_at_least_returns_false_when_input_is_smaller() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertFalse(sizeClass.isWidthAtLeast(width - 1))
+ }
+
+ @Test
+ fun is_height_at_least_returns_true_when_input_is_greater() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertTrue(sizeClass.isHeightAtLeast(height + 1))
+ }
+
+ @Test
+ fun is_height_at_least_returns_true_when_input_is_equal() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertTrue(sizeClass.isHeightAtLeast(height))
+ }
+
+ @Test
+ fun is_height_at_least_returns_false_when_input_is_smaller() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertFalse(sizeClass.isHeightAtLeast(height - 1))
+ }
+
+ @Test
+ fun is_at_least_returns_true_when_input_is_greater() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertTrue(sizeClass.isAtLeast(width, height + 1))
+ assertTrue(sizeClass.isAtLeast(width + 1, height))
+ }
+
+ @Test
+ fun is_at_least_returns_true_when_input_is_equal() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertTrue(sizeClass.isAtLeast(width, height))
+ }
+
+ @Test
+ fun is_at_least_returns_false_when_input_is_smaller() {
+ val width = 200
+ val height = 100
+ val sizeClass = WindowSizeClass(width, height)
+
+ assertFalse(sizeClass.isAtLeast(width, height - 1))
+ assertFalse(sizeClass.isAtLeast(width - 1, height))
}
}
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowWidthSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowWidthSizeClassTest.kt
index 85ab99b..520b9bf 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowWidthSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowWidthSizeClassTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
+
package androidx.window.core.layout
import androidx.window.core.layout.WindowWidthSizeClass.Companion.COMPACT
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
index 92e85eb..3ff2766 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
@@ -154,7 +154,7 @@
activityDisplayBounds = Rect(0, 0, 960, 2142),
),
)
- DemoTheme { WindowStateScreen(viewModel = WindowStateViewModel(windowStates)) }
+ DemoTheme { WindowStateScreen(viewModel = viewModel { WindowStateViewModel(windowStates) }) }
}
/**
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 5707f86..004b04a 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -54,14 +54,14 @@
implementation("androidx.core:core:1.8.0")
def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
- def extensions_version = "androidx.window.extensions:extensions:1.4.0-alpha01"
- // A compile only dependency on extnensions.core so that other libraries do not expose it
+ def extensions_version = "androidx.window.extensions:extensions:1.4.0-beta01"
+ // A compile only dependency on extensions.core so that other libraries do not expose it
// transitively.
compileOnly(extensions_core_version)
// Test implementation is required since extensions:core is on device. So it is required to
// import it in some form. For the library it will be available on device.
testImplementation(extensions_core_version)
- // A compile only dependency on extnensions.core so that other libraries do not expose it
+ // A compile only dependency on extensions.core so that other libraries do not expose it
// transitively. The androidTestCompile is added because tests are not getting the dependency
// transitively.
androidTestCompileOnly(extensions_core_version)