diff --git a/biometric/biometric/src/main/res/values-bs/strings.xml b/biometric/biometric/src/main/res/values-bs/strings.xml index ecce54149002f..6c980671bb819 100644 --- a/biometric/biometric/src/main/res/values-bs/strings.xml +++ b/biometric/biometric/src/main/res/values-bs/strings.xml @@ -35,7 +35,7 @@ "Koristi otisak prsta" "Koristi otključavanje licem" "Koristi biometriju" - "Koristi zaključavanje ekrana" + "Koristi otključavanje ekrana" "Koristi otisak prsta ili zaključavanje ekrana" "Koristi otključavanje licem ili zaključavanje ekrana" "Koristi biometriju ili zaključavanje ekrana" diff --git a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt index b44d0f7b27eaf..03d618e9344f7 100644 --- a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt +++ b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt @@ -376,9 +376,9 @@ abstract class StudioTask : DefaultTask() { ) { val url = if (filename.contains("-mac")) { - "https://redirector.gvt1.com/edgedl/android/studio/install/$studioVersion/$filename" + "https://edgedl.me.gvt1.com/android/studio/install/$studioVersion/$filename" } else { - "https://redirector.gvt1.com/edgedl/android/studio/ide-zips/$studioVersion/$filename" + "https://edgedl.me.gvt1.com/android/studio/ide-zips/$studioVersion/$filename" } val tmpDownloadPath = File("$destinationPath.tmp").absolutePath println("Downloading $url to $tmpDownloadPath") diff --git a/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutHeroBenchmark.kt b/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutHeroBenchmark.kt index 47c88f49aa23b..ee70c18152e12 100644 --- a/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutHeroBenchmark.kt +++ b/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutHeroBenchmark.kt @@ -93,7 +93,7 @@ class SysUiSceneTransitionLayoutHeroBenchmark(private val compilationMode: Compi } companion object { - const val ITERATIONS = 25 + const val ITERATIONS = 10 @Parameterized.Parameters(name = "compilation={0}") @JvmStatic diff --git a/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutStartupHeroBenchmark.kt b/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutStartupHeroBenchmark.kt index 0070326bc60ba..9f455488bc6a6 100644 --- a/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutStartupHeroBenchmark.kt +++ b/compose/integration-tests/hero/sysui/sysui-macrobenchmark/src/main/kotlin/androidx/compose/integration/hero/sysui/macrobenchmark/SysUiSceneTransitionLayoutStartupHeroBenchmark.kt @@ -47,7 +47,7 @@ class SysUiSceneTransitionLayoutStartupHeroBenchmark( // elements for the quick settings but also nested SceneTransitionLayouts for each // notification. sysuiHeroBenchmarkScope() - .startDemoActivity(StlDemoConstants.SHADE_SCENE, notificationsInShade = 300) + .startDemoActivity(StlDemoConstants.SHADE_SCENE, notificationsInShade = 10) } } diff --git a/compose/integration-tests/macrobenchmark-target/OWNERS b/compose/integration-tests/macrobenchmark-target/OWNERS index ef0fe20041317..30c81f15cc38f 100644 --- a/compose/integration-tests/macrobenchmark-target/OWNERS +++ b/compose/integration-tests/macrobenchmark-target/OWNERS @@ -1,4 +1,5 @@ # Bug component: 378604 -andreykulikov@google.com bentrengrove@google.com -ccraik@google.com \ No newline at end of file +ccraik@google.com +ashikov@google.com +jossiwolf@google.com diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerOfLazyGridActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerOfLazyGridActivity.kt index 0cf8ae29c6098..7b578d82efc7c 100644 --- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerOfLazyGridActivity.kt +++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/PagerOfLazyGridActivity.kt @@ -21,7 +21,10 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.pager.HorizontalPager @@ -61,7 +64,12 @@ private fun HorizontalPagerOfLazyGrid(pages: Int = 100, gridItems: Int = 100) { val pagerState: PagerState = rememberPagerState(initialPage = 1) { pages } val coroutineScope = rememberCoroutineScope() - Column(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { + Column( + modifier = + Modifier.fillMaxSize() + .background(MaterialTheme.colors.background) + .windowInsetsPadding(WindowInsets.safeContent) + ) { Button( onClick = { coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } diff --git a/compose/material3/material3/api/current.ignore b/compose/material3/material3/api/current.ignore index d5e349a94e1d2..978db3c0d64d1 100644 --- a/compose/material3/material3/api/current.ignore +++ b/compose/material3/material3/api/current.ignore @@ -1075,3 +1075,7 @@ RemovedMethod: androidx.compose.material3.pulltorefresh.PullToRefreshDefaults#In Binary breaking change: Removed method androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.IndicatorBox(androidx.compose.material3.pulltorefresh.PullToRefreshState,boolean,androidx.compose.ui.Modifier,float,androidx.compose.ui.graphics.Shape,long,float,kotlin.jvm.functions.Function1) RemovedMethod: androidx.compose.material3.pulltorefresh.PullToRefreshKt#pullToRefresh(androidx.compose.ui.Modifier, boolean, androidx.compose.material3.pulltorefresh.PullToRefreshState, boolean, float, kotlin.jvm.functions.Function0): Binary breaking change: Removed method androidx.compose.material3.pulltorefresh.PullToRefreshKt.pullToRefresh(androidx.compose.ui.Modifier,boolean,androidx.compose.material3.pulltorefresh.PullToRefreshState,boolean,float,kotlin.jvm.functions.Function0) + + +RemovedProperty: androidx.compose.material3.AppBarMenuState#isExpanded: + Source breaking change: Removed property androidx.compose.material3.AppBarMenuState#isExpanded diff --git a/compose/material3/material3/api/restricted_current.ignore b/compose/material3/material3/api/restricted_current.ignore index d5e349a94e1d2..978db3c0d64d1 100644 --- a/compose/material3/material3/api/restricted_current.ignore +++ b/compose/material3/material3/api/restricted_current.ignore @@ -1075,3 +1075,7 @@ RemovedMethod: androidx.compose.material3.pulltorefresh.PullToRefreshDefaults#In Binary breaking change: Removed method androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.IndicatorBox(androidx.compose.material3.pulltorefresh.PullToRefreshState,boolean,androidx.compose.ui.Modifier,float,androidx.compose.ui.graphics.Shape,long,float,kotlin.jvm.functions.Function1) RemovedMethod: androidx.compose.material3.pulltorefresh.PullToRefreshKt#pullToRefresh(androidx.compose.ui.Modifier, boolean, androidx.compose.material3.pulltorefresh.PullToRefreshState, boolean, float, kotlin.jvm.functions.Function0): Binary breaking change: Removed method androidx.compose.material3.pulltorefresh.PullToRefreshKt.pullToRefresh(androidx.compose.ui.Modifier,boolean,androidx.compose.material3.pulltorefresh.PullToRefreshState,boolean,float,kotlin.jvm.functions.Function0) + + +RemovedProperty: androidx.compose.material3.AppBarMenuState#isExpanded: + Source breaking change: Removed property androidx.compose.material3.AppBarMenuState#isExpanded diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore new file mode 100644 index 0000000000000..2d0b69d6d95bd --- /dev/null +++ b/compose/runtime/runtime/api/current.ignore @@ -0,0 +1,5 @@ +// Baseline format: 1.0 +ChangedAnnotationRetention: androidx.compose.runtime.internal.FunctionKeyMeta: + Class androidx.compose.runtime.internal.FunctionKeyMeta incompatibly changed its retention from RUNTIME to BINARY +ChangedAnnotationRetention: androidx.compose.runtime.internal.FunctionKeyMeta.Container: + Class androidx.compose.runtime.internal.FunctionKeyMeta.Container incompatibly changed its retention from RUNTIME to BINARY diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt index bca0409eb8960..e23f7bddd6eee 100644 --- a/compose/runtime/runtime/api/current.txt +++ b/compose/runtime/runtime/api/current.txt @@ -910,7 +910,7 @@ package androidx.compose.runtime.internal { method @androidx.compose.runtime.ComposeCompilerApi public static Void illegalDecoyCallException(String fName); } - @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface FunctionKeyMeta { + @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface FunctionKeyMeta { ctor @KotlinOnly public FunctionKeyMeta(int key, int startOffset, int endOffset); method @InaccessibleFromKotlin public abstract int endOffset(); method @InaccessibleFromKotlin public abstract int key(); @@ -920,7 +920,7 @@ package androidx.compose.runtime.internal { property public abstract int startOffset; } - @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface FunctionKeyMeta.Container { + @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface FunctionKeyMeta.Container { method public abstract androidx.compose.runtime.internal.FunctionKeyMeta[] value(); } diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore new file mode 100644 index 0000000000000..2d0b69d6d95bd --- /dev/null +++ b/compose/runtime/runtime/api/restricted_current.ignore @@ -0,0 +1,5 @@ +// Baseline format: 1.0 +ChangedAnnotationRetention: androidx.compose.runtime.internal.FunctionKeyMeta: + Class androidx.compose.runtime.internal.FunctionKeyMeta incompatibly changed its retention from RUNTIME to BINARY +ChangedAnnotationRetention: androidx.compose.runtime.internal.FunctionKeyMeta.Container: + Class androidx.compose.runtime.internal.FunctionKeyMeta.Container incompatibly changed its retention from RUNTIME to BINARY diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt index ac8b011dbe5e8..c5675a504247e 100644 --- a/compose/runtime/runtime/api/restricted_current.txt +++ b/compose/runtime/runtime/api/restricted_current.txt @@ -965,7 +965,7 @@ package androidx.compose.runtime.internal { method @androidx.compose.runtime.ComposeCompilerApi public static Void illegalDecoyCallException(String fName); } - @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface FunctionKeyMeta { + @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface FunctionKeyMeta { ctor @KotlinOnly public FunctionKeyMeta(int key, int startOffset, int endOffset); method @InaccessibleFromKotlin public abstract int endOffset(); method @InaccessibleFromKotlin public abstract int key(); @@ -975,7 +975,7 @@ package androidx.compose.runtime.internal { property public abstract int startOffset; } - @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface FunctionKeyMeta.Container { + @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface FunctionKeyMeta.Container { method public abstract androidx.compose.runtime.internal.FunctionKeyMeta[] value(); } diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt index 221cd36fecb96..b61cc22409c90 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.ComposeCompilerApi */ @ComposeCompilerApi @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) +@Retention(AnnotationRetention.BINARY) @Repeatable public annotation class FunctionKeyMeta(val key: Int, val startOffset: Int, val endOffset: Int) diff --git a/constraintlayout/constraintlayout-compose/api/current.ignore b/constraintlayout/constraintlayout-compose/api/current.ignore index bdeea458bd925..45e3dcfecc984 100644 --- a/constraintlayout/constraintlayout-compose/api/current.ignore +++ b/constraintlayout/constraintlayout-compose/api/current.ignore @@ -389,3 +389,7 @@ RemovedMethod: androidx.constraintlayout.compose.State#setRootIncomingConstraint Binary breaking change: Removed method androidx.constraintlayout.compose.State.setRootIncomingConstraints(long) RemovedMethod: androidx.constraintlayout.compose.VerticalAnchorable#linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor, float, float): Binary breaking change: Removed method androidx.constraintlayout.compose.VerticalAnchorable.linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor,float,float) + + +RemovedProperty: androidx.constraintlayout.compose.ConstrainedLayoutReference#id: + Source breaking change: Removed property androidx.constraintlayout.compose.ConstrainedLayoutReference#id diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.ignore b/constraintlayout/constraintlayout-compose/api/restricted_current.ignore index 06741ae384cdc..d1ee419dae853 100644 --- a/constraintlayout/constraintlayout-compose/api/restricted_current.ignore +++ b/constraintlayout/constraintlayout-compose/api/restricted_current.ignore @@ -435,3 +435,9 @@ RemovedMethod: androidx.constraintlayout.compose.State#setRootIncomingConstraint Binary breaking change: Removed method androidx.constraintlayout.compose.State.setRootIncomingConstraints(long) RemovedMethod: androidx.constraintlayout.compose.VerticalAnchorable#linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor, float, float): Binary breaking change: Removed method androidx.constraintlayout.compose.VerticalAnchorable.linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor,float,float) + + +RemovedProperty: androidx.constraintlayout.compose.ConstrainedLayoutReference#id: + Source breaking change: Removed property androidx.constraintlayout.compose.ConstrainedLayoutReference#id +RemovedProperty: androidx.constraintlayout.compose.ToolingUtilsKt#designInfoProvider: + Source breaking change: Removed property androidx.constraintlayout.compose.ToolingUtilsKt#designInfoProvider diff --git a/graphics/graphics-core/api/current.ignore b/graphics/graphics-core/api/current.ignore index 7014c649214d1..159958725e878 100644 --- a/graphics/graphics-core/api/current.ignore +++ b/graphics/graphics-core/api/current.ignore @@ -53,6 +53,168 @@ AddedMethod: androidx.graphics.surface.SurfaceControlCompat.Transaction#setBuffe Added method androidx.graphics.surface.SurfaceControlCompat.Transaction.setBuffer$default(androidx.graphics.surface.SurfaceControlCompat.Transaction,androidx.graphics.surface.SurfaceControlCompat,android.hardware.HardwareBuffer,androidx.hardware.SyncFenceCompat,kotlin.jvm.functions.Function1,int,Object) +AddedProperty: androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#ERROR_UNKNOWN: + Added property androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#ERROR_UNKNOWN +AddedProperty: androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#SUCCESS: + Added property androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#SUCCESS +AddedProperty: androidx.graphics.opengl.SyncStrategy.Companion#ALWAYS: + Added property androidx.graphics.opengl.SyncStrategy.Companion#ALWAYS +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_EXT: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_EXT +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FIXED_EXT: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FIXED_EXT +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_1010102: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_1010102 +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_8888: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_8888 +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_F16: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_F16 +AddedProperty: androidx.graphics.opengl.egl.EGLSpec.Companion#V14: + Added property androidx.graphics.opengl.egl.EGLSpec.Companion#V14 +AddedProperty: androidx.graphics.opengl.egl.EGLVersion.Companion#Unknown: + Added property androidx.graphics.opengl.egl.EGLVersion.Companion#Unknown +AddedProperty: androidx.graphics.opengl.egl.EGLVersion.Companion#V14: + Added property androidx.graphics.opengl.egl.EGLVersion.Companion#V14 +AddedProperty: androidx.graphics.opengl.egl.EGLVersion.Companion#V15: + Added property androidx.graphics.opengl.egl.EGLVersion.Companion#V15 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_IDENTITY: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_IDENTITY +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_HORIZONTAL: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_HORIZONTAL +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_VERTICAL: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_VERTICAL +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_180: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_180 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_270: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_270 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_90: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_90 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ALWAYS: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ALWAYS +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_DEFAULT: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_DEFAULT +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_FIXED_SOURCE: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_FIXED_SOURCE +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_ADOBE_RGB: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_ADOBE_RGB +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT2020: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT2020 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_HLG: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_HLG +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_PQ: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_PQ +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT601_525: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT601_525 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT601_625: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT601_625 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT709: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT709 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DCI_P3: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DCI_P3 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DEPTH: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DEPTH +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DISPLAY_P3: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DISPLAY_P3 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DYNAMIC_DEPTH: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DYNAMIC_DEPTH +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_HEIF: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_HEIF +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_JFIF: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_JFIF +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_JPEG_R: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_JPEG_R +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB_LINEAR: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB_LINEAR +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SRGB: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SRGB +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SRGB_LINEAR: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SRGB_LINEAR +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_UNKNOWN: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_UNKNOWN +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_EXTENDED: + Added property androidx.hardware.DataSpace.Companion#RANGE_EXTENDED +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_FULL: + Added property androidx.hardware.DataSpace.Companion#RANGE_FULL +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_LIMITED: + Added property androidx.hardware.DataSpace.Companion#RANGE_LIMITED +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_UNSPECIFIED: + Added property androidx.hardware.DataSpace.Companion#RANGE_UNSPECIFIED +AddedProperty: androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_INVALID: + Added property androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_INVALID +AddedProperty: androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_PENDING: + Added property androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_PENDING +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_ANDROID_CLIENT_BUFFER: + Added property androidx.opengl.EGLExt.Companion#EGL_ANDROID_CLIENT_BUFFER +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_ANDROID_IMAGE_NATIVE_BUFFER: + Added property androidx.opengl.EGLExt.Companion#EGL_ANDROID_IMAGE_NATIVE_BUFFER +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_ANDROID_NATIVE_FENCE_SYNC: + Added property androidx.opengl.EGLExt.Companion#EGL_ANDROID_NATIVE_FENCE_SYNC +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_CONDITION_SATISFIED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_CONDITION_SATISFIED_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_BUFFER_AGE: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_BUFFER_AGE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_BT2020_PQ: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_BT2020_PQ +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_SCRGB: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_SCRGB +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_PIXEL_FORMAT_FLOAT: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_PIXEL_FORMAT_FLOAT +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_FALSE: + Added property androidx.opengl.EGLExt.Companion#EGL_FALSE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_FOREVER_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_FOREVER_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_IMG_CONTEXT_PRIORITY: + Added property androidx.opengl.EGLExt.Companion#EGL_IMG_CONTEXT_PRIORITY +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_FENCE_SYNC: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_FENCE_SYNC +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_GL_COLORSPACE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_GL_COLORSPACE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE_BASE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE_BASE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_NO_CONFIG_CONTEXT: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_NO_CONFIG_CONTEXT +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_PARTIAL_UPDATE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_PARTIAL_UPDATE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_SURFACELESS_CONTEXT: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_SURFACELESS_CONTEXT +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_SWAP_BUFFERS_WITH_DAMAGE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_SWAP_BUFFERS_WITH_DAMAGE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_WAIT_SYNC: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_WAIT_SYNC +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SIGNALED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SIGNALED_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_CONDITION_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_CONDITION_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_FENCE_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_FENCE_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_FLUSH_COMMANDS_BIT_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_FLUSH_COMMANDS_BIT_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_NATIVE_FENCE_ANDROID: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_NATIVE_FENCE_ANDROID +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_PRIOR_COMMANDS_COMPLETE_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_PRIOR_COMMANDS_COMPLETE_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_STATUS_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_STATUS_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_TYPE_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_TYPE_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_TIMEOUT_EXPIRED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_TIMEOUT_EXPIRED_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_TRUE: + Added property androidx.opengl.EGLExt.Companion#EGL_TRUE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_UNSIGNALED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_UNSIGNALED_KHR + + RemovedFromJava: androidx.graphics.opengl.egl.EGLConfigAttributes.Builder#to(int, int): Source breaking change: method androidx.graphics.opengl.egl.EGLConfigAttributes.Builder.to(int,int) can no longer be resolved from Java source RemovedFromJava: androidx.graphics.opengl.egl.EGLConfigAttributesKt#EGLConfigAttributes(kotlin.jvm.functions.Function1): diff --git a/graphics/graphics-core/api/restricted_current.ignore b/graphics/graphics-core/api/restricted_current.ignore index 3b02b659eb9ee..f77a62bdfc5cc 100644 --- a/graphics/graphics-core/api/restricted_current.ignore +++ b/graphics/graphics-core/api/restricted_current.ignore @@ -53,6 +53,170 @@ AddedMethod: androidx.graphics.surface.SurfaceControlCompat.Transaction#setBuffe Added method androidx.graphics.surface.SurfaceControlCompat.Transaction.setBuffer$default(androidx.graphics.surface.SurfaceControlCompat.Transaction,androidx.graphics.surface.SurfaceControlCompat,android.hardware.HardwareBuffer,androidx.hardware.SyncFenceCompat,kotlin.jvm.functions.Function1,int,Object) +AddedProperty: androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#ERROR_UNKNOWN: + Added property androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#ERROR_UNKNOWN +AddedProperty: androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#SUCCESS: + Added property androidx.graphics.CanvasBufferedRenderer.RenderResult.Companion#SUCCESS +AddedProperty: androidx.graphics.opengl.SyncStrategy.Companion#ALWAYS: + Added property androidx.graphics.opengl.SyncStrategy.Companion#ALWAYS +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes#attrs: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes#attrs +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_EXT: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_EXT +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FIXED_EXT: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FIXED_EXT +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_1010102: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_1010102 +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_8888: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_8888 +AddedProperty: androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_F16: + Added property androidx.graphics.opengl.egl.EGLConfigAttributes.Companion#RGBA_F16 +AddedProperty: androidx.graphics.opengl.egl.EGLSpec.Companion#V14: + Added property androidx.graphics.opengl.egl.EGLSpec.Companion#V14 +AddedProperty: androidx.graphics.opengl.egl.EGLVersion.Companion#Unknown: + Added property androidx.graphics.opengl.egl.EGLVersion.Companion#Unknown +AddedProperty: androidx.graphics.opengl.egl.EGLVersion.Companion#V14: + Added property androidx.graphics.opengl.egl.EGLVersion.Companion#V14 +AddedProperty: androidx.graphics.opengl.egl.EGLVersion.Companion#V15: + Added property androidx.graphics.opengl.egl.EGLVersion.Companion#V15 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_IDENTITY: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_IDENTITY +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_HORIZONTAL: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_HORIZONTAL +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_VERTICAL: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_MIRROR_VERTICAL +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_180: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_180 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_270: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_270 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_90: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#BUFFER_TRANSFORM_ROTATE_90 +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ALWAYS: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ALWAYS +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_DEFAULT: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_DEFAULT +AddedProperty: androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_FIXED_SOURCE: + Added property androidx.graphics.surface.SurfaceControlCompat.Companion#FRAME_RATE_COMPATIBILITY_FIXED_SOURCE +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_ADOBE_RGB: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_ADOBE_RGB +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT2020: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT2020 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_HLG: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_HLG +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_PQ: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT2020_PQ +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT601_525: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT601_525 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT601_625: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT601_625 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_BT709: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_BT709 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DCI_P3: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DCI_P3 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DEPTH: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DEPTH +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DISPLAY_P3: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DISPLAY_P3 +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_DYNAMIC_DEPTH: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_DYNAMIC_DEPTH +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_HEIF: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_HEIF +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_JFIF: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_JFIF +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_JPEG_R: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_JPEG_R +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB_LINEAR: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SCRGB_LINEAR +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SRGB: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SRGB +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_SRGB_LINEAR: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_SRGB_LINEAR +AddedProperty: androidx.hardware.DataSpace.Companion#DATASPACE_UNKNOWN: + Added property androidx.hardware.DataSpace.Companion#DATASPACE_UNKNOWN +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_EXTENDED: + Added property androidx.hardware.DataSpace.Companion#RANGE_EXTENDED +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_FULL: + Added property androidx.hardware.DataSpace.Companion#RANGE_FULL +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_LIMITED: + Added property androidx.hardware.DataSpace.Companion#RANGE_LIMITED +AddedProperty: androidx.hardware.DataSpace.Companion#RANGE_UNSPECIFIED: + Added property androidx.hardware.DataSpace.Companion#RANGE_UNSPECIFIED +AddedProperty: androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_INVALID: + Added property androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_INVALID +AddedProperty: androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_PENDING: + Added property androidx.hardware.SyncFenceCompat.Companion#SIGNAL_TIME_PENDING +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_ANDROID_CLIENT_BUFFER: + Added property androidx.opengl.EGLExt.Companion#EGL_ANDROID_CLIENT_BUFFER +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_ANDROID_IMAGE_NATIVE_BUFFER: + Added property androidx.opengl.EGLExt.Companion#EGL_ANDROID_IMAGE_NATIVE_BUFFER +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_ANDROID_NATIVE_FENCE_SYNC: + Added property androidx.opengl.EGLExt.Companion#EGL_ANDROID_NATIVE_FENCE_SYNC +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_CONDITION_SATISFIED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_CONDITION_SATISFIED_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_BUFFER_AGE: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_BUFFER_AGE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_BT2020_PQ: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_BT2020_PQ +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_SCRGB: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_GL_COLORSPACE_SCRGB +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_EXT_PIXEL_FORMAT_FLOAT: + Added property androidx.opengl.EGLExt.Companion#EGL_EXT_PIXEL_FORMAT_FLOAT +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_FALSE: + Added property androidx.opengl.EGLExt.Companion#EGL_FALSE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_FOREVER_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_FOREVER_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_IMG_CONTEXT_PRIORITY: + Added property androidx.opengl.EGLExt.Companion#EGL_IMG_CONTEXT_PRIORITY +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_FENCE_SYNC: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_FENCE_SYNC +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_GL_COLORSPACE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_GL_COLORSPACE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE_BASE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_IMAGE_BASE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_NO_CONFIG_CONTEXT: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_NO_CONFIG_CONTEXT +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_PARTIAL_UPDATE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_PARTIAL_UPDATE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_SURFACELESS_CONTEXT: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_SURFACELESS_CONTEXT +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_SWAP_BUFFERS_WITH_DAMAGE: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_SWAP_BUFFERS_WITH_DAMAGE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_KHR_WAIT_SYNC: + Added property androidx.opengl.EGLExt.Companion#EGL_KHR_WAIT_SYNC +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SIGNALED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SIGNALED_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_CONDITION_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_CONDITION_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_FENCE_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_FENCE_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_FLUSH_COMMANDS_BIT_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_FLUSH_COMMANDS_BIT_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_NATIVE_FENCE_ANDROID: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_NATIVE_FENCE_ANDROID +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_PRIOR_COMMANDS_COMPLETE_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_PRIOR_COMMANDS_COMPLETE_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_STATUS_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_STATUS_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_SYNC_TYPE_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_SYNC_TYPE_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_TIMEOUT_EXPIRED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_TIMEOUT_EXPIRED_KHR +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_TRUE: + Added property androidx.opengl.EGLExt.Companion#EGL_TRUE +AddedProperty: androidx.opengl.EGLExt.Companion#EGL_UNSIGNALED_KHR: + Added property androidx.opengl.EGLExt.Companion#EGL_UNSIGNALED_KHR + + RemovedFromJava: androidx.graphics.opengl.egl.EGLConfigAttributes.Builder#to(int, int): Source breaking change: method androidx.graphics.opengl.egl.EGLConfigAttributes.Builder.to(int,int) can no longer be resolved from Java source RemovedFromJava: androidx.graphics.opengl.egl.EGLConfigAttributesKt#EGLConfigAttributes(kotlin.jvm.functions.Function1): diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt index 49c0e6eda73c4..c43af2c355c2b 100644 --- a/health/connect/connect-client/api/current.txt +++ b/health/connect/connect-client/api/current.txt @@ -289,7 +289,7 @@ package androidx.health.connect.client.records { field public static final int ACTIVITY_INTENSITY_TYPE_VIGOROUS = 1; // 0x1 field public static final androidx.health.connect.client.records.ActivityIntensityRecord.Companion Companion; field public static final androidx.health.connect.client.aggregate.AggregateMetric DURATION_TOTAL; - field public static final androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; + field public static final androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; field public static final androidx.health.connect.client.aggregate.AggregateMetric MODERATE_DURATION_TOTAL; field public static final androidx.health.connect.client.aggregate.AggregateMetric VIGOROUS_DURATION_TOTAL; } @@ -298,7 +298,7 @@ package androidx.health.connect.client.records { property public static int ACTIVITY_INTENSITY_TYPE_MODERATE; property public static int ACTIVITY_INTENSITY_TYPE_VIGOROUS; property public androidx.health.connect.client.aggregate.AggregateMetric DURATION_TOTAL; - property public androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; + property public androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; property public androidx.health.connect.client.aggregate.AggregateMetric MODERATE_DURATION_TOTAL; property public androidx.health.connect.client.aggregate.AggregateMetric VIGOROUS_DURATION_TOTAL; } diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt index 51480ab736d82..6a526f78a51b3 100644 --- a/health/connect/connect-client/api/restricted_current.txt +++ b/health/connect/connect-client/api/restricted_current.txt @@ -289,7 +289,7 @@ package androidx.health.connect.client.records { field public static final int ACTIVITY_INTENSITY_TYPE_VIGOROUS = 1; // 0x1 field public static final androidx.health.connect.client.records.ActivityIntensityRecord.Companion Companion; field public static final androidx.health.connect.client.aggregate.AggregateMetric DURATION_TOTAL; - field public static final androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; + field public static final androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; field public static final androidx.health.connect.client.aggregate.AggregateMetric MODERATE_DURATION_TOTAL; field public static final androidx.health.connect.client.aggregate.AggregateMetric VIGOROUS_DURATION_TOTAL; } @@ -298,7 +298,7 @@ package androidx.health.connect.client.records { property public static int ACTIVITY_INTENSITY_TYPE_MODERATE; property public static int ACTIVITY_INTENSITY_TYPE_VIGOROUS; property public androidx.health.connect.client.aggregate.AggregateMetric DURATION_TOTAL; - property public androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; + property public androidx.health.connect.client.aggregate.AggregateMetric INTENSITY_MINUTES_TOTAL; property public androidx.health.connect.client.aggregate.AggregateMetric MODERATE_DURATION_TOTAL; property public androidx.health.connect.client.aggregate.AggregateMetric VIGOROUS_DURATION_TOTAL; } diff --git a/health/connect/connect-client/lint-baseline.xml b/health/connect/connect-client/lint-baseline.xml index 381356704607d..e8e3305e88241 100644 --- a/health/connect/connect-client/lint-baseline.xml +++ b/health/connect/connect-client/lint-baseline.xml @@ -1,23 +1,5 @@ - - - - - - - - - + = 16) - - private val grantPermissionRule: GrantPermissionRule = - GrantPermissionRule.grant( - *listOf( - HealthPermission.getWritePermission(ActivityIntensityRecord::class), - HealthPermission.getReadPermission(ActivityIntensityRecord::class), - ) - .toTypedArray() - ) - - @get:Rule val ruleChain: RuleChain = RuleChain.outerRule(assumeRule).around(grantPermissionRule) - - @After - fun tearDown() = runTest { - healthConnectClient.deleteRecords( - ActivityIntensityRecord::class, - TimeRangeFilter.after(Instant.EPOCH), - ) - } - - // The test aims to verify the wiring with the framework rather than the aggregation logic - // itself. - @Test - fun aggregateRecords() = runTest { - val insertRecordsResponse = - healthConnectClient.insertRecords( - listOf( - ActivityIntensityRecord( - startTime = START_TIME, - startZoneOffset = ZoneOffset.UTC, - endTime = START_TIME.plusSeconds(150), - endZoneOffset = ZoneOffset.UTC, - metadata = Metadata.manualEntry(), - activityIntensityType = ACTIVITY_INTENSITY_TYPE_VIGOROUS, - ), - ActivityIntensityRecord( - startTime = START_TIME.plusSeconds(180), - startZoneOffset = ZoneOffset.UTC, - endTime = START_TIME.plusSeconds(300), - endZoneOffset = ZoneOffset.UTC, - metadata = Metadata.manualEntry(), - activityIntensityType = ACTIVITY_INTENSITY_TYPE_MODERATE, - ), - ) - ) - assertThat(insertRecordsResponse.recordIdsList.size).isEqualTo(2) - - val aggregateResponse = - healthConnectClient.aggregate( - AggregateRequest( - setOf( - ActivityIntensityRecord.DURATION_TOTAL, - ActivityIntensityRecord.MODERATE_DURATION_TOTAL, - ActivityIntensityRecord.VIGOROUS_DURATION_TOTAL, - ActivityIntensityRecord.INTENSITY_MINUTES_TOTAL, - ), - TimeRangeFilter.after(Instant.EPOCH), - ) - ) - - with(aggregateResponse) { - assertThat(this[ActivityIntensityRecord.DURATION_TOTAL]) - .isEqualTo(Duration.ofSeconds(270)) - assertThat(this[ActivityIntensityRecord.MODERATE_DURATION_TOTAL]) - .isEqualTo(Duration.ofSeconds(120)) - assertThat(this[ActivityIntensityRecord.VIGOROUS_DURATION_TOTAL]) - .isEqualTo(Duration.ofSeconds(150)) - assertThat(this[ActivityIntensityRecord.INTENSITY_MINUTES_TOTAL]).isEqualTo(7) - } - } -} diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/MindfulnessSessionRecordsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/MindfulnessSessionRecordsTest.kt deleted file mode 100644 index f56873dd24f54..0000000000000 --- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/MindfulnessSessionRecordsTest.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.health.connect.client.impl.platform.records - -import android.annotation.TargetApi -import android.content.Context -import android.os.Build -import android.os.ext.SdkExtensions -import androidx.health.connect.client.AssumeRule -import androidx.health.connect.client.HealthConnectClient -import androidx.health.connect.client.impl.HealthConnectClientUpsideDownImpl -import androidx.health.connect.client.permission.HealthPermission -import androidx.health.connect.client.records.MindfulnessSessionRecord -import androidx.health.connect.client.records.MindfulnessSessionRecord.Companion.MINDFULNESS_SESSION_TYPE_BREATHING -import androidx.health.connect.client.records.MindfulnessSessionRecord.Companion.MINDFULNESS_SESSION_TYPE_MEDITATION -import androidx.health.connect.client.records.metadata.Metadata -import androidx.health.connect.client.request.AggregateRequest -import androidx.health.connect.client.time.TimeRangeFilter -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.MediumTest -import androidx.test.filters.SdkSuppress -import androidx.test.rule.GrantPermissionRule -import com.google.common.truth.Truth.assertThat -import java.time.Duration -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Rule -import org.junit.Test -import org.junit.rules.RuleChain -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -@MediumTest -@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -class MindfulnessSessionRecordTest { - - private val context: Context = ApplicationProvider.getApplicationContext() - private val healthConnectClient: HealthConnectClient = - HealthConnectClientUpsideDownImpl(context) - - private companion object { - private val START_TIME = - LocalDate.now().minusDays(5).atStartOfDay().toInstant(ZoneOffset.UTC) - } - - private val assumeRule: AssumeRule = - AssumeRule(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 15) - - private val grantPermissionRule: GrantPermissionRule = - GrantPermissionRule.grant( - *listOf( - HealthPermission.getWritePermission(MindfulnessSessionRecord::class), - HealthPermission.getReadPermission(MindfulnessSessionRecord::class), - ) - .toTypedArray() - ) - - @get:Rule val ruleChain: RuleChain = RuleChain.outerRule(assumeRule).around(grantPermissionRule) - - @After - fun tearDown() = runTest { - healthConnectClient.deleteRecords( - MindfulnessSessionRecord::class, - TimeRangeFilter.after(Instant.EPOCH), - ) - } - - // The test aims to verify the wiring with the framework rather than the aggregation logic - // itself. - @Test - fun aggregateRecords() = runTest { - val insertRecordsResponse = - healthConnectClient.insertRecords( - listOf( - MindfulnessSessionRecord( - startTime = START_TIME, - startZoneOffset = ZoneOffset.UTC, - endTime = START_TIME.plusSeconds(150), - endZoneOffset = ZoneOffset.UTC, - metadata = Metadata.manualEntry(), - mindfulnessSessionType = MINDFULNESS_SESSION_TYPE_MEDITATION, - ), - MindfulnessSessionRecord( - startTime = START_TIME.plusSeconds(180), - startZoneOffset = ZoneOffset.UTC, - endTime = START_TIME.plusSeconds(300), - endZoneOffset = ZoneOffset.UTC, - metadata = Metadata.manualEntry(), - mindfulnessSessionType = MINDFULNESS_SESSION_TYPE_BREATHING, - ), - ) - ) - assertThat(insertRecordsResponse.recordIdsList.size).isEqualTo(2) - - val aggregateResponse = - healthConnectClient.aggregate( - AggregateRequest( - setOf(MindfulnessSessionRecord.MINDFULNESS_DURATION_TOTAL), - TimeRangeFilter.after(Instant.EPOCH), - ) - ) - - with(aggregateResponse) { - assertThat(this[MindfulnessSessionRecord.MINDFULNESS_DURATION_TOTAL]) - .isEqualTo(Duration.ofSeconds(270)) - } - } -} diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt index b7360653ee2e9..73eb1b793c745 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappings.kt @@ -29,7 +29,6 @@ import android.health.connect.datatypes.FloorsClimbedRecord as PlatformFloorsCli import android.health.connect.datatypes.HeartRateRecord as PlatformHeartRateRecord import android.health.connect.datatypes.HeightRecord as PlatformHeightRecord import android.health.connect.datatypes.HydrationRecord as PlatformHydrationRecord -import android.health.connect.datatypes.MindfulnessSessionRecord as PlatformMindfulnessSessionRecord import android.health.connect.datatypes.NutritionRecord as PlatformNutritionRecord import android.health.connect.datatypes.PowerRecord as PlatformPowerRecord import android.health.connect.datatypes.StepsRecord as PlatformStepsRecord @@ -41,10 +40,11 @@ import android.health.connect.datatypes.units.Length as PlatformLength import android.health.connect.datatypes.units.Mass as PlatformMass import android.health.connect.datatypes.units.Power as PlatformPower import android.health.connect.datatypes.units.Volume as PlatformVolume +import android.os.Build +import android.os.ext.SdkExtensions import androidx.annotation.RequiresApi import androidx.annotation.RestrictTo import androidx.health.connect.client.aggregate.AggregateMetric -import androidx.health.connect.client.impl.platform.records.PlatformActivityIntensityRecord import androidx.health.connect.client.impl.platform.records.PlatformBloodPressureRecord import androidx.health.connect.client.impl.platform.records.PlatformCyclingPedalingCadenceRecord import androidx.health.connect.client.impl.platform.records.PlatformExerciseSessionRecord @@ -56,7 +56,6 @@ import androidx.health.connect.client.impl.platform.records.PlatformSpeedRecord import androidx.health.connect.client.impl.platform.records.PlatformStepsCadenceRecord import androidx.health.connect.client.impl.platform.records.PlatformVelocity import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord -import androidx.health.connect.client.records.ActivityIntensityRecord import androidx.health.connect.client.records.BasalMetabolicRateRecord import androidx.health.connect.client.records.BloodPressureRecord import androidx.health.connect.client.records.CyclingPedalingCadenceRecord @@ -67,7 +66,6 @@ import androidx.health.connect.client.records.FloorsClimbedRecord import androidx.health.connect.client.records.HeartRateRecord import androidx.health.connect.client.records.HeightRecord import androidx.health.connect.client.records.HydrationRecord -import androidx.health.connect.client.records.MindfulnessSessionRecord import androidx.health.connect.client.records.NutritionRecord import androidx.health.connect.client.records.PowerRecord import androidx.health.connect.client.records.RestingHeartRateRecord @@ -79,10 +77,6 @@ import androidx.health.connect.client.records.StepsRecord import androidx.health.connect.client.records.TotalCaloriesBurnedRecord import androidx.health.connect.client.records.WeightRecord import androidx.health.connect.client.records.WheelchairPushesRecord -import androidx.health.connect.client.records.isAtLeastSdkExtension10 -import androidx.health.connect.client.records.isAtLeastSdkExtension13 -import androidx.health.connect.client.records.isAtLeastSdkExtension15 -import androidx.health.connect.client.records.isAtLeastSdkExtension16 import androidx.health.connect.client.units.Energy import androidx.health.connect.client.units.Length import androidx.health.connect.client.units.Mass @@ -94,7 +88,7 @@ import androidx.health.connect.client.units.Volume import java.time.Duration private val DOUBLE_AGGREGATION_METRIC_TYPE_SDK_EXT_10_PAIRS = - if (isAtLeastSdkExtension10()) { + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) { arrayOf( CyclingPedalingCadenceRecord.RPM_AVG to PlatformCyclingPedalingCadenceRecord.RPM_AVG, CyclingPedalingCadenceRecord.RPM_MAX to PlatformCyclingPedalingCadenceRecord.RPM_MAX, @@ -115,38 +109,12 @@ internal val DOUBLE_AGGREGATION_METRIC_TYPE_MAP: *DOUBLE_AGGREGATION_METRIC_TYPE_SDK_EXT_10_PAIRS, ) -internal val DURATION_TO_LONG_AGGREGATION_METRIC_TYPE_MAP: +internal val DURATION_AGGREGATION_METRIC_TYPE_MAP: Map, PlatformAggregateMetric> = mapOf( ExerciseSessionRecord.EXERCISE_DURATION_TOTAL to PlatformExerciseSessionRecord.EXERCISE_DURATION_TOTAL, SleepSessionRecord.SLEEP_DURATION_TOTAL to PlatformSleepSessionRecord.SLEEP_DURATION_TOTAL, - *if (isAtLeastSdkExtension15()) { - arrayOf( - MindfulnessSessionRecord.MINDFULNESS_DURATION_TOTAL to - PlatformMindfulnessSessionRecord.MINDFULNESS_DURATION_TOTAL - ) - } else { - emptyArray() - }, - ) - -internal val DURATION_AGGREGATION_METRIC_TYPE_MAP: - Map, PlatformAggregateMetric> = - mapOf( - *if (isAtLeastSdkExtension16()) { - arrayOf( - ActivityIntensityRecord.MODERATE_DURATION_TOTAL to - PlatformActivityIntensityRecord.MODERATE_DURATION_TOTAL, - ActivityIntensityRecord.VIGOROUS_DURATION_TOTAL to - PlatformActivityIntensityRecord.VIGOROUS_DURATION_TOTAL, - ActivityIntensityRecord.DURATION_TOTAL to - PlatformActivityIntensityRecord.DURATION_TOTAL, - ) - as Array, PlatformAggregateMetric>> - } else { - emptyArray() - } ) internal val ENERGY_AGGREGATION_METRIC_TYPE_MAP: @@ -185,14 +153,6 @@ internal val LONG_AGGREGATION_METRIC_TYPE_MAP: StepsRecord.COUNT_TOTAL to PlatformStepsRecord.STEPS_COUNT_TOTAL, WheelchairPushesRecord.COUNT_TOTAL to PlatformWheelchairPushesRecord.WHEEL_CHAIR_PUSHES_COUNT_TOTAL, - *if (isAtLeastSdkExtension16()) { - arrayOf( - ActivityIntensityRecord.INTENSITY_MINUTES_TOTAL to - PlatformActivityIntensityRecord.INTENSITY_MINUTES_TOTAL - ) - } else { - emptyArray() - }, ) internal val GRAMS_AGGREGATION_METRIC_TYPE_MAP: @@ -240,7 +200,7 @@ internal val GRAMS_AGGREGATION_METRIC_TYPE_MAP: NutritionRecord.VITAMIN_E_TOTAL to PlatformNutritionRecord.VITAMIN_E_TOTAL, NutritionRecord.VITAMIN_K_TOTAL to PlatformNutritionRecord.VITAMIN_K_TOTAL, NutritionRecord.ZINC_TOTAL to PlatformNutritionRecord.ZINC_TOTAL, - *if (isAtLeastSdkExtension10()) { + *if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) { arrayOf(NutritionRecord.TRANS_FAT_TOTAL to PlatformNutritionRecord.TRANS_FAT_TOTAL) } else { emptyArray() @@ -265,7 +225,7 @@ internal val POWER_AGGREGATION_METRIC_TYPE_MAP: internal val PRESSURE_AGGREGATION_METRIC_TYPE_MAP: Map, PlatformAggregateMetric> = - if (isAtLeastSdkExtension10()) { + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) { arrayOf( BloodPressureRecord.DIASTOLIC_AVG to PlatformBloodPressureRecord.DIASTOLIC_AVG, BloodPressureRecord.DIASTOLIC_MAX to PlatformBloodPressureRecord.DIASTOLIC_MAX, @@ -282,7 +242,7 @@ internal val PRESSURE_AGGREGATION_METRIC_TYPE_MAP: @SuppressLint("NewApi") // API 35 is covered by sdk extension check internal val TEMPERATURE_DELTA_METRIC_TYPE_MAP: Map, PlatformAggregateMetric<*>> = - if (isAtLeastSdkExtension13()) { + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 13) { arrayOf( SkinTemperatureRecord.TEMPERATURE_DELTA_AVG to PlatformSkinTemperatureRecord.SKIN_TEMPERATURE_DELTA_AVG, @@ -298,7 +258,7 @@ internal val TEMPERATURE_DELTA_METRIC_TYPE_MAP: internal val VELOCITY_AGGREGATION_METRIC_TYPE_MAP: Map, PlatformAggregateMetric> = - if (isAtLeastSdkExtension10()) { + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10) { arrayOf( SpeedRecord.SPEED_AVG to PlatformSpeedRecord.SPEED_AVG, SpeedRecord.SPEED_MAX to PlatformSpeedRecord.SPEED_MAX, diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt index 86db8cb75de8a..0d2edd5ffea21 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/request/RequestConverters.kt @@ -32,7 +32,6 @@ import androidx.annotation.RestrictTo import androidx.health.connect.client.aggregate.AggregateMetric import androidx.health.connect.client.impl.platform.aggregate.DOUBLE_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.DURATION_AGGREGATION_METRIC_TYPE_MAP -import androidx.health.connect.client.impl.platform.aggregate.DURATION_TO_LONG_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.ENERGY_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.GRAMS_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP @@ -130,7 +129,6 @@ fun AggregateGroupByPeriodRequest.toPlatformRequest(): AggregateRecordsRequest.toAggregationType(): AggregationType { return DOUBLE_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType? ?: DURATION_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType? - ?: DURATION_TO_LONG_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType? ?: ENERGY_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType? ?: GRAMS_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType? ?: LENGTH_AGGREGATION_METRIC_TYPE_MAP[this] as AggregationType? diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt index b5afbdd9a8418..48af0c5c435cc 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/ResponseConverters.kt @@ -37,7 +37,6 @@ import androidx.health.connect.client.aggregate.AggregationResultGroupedByDurati import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod import androidx.health.connect.client.impl.platform.aggregate.DOUBLE_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.DURATION_AGGREGATION_METRIC_TYPE_MAP -import androidx.health.connect.client.impl.platform.aggregate.DURATION_TO_LONG_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.ENERGY_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.GRAMS_AGGREGATION_METRIC_TYPE_MAP import androidx.health.connect.client.impl.platform.aggregate.KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP @@ -59,7 +58,6 @@ import androidx.health.connect.client.impl.platform.records.toSdkDataOrigin import androidx.health.connect.client.impl.platform.request.toAggregationType import androidx.health.connect.client.units.Energy import androidx.health.connect.client.units.Mass -import java.time.Duration import java.time.LocalDateTime import java.time.ZoneOffset @@ -141,16 +139,11 @@ internal fun getLongMetricValues( ): Map { return buildMap { metricValueMap.forEach { (key, value) -> - when (key) { - in LONG_AGGREGATION_METRIC_TYPE_MAP -> { - this[key.metricKey] = value as Long - } - in DURATION_TO_LONG_AGGREGATION_METRIC_TYPE_MAP -> { - this[key.metricKey] = value as Long - } - in DURATION_AGGREGATION_METRIC_TYPE_MAP -> { - this[key.metricKey] = (value as Duration).toMillis() - } + if ( + key in DURATION_AGGREGATION_METRIC_TYPE_MAP || + key in LONG_AGGREGATION_METRIC_TYPE_MAP + ) { + this[key.metricKey] = value as Long } } } diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ActivityIntensityRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ActivityIntensityRecord.kt index d71988374767d..95ce39591aaa7 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ActivityIntensityRecord.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ActivityIntensityRecord.kt @@ -138,10 +138,10 @@ class ActivityIntensityRecord( * [HealthConnectFeatures.FEATURE_ACTIVITY_INTENSITY] as the argument. */ @JvmField - val INTENSITY_MINUTES_TOTAL: AggregateMetric = - AggregateMetric.longMetric( + val INTENSITY_MINUTES_TOTAL: AggregateMetric = + AggregateMetric.durationMetric( "ActivityIntensity", - aggregationType = AggregateMetric.AggregationType.TOTAL, + aggregationType = AggregateMetric.AggregationType.DURATION, fieldName = "intensityMinutes", ) diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt index aaeb330d8afa7..972f00399efa4 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt @@ -23,24 +23,14 @@ import android.os.ext.SdkExtensions import androidx.annotation.RequiresApi import androidx.annotation.RestrictTo -// We additionally check for SDK_INT check for Robolectric tests, which doesn't fully support -// SdkExtensions. -@RequiresApi(Build.VERSION_CODES.R) -internal fun isAtLeastSdkExtension10(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM || - SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10 -} - @RequiresApi(Build.VERSION_CODES.R) internal fun isAtLeastSdkExtension13(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM || - SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 13 + return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 13 } @RequiresApi(Build.VERSION_CODES.R) internal fun isAtLeastSdkExtension15(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA || - SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 15 + return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 15 } @RequiresApi(Build.VERSION_CODES.R) diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappingsTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappingsTest.kt deleted file mode 100644 index 9388db7421550..0000000000000 --- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/AggregationMappingsTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.health.connect.client.impl.platform.aggregate - -import androidx.health.connect.client.aggregate.AggregateMetric -import androidx.health.connect.client.impl.converters.datatype.RECORDS_CLASS_NAME_MAP -import androidx.health.connect.client.records.isAtLeastSdkExtension16 -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertWithMessage -import java.lang.reflect.Field -import java.lang.reflect.Modifier -import org.junit.Assume -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.annotation.Config - -@RunWith(AndroidJUnit4::class) -@Config(minSdk = 36) -class AggregationMappingsTest { - - private val allAggregationMaps = - listOf( - DOUBLE_AGGREGATION_METRIC_TYPE_MAP, - DURATION_AGGREGATION_METRIC_TYPE_MAP, - DURATION_TO_LONG_AGGREGATION_METRIC_TYPE_MAP, - ENERGY_AGGREGATION_METRIC_TYPE_MAP, - GRAMS_AGGREGATION_METRIC_TYPE_MAP, - KILOGRAMS_AGGREGATION_METRIC_TYPE_MAP, - LENGTH_AGGREGATION_METRIC_TYPE_MAP, - LONG_AGGREGATION_METRIC_TYPE_MAP, - POWER_AGGREGATION_METRIC_TYPE_MAP, - PRESSURE_AGGREGATION_METRIC_TYPE_MAP, - TEMPERATURE_DELTA_METRIC_TYPE_MAP, - VELOCITY_AGGREGATION_METRIC_TYPE_MAP, - VOLUME_AGGREGATION_METRIC_TYPE_MAP, - ) - - @Before - fun setUp() { - // Update this when adding new aggregate metrics. - Assume.assumeTrue(isAtLeastSdkExtension16()) - } - - @Test - fun allAggregateMetrics_areAddedToAggregationMaps() { - val recordClasses = RECORDS_CLASS_NAME_MAP.keys - val aggregateMetrics = - recordClasses.flatMap { recordClass -> - recordClass.java.fields - .filter { it.isAggregateMetric() } - .map { it.get(null) as AggregateMetric<*> } - } - - val allMappedMetrics = allAggregationMaps.flatMap { it.keys } - val missingMetrics = aggregateMetrics.filter { !allMappedMetrics.contains(it) } - val presentMetrics = aggregateMetrics.filter { allMappedMetrics.contains(it) } - assertWithMessage( - "Missing metrics: ${missingMetrics.map { it.metricKey }}, Present Metrics: ${presentMetrics.map { it.metricKey }}" - ) - .that(allMappedMetrics) - .containsAtLeastElementsIn(aggregateMetrics) - } - - private fun Field.isAggregateMetric(): Boolean { - return Modifier.isStatic(modifiers) && - Modifier.isPublic(modifiers) && - AggregateMetric::class.java.isAssignableFrom(type) - } -} diff --git a/navigation/navigation-runtime/api/current.ignore b/navigation/navigation-runtime/api/current.ignore index 480ff049303fb..8f01dff5d1534 100644 --- a/navigation/navigation-runtime/api/current.ignore +++ b/navigation/navigation-runtime/api/current.ignore @@ -1,6 +1,6 @@ // Baseline format: 1.0 DefaultValueChange: androidx.navigation.NavController#popBackStack(boolean, boolean) parameter #1: - Source breaking change: Attempted to remove default value from parameter saveState in androidx.navigation.NavController.popBackStack + Source breaking change: Attempted to remove default value from parameter saveState in androidx.navigation.NavController.popBackStack(boolean inclusive, boolean saveState) RemovedFromBytecode: androidx.navigation.ActivityNavArgsLazyKt#navArgs(android.app.Activity): @@ -87,3 +87,13 @@ RemovedFromKotlin: androidx.navigation.NavController#setGraph(androidx.navigatio Source breaking change: method androidx.navigation.NavController.setGraph(androidx.navigation.NavGraph) can no longer be resolved from Kotlin source RemovedFromKotlin: androidx.navigation.NavHost#getNavController(): Source breaking change: method androidx.navigation.NavHost.getNavController() can no longer be resolved from Kotlin source + + +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_ARGS: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_ARGS +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_EXTRAS: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_EXTRAS +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_HANDLED: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_HANDLED +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_IDS: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_IDS diff --git a/navigation/navigation-runtime/api/restricted_current.ignore b/navigation/navigation-runtime/api/restricted_current.ignore index 480ff049303fb..8f01dff5d1534 100644 --- a/navigation/navigation-runtime/api/restricted_current.ignore +++ b/navigation/navigation-runtime/api/restricted_current.ignore @@ -1,6 +1,6 @@ // Baseline format: 1.0 DefaultValueChange: androidx.navigation.NavController#popBackStack(boolean, boolean) parameter #1: - Source breaking change: Attempted to remove default value from parameter saveState in androidx.navigation.NavController.popBackStack + Source breaking change: Attempted to remove default value from parameter saveState in androidx.navigation.NavController.popBackStack(boolean inclusive, boolean saveState) RemovedFromBytecode: androidx.navigation.ActivityNavArgsLazyKt#navArgs(android.app.Activity): @@ -87,3 +87,13 @@ RemovedFromKotlin: androidx.navigation.NavController#setGraph(androidx.navigatio Source breaking change: method androidx.navigation.NavController.setGraph(androidx.navigation.NavGraph) can no longer be resolved from Kotlin source RemovedFromKotlin: androidx.navigation.NavHost#getNavController(): Source breaking change: method androidx.navigation.NavHost.getNavController() can no longer be resolved from Kotlin source + + +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_ARGS: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_ARGS +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_EXTRAS: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_EXTRAS +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_HANDLED: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_HANDLED +RemovedProperty: androidx.navigation.NavController.Companion#KEY_DEEP_LINK_IDS: + Source breaking change: Removed property androidx.navigation.NavController.Companion#KEY_DEEP_LINK_IDS diff --git a/pdf/pdf-compose/src/androidTest/kotlin/androidx/pdf/compose/FakePdfDocument.kt b/pdf/pdf-compose/src/androidTest/kotlin/androidx/pdf/compose/FakePdfDocument.kt index 6c83c870e6b92..3ecad232d46a5 100644 --- a/pdf/pdf-compose/src/androidTest/kotlin/androidx/pdf/compose/FakePdfDocument.kt +++ b/pdf/pdf-compose/src/androidTest/kotlin/androidx/pdf/compose/FakePdfDocument.kt @@ -38,6 +38,7 @@ import androidx.pdf.content.PdfPageLinkContent import androidx.pdf.content.PdfPageTextContent import androidx.pdf.content.SelectionBoundary import androidx.pdf.models.FormWidgetInfo +import java.util.concurrent.Executor import kotlin.math.roundToInt import kotlin.random.Random @@ -184,7 +185,8 @@ internal open class FakePdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) { return } diff --git a/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/PdfFormFillingTest.kt b/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/PdfFormFillingTest.kt index e7845f910818a..68c2b21a601f2 100644 --- a/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/PdfFormFillingTest.kt +++ b/pdf/pdf-document-service/src/androidTest/kotlin/androidx/pdf/PdfFormFillingTest.kt @@ -28,9 +28,14 @@ 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.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -839,6 +844,39 @@ class PdfFormFillingTest { verifyApplyEditThrowsException(TEXT_FORM, setChoiceOnTF) } + @Test + fun pdfContentInvalidatedListener_calledOnCorrectExecutorThread() = runTest { + withEditableDocument(CLICK_FORM) { document -> + var listenerThread: Thread? = null + val listenerLatch = CountDownLatch(1) + + val callingThread: Thread = Thread.currentThread() + val customThreadName = "CustomThread" + + val customExecutor = + Executors.newSingleThreadExecutor { command -> Thread(command, customThreadName) } + + document.addOnPdfContentInvalidatedListener( + customExecutor, + object : PdfDocument.OnPdfContentInvalidatedListener { + override fun onPdfContentInvalidated(pageNumber: Int, dirtyAreas: List) { + listenerThread = Thread.currentThread() + listenerLatch.countDown() + } + }, + ) + val clickPoint = PdfPoint(pageNum = 0, x = 145f, y = 80f) + val editRec = FormEditInfo.createClick(1, clickPoint = clickPoint) + + document.applyEdit(editRec) + + assertTrue(listenerLatch.await(5, TimeUnit.SECONDS)) + assertNotEquals(callingThread, listenerThread) + assertEquals(listenerThread?.name, customThreadName) + customExecutor.shutdown() + } + } + companion object { private const val CLICK_FORM = "click_form.pdf" private const val TEXT_FORM = "text_form.pdf" @@ -892,6 +930,7 @@ class PdfFormFillingTest { ) { withEditableDocument(fileName) { document -> document.addOnPdfContentInvalidatedListener( + { command -> command.run() }, object : PdfDocument.OnPdfContentInvalidatedListener { override fun onPdfContentInvalidated( pageNumber: Int, @@ -899,7 +938,7 @@ class PdfFormFillingTest { ) { assertThat(fullyContains(expectedDirtyArea, dirtyAreas)).isTrue() } - } + }, ) val formWidgetInfos = document.getFormWidgetInfos(pageNum, intArrayOf(before.widgetType)) diff --git a/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt b/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt index 9a3fb73e5893a..889a449512a8e 100644 --- a/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt +++ b/pdf/pdf-document-service/src/main/kotlin/androidx/pdf/SandboxedPdfDocument.kt @@ -45,6 +45,7 @@ import androidx.pdf.service.connect.PdfServiceConnection import androidx.pdf.utils.toAndroidClass import androidx.pdf.utils.toContentClass import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executor import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext @@ -101,7 +102,7 @@ public class SandboxedPdfDocument( private val closeScope = CoroutineScope(coroutineContext + SupervisorJob()) private val onPdfContentInvalidatedListeners: - CopyOnWriteArrayList = + CopyOnWriteArrayList> = CopyOnWriteArrayList() /** @@ -234,23 +235,24 @@ public class SandboxedPdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) { - onPdfContentInvalidatedListeners.add(listener) + onPdfContentInvalidatedListeners.add(Pair(executor, listener)) } override fun removeOnPdfContentInvalidatedListener( listener: PdfDocument.OnPdfContentInvalidatedListener ) { - onPdfContentInvalidatedListeners.remove(listener) + onPdfContentInvalidatedListeners.removeIf { it.second == listener } } override suspend fun applyEdit(record: FormEditInfo) { val dirtyAreas = withDocument { document -> document.applyEdit(record.pageNumber, record.toAndroidClass()) } - onPdfContentInvalidatedListeners.forEach { - it.onPdfContentInvalidated(record.pageNumber, dirtyAreas) + onPdfContentInvalidatedListeners.forEach { (executor, listener) -> + executor.execute { listener.onPdfContentInvalidated(record.pageNumber, dirtyAreas) } } } diff --git a/pdf/pdf-document-service/src/test/kotlin/androidx/pdf/FakeEditablePdfDocument.kt b/pdf/pdf-document-service/src/test/kotlin/androidx/pdf/FakeEditablePdfDocument.kt index 823556399523c..cf4a2dad63e0c 100644 --- a/pdf/pdf-document-service/src/test/kotlin/androidx/pdf/FakeEditablePdfDocument.kt +++ b/pdf/pdf-document-service/src/test/kotlin/androidx/pdf/FakeEditablePdfDocument.kt @@ -30,6 +30,7 @@ import androidx.pdf.content.PageSelection import androidx.pdf.models.FormEditInfo import androidx.pdf.models.FormWidgetInfo import java.util.UUID +import java.util.concurrent.Executor /** Fake implementation of [EditablePdfDocument] for testing. */ internal class FakeEditablePdfDocument( @@ -50,7 +51,8 @@ internal class FakeEditablePdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) { TODO("Not yet implemented") } diff --git a/pdf/pdf-ink/build.gradle b/pdf/pdf-ink/build.gradle index 750da8b131832..5705ce05ddd03 100644 --- a/pdf/pdf-ink/build.gradle +++ b/pdf/pdf-ink/build.gradle @@ -59,8 +59,6 @@ dependencies { testImplementation(libs.testCoreKtx) testImplementation(libs.testExtJunitKtx) testImplementation(libs.kotlinCoroutinesTest) - testImplementation(libs.mockitoCore) - testImplementation(libs.mockitoKotlin4) androidTestImplementation(libs.junit) androidTestImplementation(libs.testRunner) @@ -71,9 +69,7 @@ dependencies { androidTestImplementation(libs.kotlinCoroutinesTest) androidTestImplementation(libs.espressoCore) androidTestImplementation(libs.bundles.espressoContrib) - androidTestImplementation(libs.espressoIdlingResource) androidTestImplementation(project(":test:screenshot:screenshot")) - androidTestImplementation("androidx.fragment:fragment-testing:1.7.1") } androidx { diff --git a/pdf/pdf-ink/src/androidTest/assets/sample_form.pdf b/pdf/pdf-ink/src/androidTest/assets/sample_form.pdf deleted file mode 100755 index ab9460180429c..0000000000000 Binary files a/pdf/pdf-ink/src/androidTest/assets/sample_form.pdf and /dev/null differ diff --git a/pdf/pdf-ink/src/androidTest/kotlin/ScubaConstants.kt b/pdf/pdf-ink/src/androidTest/kotlin/ScubaConstants.kt index 099a82474850d..02f69c38a200f 100644 --- a/pdf/pdf-ink/src/androidTest/kotlin/ScubaConstants.kt +++ b/pdf/pdf-ink/src/androidTest/kotlin/ScubaConstants.kt @@ -34,14 +34,6 @@ internal const val ANNOTATION_TOOLBAR_IN_DARK_MODE = "annotation_toolbar_in_dark internal const val ANNOTATION_TOOLBAR_IN_LIGHT_MODE = "annotation_toolbar_in_light_mode" internal const val ANNOTATION_TOOLBAR_COLLAPSED = "annotation_toolbar_collapsed" -internal const val ANNOTATION_TOOLBAR_DOCKED_START_WITH_BRUSH_SIZE = - "annotation_toolbar_docked_start_with_brush_size" -internal const val ANNOTATION_TOOLBAR_DOCKED_START_WITH_COLOR_PALETTE = - "annotation_toolbar_docked_start_with_color_palette" -internal const val ANNOTATION_TOOLBAR_DOCKED_END_WITH_BRUSH_SIZE = - "annotation_toolbar_docked_end_with_brush_size" -internal const val ANNOTATION_TOOLBAR_DOCKED_END_WITH_COLOR_PALETTE = - "annotation_toolbar_docked_end_with_color_palette" internal const val BRUSH_SIZE_SELECTED_ON_STEP_0 = "brush_size_selector_on_step_0" internal const val BRUSH_SIZE_SELECTED_ON_STEP_4 = "brush_size_selector_on_step_4" internal const val BRUSH_SIZE_IN_VERTICAL_ORIENTATION = "brush_size_in_vertical_orientation" diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/FakeEditablePdfDocument.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/FakeEditablePdfDocument.kt index 5eac2768c01c4..a94d1a795be50 100644 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/FakeEditablePdfDocument.kt +++ b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/FakeEditablePdfDocument.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -41,6 +41,7 @@ import androidx.pdf.models.FormEditInfo import androidx.pdf.models.FormWidgetInfo import androidx.pdf.models.ListItem import java.util.UUID +import java.util.concurrent.Executor import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -277,7 +278,8 @@ internal open class FakeEditablePdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) {} override fun removeOnPdfContentInvalidatedListener( diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/fragment/EditablePdfViewerFragmentTests.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/fragment/EditablePdfViewerFragmentTests.kt deleted file mode 100644 index f4b47bc22c9f8..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/fragment/EditablePdfViewerFragmentTests.kt +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.fragment - -import android.content.pm.ActivityInfo -import android.os.Build -import android.os.ext.SdkExtensions -import android.widget.LinearLayout -import androidx.annotation.RequiresExtension -import androidx.fragment.app.testing.FragmentScenario -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.lifecycle.Lifecycle -import androidx.pdf.R as PdfR -import androidx.pdf.ink.R -import androidx.pdf.ink.view.AnnotationToolbar -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START -import androidx.pdf.util.FragmentTestUtils.scenarioLoadDocument -import androidx.pdf.util.Preconditions -import androidx.pdf.util.ToolbarMatchers.matchesToolbarMask -import androidx.pdf.util.ToolbarMatchers.withDockState -import androidx.pdf.util.ToolbarViewActions -import androidx.pdf.util.ToolbarViewActions.performDragAndDrop -import androidx.test.espresso.Espresso.onIdle -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.IdlingRegistry -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.LargeTest -import com.google.common.truth.Truth.assertThat -import org.hamcrest.CoreMatchers.not -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@LargeTest -@RunWith(AndroidJUnit4::class) -@RequiresExtension(extension = Build.VERSION_CODES.S, version = 18) -class EditablePdfViewerFragmentTests { - private lateinit var scenario: FragmentScenario - - @Before - fun setup() { - scenario = - launchFragmentInContainer( - themeResId = - com.google.android.material.R.style.Theme_Material3_DayNight_NoActionBar, - initialState = Lifecycle.State.INITIALIZED, - ) - .onFragment { fragment -> - IdlingRegistry.getInstance() - .register(fragment.pdfLoadingIdlingResource.countingIdlingResource) - } - } - - @After - fun cleanup() { - if (!::scenario.isInitialized) return - - scenario.onFragment { fragment -> - IdlingRegistry.getInstance() - .unregister(fragment.pdfLoadingIdlingResource.countingIdlingResource) - } - scenario.close() - } - - private fun loadDocumentAndSetupFragment() { - scenarioLoadDocument( - scenario = scenario, - filename = TEST_DOCUMENT_FILE, - nextState = Lifecycle.State.RESUMED, - orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, - ) - onIdle() // Wait for document to load - - scenario.onFragment { fragment -> - Preconditions.checkArgument( - fragment.documentLoaded, - "Unable to load document due to ${fragment.documentError?.message}", - ) - fragment.setIsAnnotationIntentResolvable(true) - fragment.isToolboxVisible = true - } - } - - private fun enterEditMode() { - onView(withId(PdfR.id.edit_fab)).apply { - check(matches(isDisplayed())) - perform(click()) - check(matches(not(isDisplayed()))) - } - onIdle() - } - - @Test - fun test_annotationToolbar_dockedAtBottom_andCanDragToStart() { - if (!isRequiredSdkExtensionAvailable()) return - - loadDocumentAndSetupFragment() - enterEditMode() - - onView(withId(R.id.annotationToolbar)) - .check(matches(isDisplayed())) - .check(matches(withDockState(DOCK_STATE_BOTTOM))) - - // Initiate drag event - onView(withId(R.id.annotationToolbar)).perform(ViewActions.longClick()) - onIdle() - // Verify toolbar collapses on long press - onView(withId(R.id.collapsed_tool)).check(matches(isDisplayed())) - - // Drag to the left side of the screen - performDragAndDrop( - toolbarId = R.id.annotationToolbar, - to = ToolbarViewActions.DragTarget.LEFT, - ) - onIdle() - - // Verify toolbar docked to the left side - onView(withId(R.id.annotationToolbar)).check(matches(withDockState(DOCK_STATE_START))) - - // Verify tool tray orientation is vertical - scenario.onFragment { fragment -> - val toolTray = fragment.view?.findViewById(R.id.tool_tray) - assertThat(toolTray?.orientation).isEqualTo(LinearLayout.VERTICAL) - } - } - - @Test - fun test_annotationToolbar_persistsDockStateThroughRotation() { - if (!isRequiredSdkExtensionAvailable()) return - - loadDocumentAndSetupFragment() - enterEditMode() - - // Move toolbar to the END (Right) side - onView(withId(R.id.annotationToolbar)).perform(ViewActions.longClick()) - performDragAndDrop( - toolbarId = R.id.annotationToolbar, - to = ToolbarViewActions.DragTarget.RIGHT, - ) - onIdle() - - // Rotate the device to Landscape - scenario.onFragment { fragment -> - fragment.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } - onIdle() - - // Verify it remains on the END side after rotation - onView(withId(R.id.annotationToolbar)).check(matches(withDockState(DOCK_STATE_END))) - } - - @Test - fun test_toolbarMovement_updatesWetStrokesMaskPath() { - if (!isRequiredSdkExtensionAvailable()) return - - loadDocumentAndSetupFragment() - enterEditMode() - - var toolbar: AnnotationToolbar? = null - scenario.onFragment { fragment -> - toolbar = fragment.view?.findViewById(R.id.annotationToolbar) - } - - // Initial check: Toolbar is at bottom, mask should be at bottom - onView(withId(R.id.pdf_wet_strokes_view)).check(matches(matchesToolbarMask(toolbar!!))) - - // Move the toolbar to the START (Left) side - onView(withId(R.id.annotationToolbar)).perform(ViewActions.longClick()) - performDragAndDrop( - R.id.annotationToolbar, - to = ToolbarViewActions.DragTarget.LEFT, - ) // Using the helper from previous step - onIdle() - - // Verify the mask path updated to the new location (START side) - onView(withId(R.id.pdf_wet_strokes_view)).check(matches(matchesToolbarMask(toolbar!!))) - } - - @Test - fun test_annotationToolbar_reExpands_onLongPressWithoutMove() { - if (!isRequiredSdkExtensionAvailable()) return - - loadDocumentAndSetupFragment() - enterEditMode() - - // Perform Long Press but do NOT call performDragAndDrop - onView(withId(R.id.annotationToolbar)).perform(ViewActions.longClick()) - onIdle() - - // Simulate releasing the touch (Action Up) - onView(withId(R.id.annotationToolbar)).perform(click()) - // Since we didn't move, it should re-expand at same dock position - onView(withId(R.id.tool_tray)).check(matches(isDisplayed())) - onView(withId(R.id.collapsed_tool)).check(matches(not(isDisplayed()))) - onView(withId(R.id.annotationToolbar)).check(matches(withDockState(DOCK_STATE_BOTTOM))) - } - - companion object { - private const val TEST_DOCUMENT_FILE = "sample_form.pdf" - private const val REQUIRED_EXTENSION_VERSION = 18 - - fun isRequiredSdkExtensionAvailable(): Boolean { - // Get the device's version for the specified SDK extension - val deviceExtensionVersion = SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) - return deviceExtensionVersion >= REQUIRED_EXTENSION_VERSION - } - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/fragment/TestEditablePdfViewerFragment.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/fragment/TestEditablePdfViewerFragment.kt deleted file mode 100644 index c138c731c3064..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/fragment/TestEditablePdfViewerFragment.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.fragment - -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.FrameLayout.LayoutParams -import android.widget.FrameLayout.LayoutParams.MATCH_PARENT -import androidx.annotation.RequiresExtension -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.pdf.PdfDocument -import androidx.pdf.ink.EditablePdfViewerFragment -import androidx.pdf.util.PdfIdlingResource -import java.util.UUID - -/** - * A subclass fragment from [EditablePdfViewerFragment] to include - * [androidx.test.espresso.IdlingResource] while loading pdf document. - */ -@RequiresExtension(extension = Build.VERSION_CODES.S, version = 18) -internal class TestEditablePdfViewerFragment : EditablePdfViewerFragment { - - constructor() : super() - - val pdfLoadingIdlingResource = PdfIdlingResource(PDF_LOAD_RESOURCE_NAME) - - var pdfDocument: PdfDocument? = null - var documentLoaded = false - var documentError: Throwable? = null - private var hostView: FrameLayout? = null - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - val view = super.onCreateView(inflater, container, savedInstanceState) as ConstraintLayout - - // Inflate the custom layout for this fragment - hostView = - FrameLayout(requireContext()).apply { - layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - } - hostView?.let { hostView -> handleInsets(hostView) } - - // Add the default PDF viewer to the custom layout - hostView?.addView(view) - return hostView - } - - override fun onLoadDocumentSuccess(document: PdfDocument) { - super.onLoadDocumentSuccess(document) - pdfDocument = document - documentLoaded = true - pdfLoadingIdlingResource.decrement() - } - - override fun onLoadDocumentError(error: Throwable) { - super.onLoadDocumentError(error) - documentError = error - pdfLoadingIdlingResource.decrement() - } - - fun setIsAnnotationIntentResolvable(value: Boolean) { - setAnnotationIntentResolvability(value) - } - - companion object { - // Resource name must be unique to avoid conflicts while running multiple test scenarios - private val PDF_LOAD_RESOURCE_NAME = "PdfLoad-${UUID.randomUUID()}" - - fun handleInsets(hostView: View) { - ViewCompat.setOnApplyWindowInsetsListener(hostView) { view, insets -> - // Get the insets for the system bars (status bar, navigation bar) - val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - - // Adjust the padding of the container view to accommodate system windows - view.setPadding( - view.paddingLeft, - systemBarsInsets.top, - view.paddingRight, - systemBarsInsets.bottom, - ) - insets - } - } - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarTest.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarTest.kt index 34b0a1e461e5f..780b60e22658e 100644 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarTest.kt +++ b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarTest.kt @@ -18,14 +18,10 @@ package androidx.pdf.ink.view import android.content.Context import android.view.ViewGroup.LayoutParams -import android.widget.LinearLayout import androidx.pdf.PdfTestActivity import androidx.pdf.ink.R import androidx.pdf.ink.util.setSliderValue import androidx.pdf.ink.util.withSliderValue -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START import androidx.pdf.ink.view.tool.AnnotationToolView import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onIdle @@ -42,7 +38,6 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.google.android.material.slider.Slider -import com.google.common.truth.Truth.assertThat import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import kotlin.test.assertNotNull @@ -309,61 +304,6 @@ class AnnotationToolbarTest { assertFalse(annotationToolbar.isConfigPopupVisible) } - @Test - fun testSetDockState_updatesOrientationAndConstraints() { - var toolbar: AnnotationToolbar? = null - setupAnnotationToolbar { toolbar = it } - - assertNotNull(toolbar) - - // Change dock state to START (Vertical) - activityRule.scenario.onActivity { toolbar.dockState = DOCK_STATE_START } - onIdle() - - // Verify tool tray orientation updated to Vertical - val toolTray = toolbar.findViewById(R.id.tool_tray) - assertThat(toolTray.orientation).isEqualTo(LinearLayout.VERTICAL) - - // Change dock state back to BOTTOM (Horizontal) - activityRule.scenario.onActivity { toolbar.dockState = DOCK_STATE_BOTTOM } - onIdle() - - // Verify tool tray orientation reverted to Horizontal - assertThat(toolTray?.orientation).isEqualTo(LinearLayout.HORIZONTAL) - } - - @Test - fun testDockState_restoresOnConfigChange() { - // Prepare activity with a toolbar - PdfTestActivity.onCreateCallback = { activity -> - activity.container.addView( - createToolbar(activity), - LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT), - ) - } - - with(ActivityScenario.launch(PdfTestActivity::class.java)) { - // Set dock state to END - onActivity { activity -> - val toolbar = activity.findViewById(ANNOTATION_TOOLBAR_VIEW_ID) - toolbar.dockState = DOCK_STATE_END - } - onIdle() - - // Recreate activity - recreate() - - // Assert dock state is restored - onActivity { activity -> - val toolbar = activity.findViewById(ANNOTATION_TOOLBAR_VIEW_ID) - assertThat(toolbar.dockState).isEqualTo(DOCK_STATE_END) - - val toolTray = toolbar.findViewById(R.id.tool_tray) - assertThat(toolTray.orientation).isEqualTo(LinearLayout.VERTICAL) - } - } - } - private fun assertColorPaletteChecks() { onView(withId(R.id.color_palette_button)).check(matches(isEnabled())) // assert initially color palette is not visible diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarUiTest.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarUiTest.kt index 6a9f20ea0171c..b0eec8c5bc9f5 100644 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarUiTest.kt +++ b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/AnnotationToolbarUiTest.kt @@ -18,10 +18,6 @@ package androidx.pdf.ink.view import ANNOTATION_TOOLBAR import ANNOTATION_TOOLBAR_COLLAPSED -import ANNOTATION_TOOLBAR_DOCKED_END_WITH_BRUSH_SIZE -import ANNOTATION_TOOLBAR_DOCKED_END_WITH_COLOR_PALETTE -import ANNOTATION_TOOLBAR_DOCKED_START_WITH_BRUSH_SIZE -import ANNOTATION_TOOLBAR_DOCKED_START_WITH_COLOR_PALETTE import ANNOTATION_TOOLBAR_IN_DARK_MODE import ANNOTATION_TOOLBAR_IN_LIGHT_MODE import ANNOTATION_TOOLBAR_WITH_COLOR_PALETTE_VISIBLE @@ -38,8 +34,6 @@ import androidx.pdf.ink.R import androidx.pdf.ink.util.clickItemAt import androidx.pdf.ink.util.setSliderValue import androidx.pdf.ink.view.colorpalette.ColorPaletteAdapter -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START import androidx.pdf.ink.view.tool.AnnotationToolView import androidx.test.espresso.Espresso.onIdle import androidx.test.espresso.Espresso.onView @@ -165,58 +159,6 @@ class AnnotationToolbarUiTest { assertScreenshot(ANNOTATION_TOOLBAR_VIEW_ID, screenshotRule, ANNOTATION_TOOLBAR_COLLAPSED) } - @Test - fun test_annotationToolbar_dockedStart_withBrushSlider() { - setupAnnotationToolbar { it.dockState = DOCK_STATE_START } - // Open brush slider - onView(withId(R.id.pen_button)).perform(click()) - - assertScreenshot( - ANNOTATION_TOOLBAR_VIEW_ID, - screenshotRule, - ANNOTATION_TOOLBAR_DOCKED_START_WITH_BRUSH_SIZE, - ) - } - - @Test - fun test_annotationToolbar_dockedStart_withColorPalette() { - setupAnnotationToolbar { it.dockState = DOCK_STATE_START } - // Open color palette - onView(withId(R.id.color_palette_button)).perform(click()) - - assertScreenshot( - ANNOTATION_TOOLBAR_VIEW_ID, - screenshotRule, - ANNOTATION_TOOLBAR_DOCKED_START_WITH_COLOR_PALETTE, - ) - } - - @Test - fun test_annotationToolbar_dockedEnd_withBrushSlider() { - setupAnnotationToolbar { it.dockState = DOCK_STATE_END } - // Open brush slider - onView(withId(R.id.pen_button)).perform(click()) - - assertScreenshot( - ANNOTATION_TOOLBAR_VIEW_ID, - screenshotRule, - ANNOTATION_TOOLBAR_DOCKED_END_WITH_BRUSH_SIZE, - ) - } - - @Test - fun test_annotationToolbar_dockedEnd_withColorPalette() { - setupAnnotationToolbar { it.dockState = DOCK_STATE_END } - // Open color palette - onView(withId(R.id.color_palette_button)).perform(click()) - - assertScreenshot( - ANNOTATION_TOOLBAR_VIEW_ID, - screenshotRule, - ANNOTATION_TOOLBAR_DOCKED_END_WITH_COLOR_PALETTE, - ) - } - private fun setupAnnotationToolbar( isDarkMode: Boolean = false, callback: (AnnotationToolbar) -> Unit = {}, diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarCoordinatorTest.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarCoordinatorTest.kt deleted file mode 100644 index 88ef751068469..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarCoordinatorTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.view.Gravity -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.pdf.PdfTestActivity -import androidx.pdf.ink.view.AnnotationToolbar -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START -import androidx.pdf.util.ToolbarViewActions -import androidx.pdf.util.ToolbarViewActions.performDragAndDrop -import androidx.test.espresso.Espresso.onIdle -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.longClick -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.google.common.truth.Truth.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ToolbarCoordinatorTest { - - @get:Rule val activityRule = ActivityScenarioRule(PdfTestActivity::class.java) - - private val instrumentation = InstrumentationRegistry.getInstrumentation() - private lateinit var coordinator: ToolbarCoordinator - private lateinit var toolbar: AnnotationToolbar - - @Before - fun setUp() { - activityRule.scenario.onActivity { activity -> - coordinator = - ToolbarCoordinator(activity).apply { - id = COORDINATOR_VIEW_ID - areAnimationsEnabled = false - } - toolbar = - AnnotationToolbar(activity).apply { - id = TOOLBAR_VIEW_ID - areAnimationsEnabled = false - } - // Use FrameLayout.LayoutParams since coordinator is added to a FrameLayout container - activity.container.addView( - coordinator, - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ), - ) - coordinator.attachToolbar(toolbar) - } - instrumentation.waitForIdleSync() - } - - @After - fun tearDown() { - activityRule.scenario.onActivity { activity -> activity.container.removeAllViews() } - PdfTestActivity.onCreateCallback = {} - } - - @Test - fun attachToolbar_setsInitialStateToBottom() { - onIdle() - assertThat(toolbar.dockState).isEqualTo(DOCK_STATE_BOTTOM) - - val params = toolbar.layoutParams as FrameLayout.LayoutParams - assertThat(params.gravity).isEqualTo(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) - } - - @Test - fun dragAndDrop_toLeftSide_snapsToStart() { - onView(withId(TOOLBAR_VIEW_ID)).perform(longClick()) - onIdle() - - performDragAndDrop(toolbarId = TOOLBAR_VIEW_ID, to = ToolbarViewActions.DragTarget.LEFT) - onIdle() - - // Verify dock state updated to START - assertThat(toolbar.dockState).isEqualTo(DOCK_STATE_START) - - val params = toolbar.layoutParams as FrameLayout.LayoutParams - assertThat(params.gravity).isEqualTo(Gravity.CENTER_VERTICAL or Gravity.START) - } - - @Test - fun dragAndDrop_toRightSide_snapsToEnd() { - onView(withId(TOOLBAR_VIEW_ID)).perform(longClick()) - onIdle() - - performDragAndDrop(toolbarId = TOOLBAR_VIEW_ID, to = ToolbarViewActions.DragTarget.RIGHT) - onIdle() - - assertThat(toolbar.dockState).isEqualTo(DOCK_STATE_END) - val params = toolbar.layoutParams as FrameLayout.LayoutParams - assertThat(params.gravity).isEqualTo(Gravity.CENTER_VERTICAL or Gravity.END) - } - - @Test - fun dragAndDrop_toBottomSide_snapsToBottom() { - // Move to Start first so we can drag back to bottom - activityRule.scenario.onActivity { toolbar.dockState = DOCK_STATE_START } - onIdle() - - onView(withId(TOOLBAR_VIEW_ID)).perform(longClick()) - onIdle() - - performDragAndDrop(toolbarId = TOOLBAR_VIEW_ID, to = ToolbarViewActions.DragTarget.BOTTOM) - onIdle() - - assertThat(toolbar.dockState).isEqualTo(DOCK_STATE_BOTTOM) - val params = toolbar.layoutParams as FrameLayout.LayoutParams - assertThat(params.gravity).isEqualTo(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) - } - - companion object { - private const val COORDINATOR_VIEW_ID = 1001 - private const val TOOLBAR_VIEW_ID = 1002 - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/state/ToolbarInitializerTest.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/state/ToolbarInitializerTest.kt index b32e0f77681f1..ff60101e9d5ca 100644 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/state/ToolbarInitializerTest.kt +++ b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/ink/view/state/ToolbarInitializerTest.kt @@ -19,7 +19,6 @@ package androidx.pdf.ink.view.state import android.content.Context import androidx.pdf.ink.view.colorpalette.model.getHighlightPaletteItems import androidx.pdf.ink.view.colorpalette.model.getPenPaletteItems -import androidx.pdf.ink.view.draganddrop.ToolbarDockState import androidx.pdf.ink.view.tool.model.AnnotationToolsKey import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -67,7 +66,7 @@ class ToolbarInitializerTest { .isEqualTo(defaultHighlighterColorIndex) assertThat(initialState.highlighterState.paletteItem) .isEqualTo(highlightPaletteItems[defaultHighlighterColorIndex]) - assertThat(initialState.dockedState).isEqualTo(ToolbarDockState.DOCK_STATE_BOTTOM) + assertThat(initialState.isExpanded).isTrue() } } diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/FragmentTestUtils.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/FragmentTestUtils.kt deleted file mode 100644 index fe435279d3d9c..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/FragmentTestUtils.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.pdf.util - -import android.os.Build -import android.os.ext.SdkExtensions -import androidx.annotation.RequiresExtension -import androidx.fragment.app.Fragment -import androidx.fragment.app.testing.FragmentScenario -import androidx.lifecycle.Lifecycle -import androidx.pdf.ink.fragment.TestEditablePdfViewerFragment -import androidx.test.platform.app.InstrumentationRegistry - -internal object FragmentTestUtils { - - private const val TEST_DOCUMENT_FILE = "sample_form.pdf" - - @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13) - internal fun scenarioLoadDocument( - scenario: FragmentScenario, - filename: String = TEST_DOCUMENT_FILE, - nextState: Lifecycle.State, - orientation: Int, - ): FragmentScenario { - val context = InstrumentationRegistry.getInstrumentation().context - val inputStream = context.assets.open(filename) - - scenario.moveToState(nextState) - scenario.onFragment { it.requireActivity().requestedOrientation = orientation } - - // Load the document in the fragment - scenario.onFragment { fragment -> - if ( - fragment is TestEditablePdfViewerFragment && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 18 - ) { - fragment.pdfLoadingIdlingResource.increment() - fragment.documentUri = TestUtils.saveStream(inputStream, fragment.requireContext()) - } - } - - return scenario - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/PdfIdlingResource.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/PdfIdlingResource.kt deleted file mode 100644 index 5cc185da85cf6..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/PdfIdlingResource.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.pdf.util - -import androidx.test.espresso.idling.CountingIdlingResource -import java.util.concurrent.atomic.AtomicInteger - -/** - * A wrapper around Espresso's [CountingIdlingResource] that will help to define idling resource for - * any background work. - */ -internal class PdfIdlingResource(private val resourceName: String) { - - val countingIdlingResource: CountingIdlingResource = CountingIdlingResource(resourceName) - val decrementCounter: AtomicInteger = AtomicInteger() - - fun increment() { - countingIdlingResource.increment() - if (decrementCounter.get() > 0) { - decrementCounter.andDecrement - decrement() - } - } - - fun decrement() { - // Check if idling resource is not already idle - if (!countingIdlingResource.isIdleNow) { - countingIdlingResource.decrement() - } else { - decrementCounter.andIncrement - } - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/TestUtils.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/TestUtils.kt deleted file mode 100644 index f7ece2a8e7f63..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/TestUtils.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.pdf.util - -import android.content.Context -import android.net.Uri -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream - -object TestUtils { - private val TEMP_FILE_NAME = "temp" - private val TEMP_FILE_TYPE = ".pdf" - - fun saveStream(inputStream: InputStream, context: Context): Uri { - val tempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_TYPE, context.cacheDir) - FileOutputStream(tempFile).use { outputStream -> inputStream.copyTo(outputStream) } - return Uri.fromFile(tempFile) - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/ToolbarMatchers.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/ToolbarMatchers.kt deleted file mode 100644 index 1b905afefd150..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/ToolbarMatchers.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.pdf.util - -import android.graphics.RectF -import android.view.View -import androidx.ink.authoring.InProgressStrokesView -import androidx.pdf.ink.view.AnnotationToolbar -import androidx.test.espresso.matcher.BoundedMatcher -import org.hamcrest.Matcher - -internal object ToolbarMatchers { - /** - * Verifies that the [InProgressStrokesView] has a mask that matches the bounds of the toolbar. - */ - fun matchesToolbarMask(toolbar: AnnotationToolbar): Matcher { - return object : - BoundedMatcher(InProgressStrokesView::class.java) { - override fun describeTo(description: org.hamcrest.Description) { - description.appendText( - "with mask path matching toolbar at ${toolbar.x}, ${toolbar.y}" - ) - } - - override fun matchesSafely(view: InProgressStrokesView): Boolean { - val mask = view.maskPath ?: return false - val bounds = RectF() - mask.computeBounds(bounds, true) - - // The mask should roughly match the toolbar's location and size - return bounds.left == toolbar.x && - bounds.top == toolbar.y && - bounds.width() == toolbar.width.toFloat() && - bounds.height() == toolbar.height.toFloat() - } - } - } - - /** Custom Matcher to check the internal dockState of the AnnotationToolbar. */ - fun withDockState(expectedState: Int): Matcher { - return object : BoundedMatcher(AnnotationToolbar::class.java) { - override fun describeTo(description: org.hamcrest.Description) { - description.appendText("with dockState: $expectedState") - } - - override fun matchesSafely(toolbar: AnnotationToolbar): Boolean { - return toolbar.dockState == expectedState - } - } - } -} diff --git a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/ToolbarViewActions.kt b/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/ToolbarViewActions.kt deleted file mode 100644 index 593fd3f6e001f..0000000000000 --- a/pdf/pdf-ink/src/androidTest/kotlin/androidx/pdf/util/ToolbarViewActions.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.pdf.util - -import android.view.View -import androidx.pdf.ink.view.draganddrop.ToolbarCoordinator -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.GeneralLocation -import androidx.test.espresso.action.GeneralSwipeAction -import androidx.test.espresso.action.Press -import androidx.test.espresso.action.Swipe -import androidx.test.espresso.matcher.ViewMatchers.withId - -internal object ToolbarViewActions { - - // Space from boundary of the view - private const val DELTA_FROM_BOUNDARY = 10f - - /** - * Performs a drag and drop action from the center of the toolbar to a specified target edge of - * the [ToolbarCoordinator]. - * - * @param to The target edge to drag the toolbar towards. - */ - fun performDragAndDrop(toolbarId: Int, to: DragTarget) { - onView(withId(toolbarId)) - .perform( - GeneralSwipeAction( - Swipe.FAST, - GeneralLocation.CENTER, - { view -> - val coordinator = view.parent as View - val screenPos = IntArray(2) - coordinator.getLocationOnScreen(screenPos) - val x: Float - val y: Float - - when (to) { - DragTarget.LEFT -> { - x = screenPos[0].toFloat() + DELTA_FROM_BOUNDARY - y = screenPos[1].toFloat() + (coordinator.height / 2f) - } - DragTarget.RIGHT -> { - x = screenPos[0].toFloat() + coordinator.width - DELTA_FROM_BOUNDARY - y = screenPos[1].toFloat() + (coordinator.height / 2f) - } - DragTarget.BOTTOM -> { - x = screenPos[0].toFloat() + (coordinator.width / 2f) - y = - screenPos[1].toFloat() + coordinator.height - - DELTA_FROM_BOUNDARY - } - } - floatArrayOf(x, y) - }, - Press.FINGER, - ) - ) - } - - enum class DragTarget { - LEFT, - RIGHT, - BOTTOM, - } -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt index 8205e5971021b..5992c7f07bf0c 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt @@ -30,7 +30,6 @@ import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams import androidx.annotation.RequiresExtension import androidx.annotation.RestrictTo import androidx.constraintlayout.widget.ConstraintLayout @@ -60,7 +59,6 @@ import androidx.pdf.ink.util.PageTransformCalculator import androidx.pdf.ink.util.toHighlighterConfig import androidx.pdf.ink.util.toInkBrush import androidx.pdf.ink.view.AnnotationToolbar -import androidx.pdf.ink.view.draganddrop.ToolbarCoordinator import androidx.pdf.ink.view.tool.AnnotationToolInfo import androidx.pdf.view.PdfContentLayout import androidx.pdf.view.PdfView @@ -204,25 +202,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { private lateinit var wetStrokesViewTouchHandler: WetStrokesViewTouchHandler private lateinit var pdfContentLayoutTouchListener: PdfContentLayoutTouchListener private lateinit var annotationToolbar: AnnotationToolbar - - private lateinit var toolbarCoordinator: ToolbarCoordinator - - private val toolbarLayoutChangeListener = - View.OnLayoutChangeListener { - v, - left, - top, - right, - bottom, - oldLeft, - oldTop, - oldRight, - oldBottom -> - if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { - wetStrokesView.maskPath = createToolbarMaskPath() - } - } - private lateinit var pageInfoProvider: PageInfoProviderImpl private val annotationsViewDispatcher = AnnotationsViewTouchEventDispatcher() @@ -281,7 +260,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { wetStrokesView = InProgressStrokesView(requireContext()).apply { - id = R.id.pdf_wet_strokes_view layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -299,12 +277,9 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { ViewGroup.LayoutParams.MATCH_PARENT, ) } - annotationToolbar = - inflater.inflate(R.layout.annotation_toolbar_layout, null, false) as AnnotationToolbar - toolbarCoordinator = - ToolbarCoordinator(requireContext()).apply { - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - } + + inflater.inflate(R.layout.annotation_toolbar_layout, rootView, true) + annotationToolbar = rootView.findViewById(R.id.annotationToolbar) val pdfContentLayout = rootView.findViewById( @@ -313,8 +288,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { pdfContentLayout.addView(annotationView) pdfContentLayout.addView(wetStrokesView) - rootView.addView(toolbarCoordinator) - return rootView } @@ -345,7 +318,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { setupPdfViewListeners() setupAnnotationViewListeners() setupAnnotationToolbar() - setupToolbarCoordinator(annotationToolbar) } private fun setupAnnotationViewListeners() { @@ -380,10 +352,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { } } - private fun setupToolbarCoordinator(toolbar: AnnotationToolbar) { - toolbarCoordinator.apply { attachToolbar(toolbar) } - } - /** * If the document is an [EditablePdfDocument], sets it for editing and initializes draft state. * This method must call `super.onLoadDocumentSuccess(document)` first. @@ -414,9 +382,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { wetStrokesView.removeFinishedStrokesListener(wetStrokesOnFinishedListener) annotationToolbar.setAnnotationToolbarListener(null) pdfContainer.setOnTouchListener(null) - if (::annotationToolbar.isInitialized) { - annotationToolbar.removeOnLayoutChangeListener(toolbarLayoutChangeListener) - } } private fun updateUiForEditMode(isEnabled: Boolean) { @@ -431,9 +396,8 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { } else { annotationToolbar.apply { reset() - wetStrokesView.maskPath = null + post { wetStrokesView.maskPath = null } } - toolbarCoordinator.updateLayout() } } @@ -606,7 +570,6 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { } private fun setupAnnotationToolbar() { - annotationToolbar.addOnLayoutChangeListener(toolbarLayoutChangeListener) annotationToolbar.setAnnotationToolbarListener( object : AnnotationToolbar.AnnotationToolbarListener { override fun onToolChanged(toolInfo: AnnotationToolInfo) { diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbar.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbar.kt index 4abe678e446ef..f967f2d4722c3 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbar.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbar.kt @@ -17,20 +17,15 @@ package androidx.pdf.ink.view import android.content.Context -import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.os.Parcelable import android.util.AttributeSet import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup import android.view.animation.DecelerateInterpolator import android.widget.LinearLayout import android.widget.LinearLayout.HORIZONTAL -import android.widget.LinearLayout.VERTICAL import androidx.annotation.RestrictTo import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat @@ -48,13 +43,6 @@ import androidx.pdf.ink.view.colorpalette.model.Emoji import androidx.pdf.ink.view.colorpalette.model.PaletteItem import androidx.pdf.ink.view.colorpalette.model.getHighlightPaletteItems import androidx.pdf.ink.view.colorpalette.model.getPenPaletteItems -import androidx.pdf.ink.view.draganddrop.AnnotationToolbarTouchHandler -import androidx.pdf.ink.view.draganddrop.ToolbarDockState -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START -import androidx.pdf.ink.view.draganddrop.ToolbarDragListener -import androidx.pdf.ink.view.layout.AnnotationToolbarConstraintSet import androidx.pdf.ink.view.state.AnnotationToolbarState import androidx.pdf.ink.view.state.ToolbarEffect import androidx.pdf.ink.view.state.ToolbarInitializer @@ -73,22 +61,20 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting /** * A toolbar that hosts a set of annotation tools for interacting with a PDF document. * - * This custom [android.view.ViewGroup] contains a predefined set of [AnnotationToolView] buttons - * such as pen, highlighter, eraser, etc. aligned based on the [dockState] set. + * This custom [android.view.ViewGroup] contains a predefined set of [AnnotaonToolView] buttons such + * as pen, highlighter, eraser, etc. aligned based on the [LinearLayout.orientation] set. */ @RestrictTo(RestrictTo.Scope.LIBRARY) public class AnnotationToolbar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : - ConstraintLayout(context, attrs, defStyle), ToolbarDockState { + ConstraintLayout(context, attrs, defStyle) { private val viewModel = AnnotationToolbarViewModel(ToolbarInitializer.createInitialState(context)) @@ -96,9 +82,9 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : /** * A [android.view.ViewGroup] containing all the annotation tools button. * - * Custom tools can be dynamically added to this container using [ViewGroup.addView]. + * Custom tools can be dynamically added to this container using [LinearLayout.addView]. */ - public val toolTray: ViewGroup + public val toolTray: LinearLayout /** * Controls the enabled state of the undo button. @@ -153,25 +139,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : viewModel.updateState(ToolbarInitializer.createInitialState(context = context)) } - override var dockState: Int - get() = viewModel.state.value.dockedState - set(value) { - if (viewModel.state.value.dockedState == value) return - - viewModel.onAction(ToolbarIntent.DockStateChanged(value)) - } - - /** - * Sets a listener to receive drag events when the toolbar is being moved and docked. - * - * @param dragListener The [ToolbarDragListener] to be notified of drag lifecycle events. - */ - public fun setOnToolbarDragListener(dragListener: ToolbarDragListener) { - toolbarTouchHandler.setOnDragListener(dragListener) - } - - private val constraintSet = AnnotationToolbarConstraintSet(this.context) - private val pen: AnnotationToolView private val highlighter: AnnotationToolView private val eraser: AnnotationToolView @@ -198,14 +165,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : ContextCompat.getDrawable(context, R.drawable.color_palette_icon_stroke)?.mutate() } - private val toolbarTouchHandler: AnnotationToolbarTouchHandler by lazy { - AnnotationToolbarTouchHandler(this) { event -> - // Intercepting a long press during a slide on the brush size selector is unintended. - // Ignore long press detection when the touch target is the brush size selector. - (brushSizeSelectorView.isVisible && brushSizeSelectorView.isTouchInView(event)) - } - } - // Required to disable any animation while performing screenshot tests @VisibleForTesting internal var areAnimationsEnabled: Boolean = true @@ -254,6 +213,9 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : } private fun setupToolTray() { + // default orientation + toolTray.orientation = HORIZONTAL + // Set click listeners for all tool views pen.setOnClickListener { viewModel.onAction(ToolbarIntent.PenToolClicked) } highlighter.setOnClickListener { viewModel.onAction(ToolbarIntent.HighlighterToolClicked) } @@ -295,17 +257,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : } private suspend fun collectUiStates() = coroutineScope { - /** - * Collects any expensive UI updates (such as the toolbar's `dockedState`) only when they - * are distinct from the previous state. - */ - launch { - viewModel.state - .map { it.dockedState } - .distinctUntilChanged() - .collect { dockedState -> updateDockState(dockedState) } - } - launch { viewModel.state.collect { state -> updateExpandedState(state) @@ -506,16 +457,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : } } - override fun onInterceptTouchEvent(event: MotionEvent): Boolean { - return toolbarTouchHandler.onInterceptTouchEvent(event) - } - - override fun onTouchEvent(event: MotionEvent?): Boolean { - if (event == null) return super.onTouchEvent(event) - - return toolbarTouchHandler.onTouchEvent(event) || super.onTouchEvent(event) - } - private fun updateExpandedState(state: AnnotationToolbarState) { if (areAnimationsEnabled) { val transition = AutoTransition().apply { duration = AUTO_TRANSITION_DURATION } @@ -526,31 +467,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : collapsedIcon.isVisible = !state.isExpanded } - private fun updateDockState(dockedState: Int) { - when (dockedState) { - DOCK_STATE_START -> { - if (toolTray is LinearLayout) toolTray.orientation = VERTICAL - undoRedoContainer.orientation = VERTICAL - brushSizeSelectorView.orientation = VERTICAL - constraintSet.dockStateStart.applyTo(this) - } - - DOCK_STATE_BOTTOM -> { - if (toolTray is LinearLayout) toolTray.orientation = HORIZONTAL - undoRedoContainer.orientation = HORIZONTAL - brushSizeSelectorView.orientation = HORIZONTAL - constraintSet.dockStateBottom.applyTo(this) - } - - DOCK_STATE_END -> { - if (toolTray is LinearLayout) toolTray.orientation = VERTICAL - undoRedoContainer.orientation = VERTICAL - brushSizeSelectorView.orientation = VERTICAL - constraintSet.dockStateEnd.applyTo(this) - } - } - } - /** * Interface definition for a callback to be invoked when interaction occurs with the * [AnnotationToolbar]. @@ -565,10 +481,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : */ public fun onToolChanged(toolInfo: AnnotationToolInfo) - /** Called when an undo button is clicked if [canUndo] is set to enable. */ + /** Called when a undo button is clicked if [canUndo] is set to enabled. */ public fun onUndo() - /** Called when a redo button is clicked if [canRedo] is set to enable. */ + /** Called when a redo button is clicked if [canRedo] is set to enabled. */ public fun onRedo() /** @@ -587,10 +503,3 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : private const val AUTO_TRANSITION_DURATION = 250L } } - -/** Helper function to check if a touch event is within the bounds of a given view. */ -internal fun View.isTouchInView(event: MotionEvent): Boolean { - val viewRect = Rect() - getHitRect(viewRect) - return viewRect.contains(event.x.toInt(), event.y.toInt()) -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbarViewModel.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbarViewModel.kt index 2927efe5e6f74..882c29e56309b 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbarViewModel.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/AnnotationToolbarViewModel.kt @@ -93,8 +93,6 @@ internal class AnnotationToolbarViewModel(initialState: AnnotationToolbarState) is ToolbarIntent.BrushSizeChanged -> onBrushSizeChanged(intent) is ToolbarIntent.ColorSelected -> onColorSelected(intent) is ToolbarIntent.DismissPopups -> hideAnyPopup() - is ToolbarIntent.DockStateChanged -> - _state.value = _state.value.copy(dockedState = intent.dockedState) is ToolbarIntent.ExpandToolbar -> expandOrCollapseToolbar(isExpanded = true) is ToolbarIntent.CollapseToolbar -> expandOrCollapseToolbar(isExpanded = false) } diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/AnchorManager.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/AnchorManager.kt deleted file mode 100644 index 14c29d0d1a750..0000000000000 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/AnchorManager.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.view.View -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START -import kotlin.math.hypot - -/** - * Manages a set of views that act as anchor points for a drag-and-drop operation. - * - * This class is responsible for: - * - Toggling the visibility of the anchor views. - * - Calculating which anchor is closest to a given point on the screen. - * - Providing visual feedback by highlighting the closest (active) anchor. - * - * @param left The [View] representing the anchor on the start/left side. - * @param right The [View] representing the anchor on the end/right side. - * @param bottom The [View] representing the anchor on the bottom. - */ -internal class AnchorManager( - private val left: View, - private val right: View, - private val bottom: View, -) { - - private val anchors = - mapOf(DOCK_STATE_START to left, DOCK_STATE_END to right, DOCK_STATE_BOTTOM to bottom) - - fun showAnchors() { - anchors.values.forEach { view -> - // Reset to inactive state initially - view.alpha = ALPHA_INACTIVE - view.visibility = View.VISIBLE - } - } - - fun hideAnchors() { - anchors.values.forEach { it.visibility = View.GONE } - } - - /** Calculates nearest anchor, updates UI highlights, and returns the closest State. */ - fun updateHighlightingAndGetClosest( - currentX: Float, - currentY: Float, - viewWidth: Int, - viewHeight: Int, - ): Int { - val centerX = currentX + viewWidth / 2 - val centerY = currentY + viewHeight / 2 - - val closestEntry = anchors.minByOrNull { (_, view) -> getDistance(centerX, centerY, view) } - - anchors.values.forEach { it.alpha = ALPHA_INACTIVE } - - // Highlight the closest one and return its state. - closestEntry?.let { (state, view) -> - view.alpha = ALPHA_ACTIVE - return state - } - - return DOCK_STATE_BOTTOM - } - - fun getAnchorView(@ToolbarDockState.DockState state: Int): View? = anchors[state] - - private fun getDistance(x1: Float, y1: Float, target: View): Float { - val x2 = target.x + target.width / 2 - val y2 = target.y + target.height / 2 - return hypot(x1 - x2, y1 - y2) - } - - companion object { - // Opacity values for visual feedback - private const val ALPHA_ACTIVE = 0.8f - private const val ALPHA_INACTIVE = 0.3f - } -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/AnnotationToolbarTouchHandler.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/AnnotationToolbarTouchHandler.kt deleted file mode 100644 index ff0c391dc85ee..0000000000000 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/AnnotationToolbarTouchHandler.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.view.GestureDetector -import android.view.HapticFeedbackConstants -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import kotlin.math.abs - -/** - * A delegate class that encapsulates all touch handling logic for the - * [androidx.pdf.ink.view.AnnotationToolbar]. - * - * This handler is responsible for: - * 1. **Detecting a long-press** on the contents of the toolbar to initiate a drag operation. - * 2. **Notifying an external [ToolbarDragListener]** about the start, move, and end of a drag - * gesture, without handling the view movement itself. - * - * @param toolbarView The [androidx.pdf.ink.view.AnnotationToolbar] instance whose touches are being - * handled. - * @param isTouchOnInteractiveChild A lambda to verify if long press is registered on long press - * interactive child(for e.g. brush size selector). - */ -internal class AnnotationToolbarTouchHandler( - private val toolbarView: View, - private val isTouchOnInteractiveChild: (MotionEvent) -> Boolean, -) { - private var isDragging = false - private val touchSlop = ViewConfiguration.get(toolbarView.context).scaledTouchSlop - - private var dragListener: ToolbarDragListener? = null - - fun setOnDragListener(listener: ToolbarDragListener) { - dragListener = listener - } - - private val gestureDetector: GestureDetector = - GestureDetector( - toolbarView.context, - object : GestureDetector.SimpleOnGestureListener() { - override fun onLongPress(event: MotionEvent) { - isDragging = true - // Critical: Stop children (buttons) from handling this touch any further - toolbarView.parent.requestDisallowInterceptTouchEvent(true) - toolbarView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - dragListener?.onDragStart(event) - } - - override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent, - distanceX: Float, - distanceY: Float, - ): Boolean { - val dx = abs(e2.x - (e1?.x ?: 0f)) - val dy = abs(e2.y - (e1?.y ?: 0f)) - - // Only make a decision once the user has moved past the touch slop threshold - if (dx > touchSlop || dy > touchSlop) { - gestureDetector.setIsLongpressEnabled(false) - } - - // We return false here because we don't want onScroll to consume the event. - // We only use it to detect the start of a scroll and disable long press. - return false - } - - override fun onDown(e: MotionEvent): Boolean { - // Re-enable long press detection at the start of every new gesture. - gestureDetector.setIsLongpressEnabled(true) - // Necessary to continue tracking the gesture - return true - } - }, - ) - - fun onInterceptTouchEvent(event: MotionEvent): Boolean { - if (isTouchOnInteractiveChild(event)) return false - - gestureDetector.onTouchEvent(event) - - // If dragging is in progress, "steal" the event stream from child views (buttons) - return isDragging - } - - fun onTouchEvent(event: MotionEvent): Boolean { - if (isTouchOnInteractiveChild(event)) return false - - gestureDetector.onTouchEvent(event) - - when (event.actionMasked) { - MotionEvent.ACTION_MOVE -> { - if (isDragging) dragListener?.onDragMove(event) - } - MotionEvent.ACTION_UP, - MotionEvent.ACTION_CANCEL -> { - if (isDragging) dragListener?.onDragEnd() - - isDragging = false - toolbarView.parent.requestDisallowInterceptTouchEvent(false) - } - } - - return isDragging - } -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarCoordinator.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarCoordinator.kt deleted file mode 100644 index bb2fa9bbf5662..0000000000000 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarCoordinator.kt +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.content.Context -import android.util.AttributeSet -import android.view.Gravity -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.animation.OvershootInterpolator -import android.widget.FrameLayout -import androidx.pdf.ink.R -import androidx.pdf.ink.view.AnnotationToolbar -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START -import org.jetbrains.annotations.VisibleForTesting - -/** - * A [FrameLayout] layout that manages the dragging, dropping, and docking of an - * [AnnotationToolbar]. - * - * This coordinator is responsible for providing visible anchor points for docking, listening for - * drag gestures initiated on the attached toolbar, moving the toolbar in response to the user's - * drag input and finally snapping it to the closest anchor when the drag ends. - * - * It also applied the correct layout parameters and orientation for toolbar's final docked state. - */ -internal class ToolbarCoordinator(context: Context, attrs: AttributeSet? = null) : - FrameLayout(context, attrs) { - - // Required to disable any animation while performing ui tests - @VisibleForTesting internal var areAnimationsEnabled: Boolean = true - - private var toolbar: AnnotationToolbar? = null - - private val anchorManager: AnchorManager - - private lateinit var toolbarDragListener: ToolbarDragListener - - private val collapseToolWidth = resources.getDimensionPixelSize(R.dimen.annotation_tool_width) - private val collapsedToolHeight = - resources.getDimensionPixelSize(R.dimen.annotation_tool_height) - - private val margin16Dp = resources.getDimensionPixelSize(R.dimen.margin_16dp) - - init { - LayoutInflater.from(context).inflate(R.layout.toolbar_coordinator, this, true) - - anchorManager = - AnchorManager( - left = findViewById(R.id.anchorLeft), - right = findViewById(R.id.anchorRight), - bottom = findViewById(R.id.anchorBottom), - ) - } - - /** - * Attaches the [AnnotationToolbar] to this coordinator and sets up drag-and-drop handling. - * - * @param view The [AnnotationToolbar] instance to manage. - */ - fun attachToolbar(view: AnnotationToolbar) { - // Remove if already added - toolbar?.let { removeView(it) } - - this.toolbar = view - - addView(view) - initializeDragAndDrop() - updateLayout() - } - - /** Re-applies the layout constraints for the toolbar's current dock state. */ - fun updateLayout() { - toolbar?.post { - applyDockLayoutParams( - toolbar?.dockState ?: throw IllegalStateException("dock state not initialized") - ) - } - } - - private fun initializeDragAndDrop() { - val toolbar = toolbar ?: return - val isToolbarVertical = toolbar.dockState != DOCK_STATE_BOTTOM - - toolbarDragListener = - object : ToolbarDragListener { - - override fun onDragStart(event: MotionEvent) { - anchorManager.showAnchors() - toolbar.collapseToolbar() - } - - override fun onDragMove(event: MotionEvent) { - val collapseViewSize = - if (isToolbarVertical) collapsedToolHeight else collapseToolWidth - toolbar.x = event.rawX - collapseViewSize - toolbar.y = event.rawY - collapseViewSize - - anchorManager.updateHighlightingAndGetClosest( - toolbar.x, - toolbar.y, - toolbar.width, - toolbar.height, - ) - } - - override fun onDragEnd() { - val closestState = - anchorManager.updateHighlightingAndGetClosest( - toolbar.x, - toolbar.y, - toolbar.width, - toolbar.height, - ) - - anchorManager.hideAnchors() - - snapToState(closestState) - } - } - - toolbar.setOnToolbarDragListener(toolbarDragListener) - } - - /** - * Animates the toolbar to its final position over the target anchor. - * - * @param state The target [ToolbarDockState] to snap to. - */ - private fun snapToState(@ToolbarDockState.DockState state: Int) { - val localToolbar = toolbar ?: return - val targetAnchor = anchorManager.getAnchorView(state) ?: return - - // Calculate target position (centering toolbar over anchor) - val targetX = targetAnchor.x + (targetAnchor.width / 2) - (localToolbar.width / 2) - val targetY = targetAnchor.y + (targetAnchor.height / 2) - (localToolbar.height / 2) - - dockToolbar(localToolbar, targetX, targetY, state) - } - - private fun dockToolbar( - toolbar: AnnotationToolbar, - targetX: Float, - targetY: Float, - state: Int, - ) { - if (areAnimationsEnabled) { - toolbar - .animate() - .x(targetX) - .y(targetY) - .setDuration(SNAP_ANIMATION_DURATION) - .setInterpolator(OvershootInterpolator(SNAP_BOUNCE_TENSION)) // The "Snap" bounce - .withEndAction { - applyDockLayoutParams(state) - toolbar.post { toolbar.expandToolbar() } - } - .start() - } else { - toolbar.x = targetX - toolbar.y = targetY - applyDockLayoutParams(state) - toolbar.post { toolbar.expandToolbar() } - } - } - - /** - * Applies the final layout parameters to the toolbar based on its new docked state. - * - * @param state The target [ToolbarDockState]. - */ - private fun applyDockLayoutParams(@ToolbarDockState.DockState state: Int) { - val localToolbar = toolbar ?: return - - val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) - - // Reset toolbar translation; critical as we previously animated view.translateX and - // view.translateY - localToolbar.translationX = 0f - localToolbar.translationY = 0f - - when (state) { - DOCK_STATE_START -> { - params.gravity = Gravity.CENTER_VERTICAL or Gravity.START - params.marginStart = margin16Dp - } - DOCK_STATE_END -> { - params.gravity = Gravity.CENTER_VERTICAL or Gravity.END - params.marginEnd = margin16Dp - } - DOCK_STATE_BOTTOM -> { - params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - params.bottomMargin = margin16Dp - } - } - - localToolbar.layoutParams = params - localToolbar.dockState = state - } - - companion object { - private const val SNAP_ANIMATION_DURATION = 250L - private const val SNAP_BOUNCE_TENSION = 1.0f - } -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarDockState.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarDockState.kt deleted file mode 100644 index c8f70cc8a2b95..0000000000000 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarDockState.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import androidx.annotation.IntDef -import androidx.annotation.RestrictTo - -/** - * Interface defining a contract for UI components that can be docked to specific edges of a - * container, such as the start, end, or bottom. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY) -public interface ToolbarDockState { - - /** - * An annotation that defines the set of integer constants representing the valid docking states - * for a component. - */ - @Retention(AnnotationRetention.SOURCE) - @IntDef(DOCK_STATE_START, DOCK_STATE_BOTTOM, DOCK_STATE_END) - public annotation class DockState - - /** - * The current docking state of the component. - * - * Implementations should update their layout and orientation according to the value of this - * property. - */ - @DockState public var dockState: Int - - public companion object { - /** - * Represents a state where the component is docked to the start (left, in LTR mode) edge. - */ - public const val DOCK_STATE_START: Int = 0 - - /** Represents a state where the component is docked to the bottom edge. */ - public const val DOCK_STATE_BOTTOM: Int = 1 - - /** - * Represents a state where the component is docked to the end (right, in LTR mode) edge. - */ - public const val DOCK_STATE_END: Int = 2 - } -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarDragListener.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarDragListener.kt deleted file mode 100644 index ef7205acf32d7..0000000000000 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/draganddrop/ToolbarDragListener.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.view.MotionEvent -import androidx.annotation.RestrictTo -import androidx.pdf.ink.view.AnnotationToolbar - -/** - * Interface definition for callbacks to be invoked when a drag gesture is performed on the - * [AnnotationToolbar]. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY) -public interface ToolbarDragListener { - /** - * Called when a long-press gesture is detected and a drag operation is initiated. - * - * @param event The raw [MotionEvent] containing the current pointer coordinates, which can be - * used to calculate initial touch offset. - */ - public fun onDragStart(event: MotionEvent) - - /** - * Called for each movement of the user's finger across the screen during a drag. - * - * @param event The raw [MotionEvent] containing the current pointer coordinates, which can be - * used to update the toolbar's position. - */ - public fun onDragMove(event: MotionEvent) - - /** - * Called when the user lifts their finger, completing the drag operation. This is typically - * used to snap the toolbar to a final docked position. - */ - public fun onDragEnd() -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/layout/AnnotationToolbarConstraintSet.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/layout/AnnotationToolbarConstraintSet.kt deleted file mode 100644 index 6f50009964f9f..0000000000000 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/layout/AnnotationToolbarConstraintSet.kt +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.layout - -import android.content.Context -import androidx.constraintlayout.widget.ConstraintSet -import androidx.pdf.ink.R -import androidx.pdf.ink.view.draganddrop.ToolbarDockState -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START - -internal class AnnotationToolbarConstraintSet(context: Context) { - - private val margin16dp = context.resources.getDimensionPixelSize(R.dimen.margin_16dp) - private val colorPaletteMaxWidth = - context.resources.getDimensionPixelSize(R.dimen.color_palette_max_width) - - val dockStateStart: ConstraintSet = createConstraintSetFor(DOCK_STATE_START) - val dockStateEnd: ConstraintSet = createConstraintSetFor(DOCK_STATE_END) - val dockStateBottom: ConstraintSet = createConstraintSetFor(DOCK_STATE_BOTTOM) - - /** - * Creates a new [ConstraintSet] configured for the given [ToolbarDockState.DockState]. This is - * the main factory method that delegates to helper functions. - */ - private fun createConstraintSetFor(@ToolbarDockState.DockState dockState: Int): ConstraintSet { - return ConstraintSet().apply { - applyToolTrayConstraints(dockState) - applyColorPaletteConstraints(dockState) - applyBrushSliderConstraints(dockState) - } - } - - /** Applies the constraints for the main `scrollable_tool_tray_container`. */ - private fun ConstraintSet.applyToolTrayConstraints(@ToolbarDockState.DockState dockState: Int) { - clear(R.id.scrollable_tool_tray_container, ConstraintSet.TOP) - clear(R.id.scrollable_tool_tray_container, ConstraintSet.BOTTOM) - clear(R.id.scrollable_tool_tray_container, ConstraintSet.START) - clear(R.id.scrollable_tool_tray_container, ConstraintSet.END) - - constrainWidth(R.id.scrollable_tool_tray_container, ConstraintSet.WRAP_CONTENT) - constrainHeight(R.id.scrollable_tool_tray_container, ConstraintSet.WRAP_CONTENT) - - when (dockState) { - DOCK_STATE_START -> { - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - ) - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ConstraintSet.PARENT_ID, - ConstraintSet.BOTTOM, - ) - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - ) - } - DOCK_STATE_END -> { - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - ) - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ConstraintSet.PARENT_ID, - ConstraintSet.BOTTOM, - ) - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END, - ) - } - DOCK_STATE_BOTTOM -> { - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - ) - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END, - ) - connect( - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ConstraintSet.PARENT_ID, - ConstraintSet.BOTTOM, - ) - } - } - } - - /** Applies the constraints for the `color_palette` view. */ - private fun ConstraintSet.applyColorPaletteConstraints( - @ToolbarDockState.DockState dockState: Int - ) { - clear(R.id.color_palette, ConstraintSet.TOP) - clear(R.id.color_palette, ConstraintSet.BOTTOM) - clear(R.id.color_palette, ConstraintSet.START) - clear(R.id.color_palette, ConstraintSet.END) - setVisibility(R.id.color_palette, ConstraintSet.GONE) - - when (dockState) { - DOCK_STATE_START -> { - constrainWidth(R.id.color_palette, colorPaletteMaxWidth) - constrainHeight(R.id.color_palette, ConstraintSet.MATCH_CONSTRAINT) - connect( - R.id.color_palette, - ConstraintSet.TOP, - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ) - connect( - R.id.color_palette, - ConstraintSet.BOTTOM, - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ) - connect( - R.id.color_palette, - ConstraintSet.START, - R.id.scrollable_tool_tray_container, - ConstraintSet.END, - ) - setMargin(R.id.color_palette, ConstraintSet.START, margin16dp) - } - DOCK_STATE_END -> { - constrainWidth(R.id.color_palette, colorPaletteMaxWidth) - constrainHeight(R.id.color_palette, ConstraintSet.MATCH_CONSTRAINT) - connect( - R.id.color_palette, - ConstraintSet.TOP, - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ) - connect( - R.id.color_palette, - ConstraintSet.BOTTOM, - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ) - connect( - R.id.color_palette, - ConstraintSet.END, - R.id.scrollable_tool_tray_container, - ConstraintSet.START, - ) - setMargin(R.id.color_palette, ConstraintSet.END, margin16dp) - } - DOCK_STATE_BOTTOM -> { - constrainWidth(R.id.color_palette, ConstraintSet.MATCH_CONSTRAINT) - constrainHeight(R.id.color_palette, ConstraintSet.WRAP_CONTENT) - connect( - R.id.color_palette, - ConstraintSet.START, - R.id.scrollable_tool_tray_container, - ConstraintSet.START, - ) - connect( - R.id.color_palette, - ConstraintSet.END, - R.id.scrollable_tool_tray_container, - ConstraintSet.END, - ) - connect( - R.id.color_palette, - ConstraintSet.BOTTOM, - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ) - setMargin(R.id.color_palette, ConstraintSet.BOTTOM, margin16dp) - } - } - } - - /** Applies the constraints for the `brush_size_selector` view. */ - private fun ConstraintSet.applyBrushSliderConstraints( - @ToolbarDockState.DockState dockState: Int - ) { - clear(R.id.brush_size_selector, ConstraintSet.TOP) - clear(R.id.brush_size_selector, ConstraintSet.BOTTOM) - clear(R.id.brush_size_selector, ConstraintSet.START) - clear(R.id.brush_size_selector, ConstraintSet.END) - setVisibility(R.id.brush_size_selector, ConstraintSet.GONE) - - when (dockState) { - DOCK_STATE_START -> { - constrainWidth(R.id.brush_size_selector, ConstraintSet.WRAP_CONTENT) - constrainHeight(R.id.brush_size_selector, ConstraintSet.MATCH_CONSTRAINT) - connect( - R.id.brush_size_selector, - ConstraintSet.TOP, - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ) - connect( - R.id.brush_size_selector, - ConstraintSet.BOTTOM, - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ) - connect( - R.id.brush_size_selector, - ConstraintSet.START, - R.id.scrollable_tool_tray_container, - ConstraintSet.END, - ) - setMargin(R.id.brush_size_selector, ConstraintSet.START, margin16dp) - } - DOCK_STATE_END -> { - constrainWidth(R.id.brush_size_selector, ConstraintSet.WRAP_CONTENT) - constrainHeight(R.id.brush_size_selector, ConstraintSet.MATCH_CONSTRAINT) - connect( - R.id.brush_size_selector, - ConstraintSet.TOP, - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ) - connect( - R.id.brush_size_selector, - ConstraintSet.BOTTOM, - R.id.scrollable_tool_tray_container, - ConstraintSet.BOTTOM, - ) - connect( - R.id.brush_size_selector, - ConstraintSet.END, - R.id.scrollable_tool_tray_container, - ConstraintSet.START, - ) - setMargin(R.id.brush_size_selector, ConstraintSet.END, margin16dp) - } - DOCK_STATE_BOTTOM -> { - constrainWidth(R.id.brush_size_selector, ConstraintSet.MATCH_CONSTRAINT) - constrainHeight(R.id.brush_size_selector, ConstraintSet.WRAP_CONTENT) - connect( - R.id.brush_size_selector, - ConstraintSet.START, - R.id.scrollable_tool_tray_container, - ConstraintSet.START, - ) - connect( - R.id.brush_size_selector, - ConstraintSet.END, - R.id.scrollable_tool_tray_container, - ConstraintSet.END, - ) - connect( - R.id.brush_size_selector, - ConstraintSet.BOTTOM, - R.id.scrollable_tool_tray_container, - ConstraintSet.TOP, - ) - setMargin(R.id.brush_size_selector, ConstraintSet.BOTTOM, margin16dp) - } - } - } -} diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarState.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarState.kt index a836d971ffaf7..166b5cd309e57 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarState.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarState.kt @@ -23,7 +23,6 @@ import androidx.core.os.ParcelCompat import androidx.pdf.ink.view.brush.BrushSizeSelectorView import androidx.pdf.ink.view.colorpalette.ColorPaletteView import androidx.pdf.ink.view.colorpalette.model.PaletteItem -import androidx.pdf.ink.view.draganddrop.ToolbarDockState import androidx.pdf.ink.view.tool.model.AnnotationToolsKey.HIGHLIGHTER import androidx.pdf.ink.view.tool.model.AnnotationToolsKey.PEN @@ -117,9 +116,6 @@ internal data class AnnotationToolbarState( */ val highlighterState: ToolAttributes, - /** The current docking state of the toolbar. */ - @get:ToolbarDockState.DockState @param:ToolbarDockState.DockState val dockedState: Int, - /** Whether the toolbar is currently expanded. */ val isExpanded: Boolean, ) : Parcelable { @@ -151,7 +147,6 @@ internal data class AnnotationToolbarState( ToolAttributes::class.java, ) ), - dockedState = parcel.readInt(), isExpanded = parcel.readByte() != 0.toByte(), ) @@ -165,7 +160,6 @@ internal data class AnnotationToolbarState( parcel.writeByte(if (isColorPaletteVisible) 1 else 0) parcel.writeParcelable(penState, flags) parcel.writeParcelable(highlighterState, flags) - parcel.writeInt(dockedState) parcel.writeByte(if (isExpanded) 1 else 0) } diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarInitializer.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarInitializer.kt index 02642d1a38e40..04ac89f88304f 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarInitializer.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarInitializer.kt @@ -19,17 +19,11 @@ package androidx.pdf.ink.view.state import android.content.Context import androidx.pdf.ink.view.colorpalette.model.getHighlightPaletteItems import androidx.pdf.ink.view.colorpalette.model.getPenPaletteItems -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END import androidx.pdf.ink.view.tool.model.AnnotationToolsKey.PEN /** Responsible for creating the fully-formed initial state for the annotation toolbar */ internal object ToolbarInitializer { - // The smallest screen width threshold in density-independent pixels (dp) used to differentiate - // between phone and tablet form factors. - private const val TABLET_SMALLEST_SCREEN_WIDTH_DP = 600 - /** * Creates the default initial `AnnotationToolbarState`. * @@ -63,23 +57,7 @@ internal object ToolbarInitializer { selectedColorIndex = defaultHighlighterColorIndex, paletteItem = highlightPaletteItems[defaultHighlighterColorIndex], ), - dockedState = getDefaultDockState(context), isExpanded = true, ) } - - /** - * Determines the default dock state for the toolbar based on the device's screen size. - * - On tablets (smallest width >= 600dp), it returns [DOCK_STATE_END] to place the toolbar - * vertically on the side. - * - On phones, it returns [DOCK_STATE_BOTTOM] to place the toolbar horizontally at the bottom. - * - * @param context The context used to access screen configuration. - * @return The calculated default dock state. - */ - private fun getDefaultDockState(context: Context): Int { - val screenWidthDp = context.resources.configuration.smallestScreenWidthDp - return if (screenWidthDp >= TABLET_SMALLEST_SCREEN_WIDTH_DP) DOCK_STATE_END - else DOCK_STATE_BOTTOM - } } diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarIntentAndEffects.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarIntentAndEffects.kt index d5054af05d52f..ab33f093ed837 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarIntentAndEffects.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/view/state/ToolbarIntentAndEffects.kt @@ -71,9 +71,6 @@ internal sealed interface ToolbarIntent { /** Intent to dismiss any popups(brush slider, color palette) shown. */ object DismissPopups : ToolbarIntent - /** Intent to update the docked state of the toolbar. */ - data class DockStateChanged(val dockedState: Int) : ToolbarIntent - /** Intent to expand the toolbar. */ object ExpandToolbar : ToolbarIntent diff --git a/pdf/pdf-ink/src/main/res/drawable/anchor_background.xml b/pdf/pdf-ink/src/main/res/drawable/anchor_background.xml deleted file mode 100644 index cbca86fb9ad8a..0000000000000 --- a/pdf/pdf-ink/src/main/res/drawable/anchor_background.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/pdf/pdf-ink/src/main/res/layout/annotation_toolbar.xml b/pdf/pdf-ink/src/main/res/layout/annotation_toolbar.xml index 7a097226249c9..9e9216d383f96 100644 --- a/pdf/pdf-ink/src/main/res/layout/annotation_toolbar.xml +++ b/pdf/pdf-ink/src/main/res/layout/annotation_toolbar.xml @@ -21,29 +21,21 @@ android:animateLayoutChanges="true" android:padding="@dimen/padding_8dp"> - - - - - - - - + + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> diff --git a/pdf/pdf-ink/src/main/res/layout/toolbar_coordinator.xml b/pdf/pdf-ink/src/main/res/layout/toolbar_coordinator.xml deleted file mode 100644 index ceaf701c43def..0000000000000 --- a/pdf/pdf-ink/src/main/res/layout/toolbar_coordinator.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/pdf/pdf-ink/src/main/res/values/dimens.xml b/pdf/pdf-ink/src/main/res/values/dimens.xml index eb827e2153320..4a40ef94a3ce2 100644 --- a/pdf/pdf-ink/src/main/res/values/dimens.xml +++ b/pdf/pdf-ink/src/main/res/values/dimens.xml @@ -22,7 +22,6 @@ 12dp 16dp 8dp - 176dp 48dp @@ -41,8 +40,5 @@ 40dp 1dp 28dp - - 388dp - 72dp \ No newline at end of file diff --git a/pdf/pdf-ink/src/main/res/values/ids.xml b/pdf/pdf-ink/src/main/res/values/ids.xml index 7189b7f9a811e..42f4ee8f27cfd 100644 --- a/pdf/pdf-ink/src/main/res/values/ids.xml +++ b/pdf/pdf-ink/src/main/res/values/ids.xml @@ -15,7 +15,5 @@ --> - - \ No newline at end of file diff --git a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/ViewExtensionsTest.kt b/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/ViewExtensionsTest.kt deleted file mode 100644 index 56fdfcbea1b65..0000000000000 --- a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/ViewExtensionsTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view - -import android.content.Context -import android.view.MotionEvent -import android.view.View -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [Config.TARGET_SDK]) -class ViewExtensionsTest { - - private lateinit var context: Context - private lateinit var view: View - - @Before - fun setUp() { - context = ApplicationProvider.getApplicationContext() - view = View(context) - // Define the view's bounds for testing. Let's assume it's at (100, 100) - // with a size of 200x100. So its rect is (100, 100, 300, 200). - view.layout(100, 100, 300, 200) - } - - @Test - fun isTouchInView_whenTouchIsInside_returnsTrue() { - // Create a touch event in the middle of the view. - val touchEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 150f, 150f, 0) - - assertThat(view.isTouchInView(touchEvent)).isTrue() - } - - @Test - fun isTouchInView_whenTouchIsOutside_returnsFalse() { - // Create a touch event far outside the view's bounds. - val touchEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500f, 500f, 0) - - assertThat(view.isTouchInView(touchEvent)).isFalse() - } - - @Test - fun isTouchInView_whenTouchIsOnLeftEdge_returnsTrue() { - // Create a touch event exactly on the left edge. - val touchEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 100f, 150f, 0) - - assertThat(view.isTouchInView(touchEvent)).isTrue() - } - - @Test - fun isTouchInView_whenTouchIsJustOutsideRightEdge_returnsFalse() { - // The rect's right is 300, so a touch at 300.0f is outside because - // Rect.contains checks for left <= x < right. - val touchEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 300f, 150f, 0) - - assertThat(view.isTouchInView(touchEvent)).isFalse() - } - - @Test - fun isTouchInView_whenTouchIsOnTopEdge_returnsTrue() { - // Create a touch event exactly on the top edge. - val touchEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 150f, 100f, 0) - - assertThat(view.isTouchInView(touchEvent)).isTrue() - } - - @Test - fun isTouchInView_whenTouchIsJustOutsideBottomEdge_returnsFalse() { - // The rect's bottom is 200, so a touch at 200.0f is outside. - val touchEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 150f, 200f, 0) - - assertThat(view.isTouchInView(touchEvent)).isFalse() - } -} diff --git a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/draganddrop/AnchorManagerTest.kt b/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/draganddrop/AnchorManagerTest.kt deleted file mode 100644 index 23d4fce3f4e98..0000000000000 --- a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/draganddrop/AnchorManagerTest.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.content.Context -import android.view.View -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_START -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -@org.robolectric.annotation.Config(sdk = [org.robolectric.annotation.Config.TARGET_SDK]) -class AnchorManagerTest { - - private lateinit var context: Context - private lateinit var leftAnchor: View - private lateinit var rightAnchor: View - private lateinit var bottomAnchor: View - private lateinit var anchorManager: AnchorManager - - private val ALPHA_ACTIVE = 0.8f - private val ALPHA_INACTIVE = 0.3f - - @Before - fun setUp() { - context = ApplicationProvider.getApplicationContext() - leftAnchor = View(context) - rightAnchor = View(context) - bottomAnchor = View(context) - - // Give anchors some size and position for distance calculations - // Left: (0, 500), Right: (1000, 500), Bottom: (500, 1000) - leftAnchor.layout(0, 450, 100, 550) - rightAnchor.layout(900, 450, 1000, 550) - bottomAnchor.layout(450, 950, 550, 1050) - - anchorManager = AnchorManager(leftAnchor, rightAnchor, bottomAnchor) - } - - @Test - fun showAnchors_makesAllAnchorsVisibleAndInactive() { - anchorManager.showAnchors() - - assertThat(leftAnchor.visibility).isEqualTo(View.VISIBLE) - assertThat(rightAnchor.visibility).isEqualTo(View.VISIBLE) - assertThat(bottomAnchor.visibility).isEqualTo(View.VISIBLE) - - assertThat(leftAnchor.alpha).isEqualTo(ALPHA_INACTIVE) - assertThat(rightAnchor.alpha).isEqualTo(ALPHA_INACTIVE) - assertThat(bottomAnchor.alpha).isEqualTo(ALPHA_INACTIVE) - } - - @Test - fun hideAnchors_makesAllAnchorsGone() { - anchorManager.showAnchors() // First make them visible - anchorManager.hideAnchors() - - assertThat(leftAnchor.visibility).isEqualTo(View.GONE) - assertThat(rightAnchor.visibility).isEqualTo(View.GONE) - assertThat(bottomAnchor.visibility).isEqualTo(View.GONE) - } - - @Test - fun updateHighlighting_nearLeft_highlightsLeftAndReturnsStart() { - // Position "toolbar" center near left anchor (50, 500) - val state = - anchorManager.updateHighlightingAndGetClosest( - currentX = 0f, - currentY = 450f, - viewWidth = 100, - viewHeight = 100, - ) - - assertThat(state).isEqualTo(DOCK_STATE_START) - assertThat(leftAnchor.alpha).isEqualTo(ALPHA_ACTIVE) - assertThat(rightAnchor.alpha).isEqualTo(ALPHA_INACTIVE) - assertThat(bottomAnchor.alpha).isEqualTo(ALPHA_INACTIVE) - } - - @Test - fun updateHighlighting_nearBottom_highlightsBottomAndReturnsBottom() { - // Position "toolbar" center near bottom anchor (500, 1000) - val state = - anchorManager.updateHighlightingAndGetClosest( - currentX = 450f, - currentY = 950f, - viewWidth = 100, - viewHeight = 100, - ) - - assertThat(state).isEqualTo(DOCK_STATE_BOTTOM) - assertThat(bottomAnchor.alpha).isEqualTo(ALPHA_ACTIVE) - assertThat(leftAnchor.alpha).isEqualTo(ALPHA_INACTIVE) - } - - @Test - fun getAnchorView_returnsCorrectViewForState() { - assertThat(anchorManager.getAnchorView(DOCK_STATE_START)).isEqualTo(leftAnchor) - assertThat(anchorManager.getAnchorView(DOCK_STATE_END)).isEqualTo(rightAnchor) - assertThat(anchorManager.getAnchorView(DOCK_STATE_BOTTOM)).isEqualTo(bottomAnchor) - } -} diff --git a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/draganddrop/AnnotationToolbarTouchHandlerTest.kt b/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/draganddrop/AnnotationToolbarTouchHandlerTest.kt deleted file mode 100644 index 03b1455066a26..0000000000000 --- a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/draganddrop/AnnotationToolbarTouchHandlerTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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.ink.view.draganddrop - -import android.content.Context -import android.os.SystemClock -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import android.widget.FrameLayout -import androidx.test.core.app.ApplicationProvider -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.any -import org.mockito.kotlin.spy -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import org.robolectric.shadows.ShadowLooper - -@RunWith(RobolectricTestRunner::class) -@org.robolectric.annotation.Config(sdk = [org.robolectric.annotation.Config.TARGET_SDK]) -class AnnotationToolbarTouchHandlerTest { - - private val context = ApplicationProvider.getApplicationContext() - - private lateinit var dummyToolbar: View - private lateinit var parentContainer: FrameLayout // Acts as the parent - - @Mock private lateinit var dragListener: ToolbarDragListener - - private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop - - @Before - fun setup() { - // Initialize @Mock annotations - MockitoAnnotations.openMocks(this) - - parentContainer = spy(FrameLayout(context)) - - dummyToolbar = View(context, null) - - parentContainer.addView(dummyToolbar) - } - - @Test - fun test_onTouchEvent_onInteractiveChild_ignoresDrag() { - val handler = AnnotationToolbarTouchHandler(dummyToolbar) { true } - handler.setOnDragListener(dragListener) - val event = obtainEvent(MotionEvent.ACTION_DOWN, 0f, 0f) - - val result = handler.onTouchEvent(event) - - assertFalse(result) - verify(dragListener, never()).onDragStart(any()) - } - - @Test - fun test_touchHandler_longPress_startDrag() { - val handler = AnnotationToolbarTouchHandler(dummyToolbar) { false } - handler.setOnDragListener(dragListener) - val event = obtainEvent(MotionEvent.ACTION_DOWN, 0f, 0f) - - handler.onTouchEvent(event) - - // fast-forward time - ShadowLooper.runUiThreadTasksIncludingDelayedTasks() - - val shadowParent = shadowOf(parentContainer) - // Check the actual state of the view - assertTrue(shadowParent.disallowInterceptTouchEvent) - - verify(dragListener).onDragStart(any()) - } - - @Test - fun test_touchHandler_continuous_actionMove_doesNotStartDrag() { - val handler = AnnotationToolbarTouchHandler(dummyToolbar) { false } - handler.setOnDragListener(dragListener) - - val downEvent = obtainEvent(MotionEvent.ACTION_DOWN, 0f, 0f) - val moveEvent = obtainEvent(MotionEvent.ACTION_MOVE, 0f, (touchSlop + 10).toFloat()) - - handler.onTouchEvent(downEvent) - handler.onTouchEvent(moveEvent) - - ShadowLooper.runUiThreadTasksIncludingDelayedTasks() - - verify(dragListener, never()).onDragStart(any()) - } - - @Test - fun test_touchHandler_actionUp_endsDrag() { - val handler = AnnotationToolbarTouchHandler(dummyToolbar) { false } - handler.setOnDragListener(dragListener) - val downEvent = obtainEvent(MotionEvent.ACTION_DOWN, 0f, 0f) - val upEvent = obtainEvent(MotionEvent.ACTION_UP, 0f, 0f) - - handler.onTouchEvent(downEvent) - ShadowLooper.runUiThreadTasksIncludingDelayedTasks() // Trigger Drag - handler.onTouchEvent(upEvent) - - verify(dragListener).onDragEnd() - - val shadowParent = shadowOf(parentContainer) - // Assert parent's disallowInterceptTouchEvent state - assertFalse(shadowParent.disallowInterceptTouchEvent) - } - - private fun obtainEvent(action: Int, x: Float, y: Float): MotionEvent { - return MotionEvent.obtain( - SystemClock.uptimeMillis(), - SystemClock.uptimeMillis(), - action, - x, - y, - 0, - ) - } -} diff --git a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarViewModelTest.kt b/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarViewModelTest.kt index 37a6966f7f280..24725af20f27b 100644 --- a/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarViewModelTest.kt +++ b/pdf/pdf-ink/src/test/kotlin/androidx/pdf/ink/view/state/AnnotationToolbarViewModelTest.kt @@ -19,7 +19,6 @@ package androidx.pdf.ink.view.state import androidx.pdf.ink.view.AnnotationToolbarViewModel import androidx.pdf.ink.view.brush.model.BrushSizes import androidx.pdf.ink.view.colorpalette.model.Color -import androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_BOTTOM import androidx.pdf.ink.view.tool.Eraser import androidx.pdf.ink.view.tool.Highlighter import androidx.pdf.ink.view.tool.Pen @@ -51,7 +50,6 @@ class AnnotationToolbarViewModelTest { canRedo = false, highlighterState = ToolAttributes(1, 2, Color(123, 4, 5, "blue color")), penState = ToolAttributes(2, 3, Color(123, 4, 5, "red color")), - dockedState = DOCK_STATE_BOTTOM, isExpanded = true, ) @@ -327,7 +325,6 @@ class AnnotationToolbarViewModelTest { canRedo = false, highlighterState = ToolAttributes(1, 2, Color(123, 4, 5, "blue color")), penState = ToolAttributes(2, 3, Color(123, 4, 5, "red color")), - dockedState = DOCK_STATE_BOTTOM, isExpanded = true, ) val collectedEffects = mutableListOf() @@ -373,18 +370,6 @@ class AnnotationToolbarViewModelTest { assertThat(viewmodel.state.value.isBrushSizeSliderVisible).isFalse() } - @Test - fun onAction_DockedStateChanged_updatesState() { - val viewmodel = createViewModel() - // Assuming the initial state is DOCK_STATE_BOTTOM, change it to DOCK_STATE_END - val newDockState = - androidx.pdf.ink.view.draganddrop.ToolbarDockState.Companion.DOCK_STATE_END - - viewmodel.onAction(ToolbarIntent.DockStateChanged(newDockState)) - - assertThat(viewmodel.state.value.dockedState).isEqualTo(newDockState) - } - private fun CoroutineScope.collectInto(flow: Flow, destination: MutableList): Job { // We launch on the receiver CoroutineScope. Dispatchers.Unconfined is good for eager tests. return launch(Dispatchers.Unconfined) { flow.collect { destination.add(it) } } diff --git a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/document/FakePdfDocument.kt b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/document/FakePdfDocument.kt index b10808e16efb4..36d0bcbabb0dc 100644 --- a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/document/FakePdfDocument.kt +++ b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/document/FakePdfDocument.kt @@ -34,6 +34,7 @@ import androidx.pdf.content.PageMatchBounds import androidx.pdf.content.PageSelection import androidx.pdf.content.SelectionBoundary import androidx.pdf.models.FormWidgetInfo +import java.util.concurrent.Executor import kotlin.random.Random /** @@ -76,7 +77,8 @@ internal open class FakePdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) { TODO("Not yet implemented") } diff --git a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt index 634e5f6af38ab..1495b0f971f94 100644 --- a/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt +++ b/pdf/pdf-viewer-fragment/src/androidTest/kotlin/androidx/pdf/viewer/fragment/PdfDocumentViewModelTest.kt @@ -303,7 +303,7 @@ class PdfDocumentViewModelTest { latch.countDown() } } - document.addOnPdfContentInvalidatedListener(listener) + document.addOnPdfContentInvalidatedListener({ command -> command.run() }, listener) // Bounds in content coordinates of the widget on which the edit is applied val widgetArea = Rect(135, 70, 155, 90) diff --git a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt index 800ba81eccbb3..24fc245072927 100644 --- a/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt +++ b/pdf/pdf-viewer/src/androidTest/kotlin/androidx/pdf/view/FakePdfDocument.kt @@ -40,6 +40,7 @@ import androidx.pdf.content.SelectionBoundary import androidx.pdf.models.FormEditInfo import androidx.pdf.models.FormWidgetInfo import androidx.pdf.models.ListItem +import java.util.concurrent.Executor import kotlin.math.roundToInt import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -123,7 +124,8 @@ internal open class FakePdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) { return } diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/PdfDocument.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/PdfDocument.kt index 2768eb0200307..eb9d2264cd7f7 100644 --- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/PdfDocument.kt +++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/PdfDocument.kt @@ -33,6 +33,7 @@ import androidx.pdf.content.PdfPageLinkContent import androidx.pdf.content.PdfPageTextContent import androidx.pdf.models.FormWidgetInfo import java.io.Closeable +import java.util.concurrent.Executor import kotlinx.coroutines.CancellationException /** Represents a PDF document and provides methods to interact with its content. */ @@ -216,9 +217,26 @@ public interface PdfDocument : Closeable { } @RestrictTo(RestrictTo.Scope.LIBRARY) - public fun addOnPdfContentInvalidatedListener(listener: OnPdfContentInvalidatedListener) + /** + * Adds a listener to receive notifications when some regions of the pdf content are + * invalidated. + * + * @param executor The executor on which the listener's methods will be called. + * @param listener The listener to add. + * @see [OnPdfContentInvalidatedListener] + */ + public fun addOnPdfContentInvalidatedListener( + executor: Executor, + listener: OnPdfContentInvalidatedListener, + ) @RestrictTo(RestrictTo.Scope.LIBRARY) + /** + * Removes the listener from the list of listeners which are notified when some regions of the + * pdf content are invalidated. + * + * @param listener The listener to remove. + */ public fun removeOnPdfContentInvalidatedListener(listener: OnPdfContentInvalidatedListener) /** diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt index ea00491295748..94d6ffebd68ed 100644 --- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt +++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt @@ -507,8 +507,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : } /** - * Adds the specified listener to the list of listeners that is notified when any form widget is - * updated due to an edit action on the widget e.g. click on a radio button. + * Removes the specified listener from the list of listeners that is notified when any form + * widget is updated due to an edit action on the widget e.g. click on a radio button. * * @param listener The listener to remove */ @@ -1828,7 +1828,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : this.formFillingEditText = formFillingEditText } - localPdfDocument.addOnPdfContentInvalidatedListener(onPdfContentInvalidatedListener) + localPdfDocument.addOnPdfContentInvalidatedListener( + context.mainExecutor, + onPdfContentInvalidatedListener, + ) val fastScrollCalculator = FastScrollCalculator(context) val fastScrollDrawer = diff --git a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/FakePdfDocument.kt b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/FakePdfDocument.kt index e1afc8b67927c..e0c0e92784f0b 100644 --- a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/FakePdfDocument.kt +++ b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/FakePdfDocument.kt @@ -38,6 +38,7 @@ import androidx.pdf.content.SelectionBoundary import androidx.pdf.models.FormEditInfo import androidx.pdf.models.FormWidgetInfo import androidx.pdf.models.ListItem +import java.util.concurrent.Executor import kotlin.math.roundToInt import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -101,7 +102,8 @@ internal open class FakePdfDocument( } override fun addOnPdfContentInvalidatedListener( - listener: PdfDocument.OnPdfContentInvalidatedListener + executor: Executor, + listener: PdfDocument.OnPdfContentInvalidatedListener, ) { return } diff --git a/tracing/tracing-perfetto-handshake/api/current.ignore b/tracing/tracing-perfetto-handshake/api/current.ignore index 503437f812227..7aa50845299be 100644 --- a/tracing/tracing-perfetto-handshake/api/current.ignore +++ b/tracing/tracing-perfetto-handshake/api/current.ignore @@ -5,6 +5,22 @@ AddedMethod: androidx.tracing.perfetto.handshake.PerfettoSdkHandshake#enableTrac Added method androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.enableTracingImmediate$default(androidx.tracing.perfetto.handshake.PerfettoSdkHandshake,androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.LibrarySource,int,Object) +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ALREADY_ENABLED: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ALREADY_ENABLED +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_CANCELLED: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_CANCELLED +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_MISSING: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_MISSING +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_OTHER: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_OTHER +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_SUCCESS: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_SUCCESS + + RemovedFromKotlin: androidx.tracing.perfetto.handshake.protocol.Response#getMessage(): Source breaking change: method androidx.tracing.perfetto.handshake.protocol.Response.getMessage() can no longer be resolved from Kotlin source RemovedFromKotlin: androidx.tracing.perfetto.handshake.protocol.Response#getRequiredVersion(): diff --git a/tracing/tracing-perfetto-handshake/api/restricted_current.ignore b/tracing/tracing-perfetto-handshake/api/restricted_current.ignore index 503437f812227..7aa50845299be 100644 --- a/tracing/tracing-perfetto-handshake/api/restricted_current.ignore +++ b/tracing/tracing-perfetto-handshake/api/restricted_current.ignore @@ -5,6 +5,22 @@ AddedMethod: androidx.tracing.perfetto.handshake.PerfettoSdkHandshake#enableTrac Added method androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.enableTracingImmediate$default(androidx.tracing.perfetto.handshake.PerfettoSdkHandshake,androidx.tracing.perfetto.handshake.PerfettoSdkHandshake.LibrarySource,int,Object) +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ALREADY_ENABLED: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ALREADY_ENABLED +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_CANCELLED: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_CANCELLED +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_MISSING: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_MISSING +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_OTHER: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_ERROR_OTHER +AddedProperty: androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_SUCCESS: + Added property androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes#RESULT_CODE_SUCCESS + + RemovedFromKotlin: androidx.tracing.perfetto.handshake.protocol.Response#getMessage(): Source breaking change: method androidx.tracing.perfetto.handshake.protocol.Response.getMessage() can no longer be resolved from Kotlin source RemovedFromKotlin: androidx.tracing.perfetto.handshake.protocol.Response#getRequiredVersion(): diff --git a/wear/compose/compose-navigation3/src/androidTest/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategyTest.kt b/wear/compose/compose-navigation3/src/androidTest/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategyTest.kt index 859528e4f957a..3eb8d9e947042 100644 --- a/wear/compose/compose-navigation3/src/androidTest/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategyTest.kt +++ b/wear/compose/compose-navigation3/src/androidTest/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategyTest.kt @@ -84,6 +84,7 @@ import androidx.wear.compose.material3.Text import com.google.common.truth.Truth.assertThat import kotlin.String import kotlinx.coroutines.test.StandardTestDispatcher +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @@ -227,6 +228,27 @@ class SwipeDismissableSceneStrategyTest { assertThat(lifecycleState.currentState).isEqualTo(Lifecycle.State.DESTROYED) } + @Test + @SdkSuppress(minSdkVersion = 36) + fun does_not_intercept_back_pressed_with_single_item_backstack() { + rule.setContentWithBackPressedDispatcher { TestNavDisplay() } + // If NavDisplay is handling back, 'hasEnabledCallbacks' would be true. + // For a single item, it should be false so the system (Activity) can handle it. + assertFalse(backPressedDispatcher.hasEnabledCallbacks()) + } + + @Test + fun does_not_crash_when_swiping_back_with_single_item_backstack() { + rule.setContentWithBackPressedDispatcher { TestNavDisplay() } + + val result = runCatching { + rule.swipeRight() + rule.waitForIdle() + } + + assertTrue("Caught exception while swiping back.", result.isSuccess) + } + @Test fun displays_previous_screen_during_swipe_gesture() { rule.setContentWithBackPressedDispatcher { WithTouchSlop(0f) { TestNavDisplay() } } @@ -511,9 +533,8 @@ class SwipeDismissableSceneStrategyTest { @Test @SdkSuppress(minSdkVersion = 36) fun press_back_during_animation_does_not_reset_animation() { - rule.setContentWithBackPressedDispatcher { TestNavDisplay() } - // Click to move to next destination then swipe back. - rule.onNodeWithText(FIRST_SCREEN).performClick() + val backStack = mutableStateListOf(FIRST_KEY, SECOND_KEY, THIRD_KEY) + rule.setContentWithBackPressedDispatcher { TestNavDisplay(backStack = backStack) } rule.waitForIdle() @@ -527,7 +548,7 @@ class SwipeDismissableSceneStrategyTest { // fast-forward few frames. repeat(3) { rule.mainClock.advanceTimeByFrame() } var previousLeft = - rule.onNodeWithTag(TEST_TAG_SECOND_CONTAINER).fetchSemanticsNode().boundsInWindow.left + rule.onNodeWithTag(TEST_TAG_THIRD_CONTAINER).fetchSemanticsNode().boundsInWindow.left // Press back while animation is running rule.runOnIdle { backPressedDispatcher.onBackPressed() } @@ -538,7 +559,7 @@ class SwipeDismissableSceneStrategyTest { val currentLeft = rule - .onNodeWithTag(TEST_TAG_SECOND_CONTAINER) + .onNodeWithTag(TEST_TAG_THIRD_CONTAINER) .fetchSemanticsNode() .boundsInWindow .left @@ -718,12 +739,25 @@ class SwipeDismissableSceneStrategyTest { } } } + entry(THIRD_KEY) { + Box( + modifier = Modifier.fillMaxSize().testTag(TEST_TAG_THIRD_CONTAINER), + contentAlignment = Alignment.Center, + ) { + ScalingLazyColumn(modifier = Modifier.testTag(THIRD_SCREEN)) { + item { Text(THIRD_SCREEN) } + } + } + } } val FIRST_KEY = "First" val FIRST_SCREEN = "FirstTag" val SECOND_KEY = "Second" val SECOND_SCREEN = "SecondTag" + val THIRD_KEY = "Third" + val THIRD_SCREEN = "ThirdTag" val SCENE_TAG = "SceneModifierTag" val TEST_TAG_SECOND_CONTAINER = "SecondContainerTag" + val TEST_TAG_THIRD_CONTAINER = "ThirdContainerTag" } diff --git a/wear/compose/compose-navigation3/src/main/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategy.kt b/wear/compose/compose-navigation3/src/main/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategy.kt index 51c66370fa903..3073e359d8140 100644 --- a/wear/compose/compose-navigation3/src/main/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategy.kt +++ b/wear/compose/compose-navigation3/src/main/kotlin/androidx/wear/compose/navigation3/SwipeDismissableSceneStrategy.kt @@ -117,7 +117,6 @@ public class SwipeDismissableSceneStrategy( val currentEntry = entries.last() val previousEntries = entries.dropLast(1) val background = previousEntries.lastOrNull() - val backEnabled = isUserSwipeEnabled && background != null return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) { // api 36+, support predictive back @@ -125,7 +124,7 @@ public class SwipeDismissableSceneStrategy( modifier = modifier, currentEntry = currentEntry, previousEntries = previousEntries, - backEnabled = backEnabled, + backEnabled = isUserSwipeEnabled, ) } else { val swipeToDismissBoxState = state.swipeToDismissBoxState @@ -140,7 +139,7 @@ public class SwipeDismissableSceneStrategy( currentBackStack = entries, previousEntries = previousEntries, swipeToDismissBoxState = swipeToDismissBoxState, - backEnabled = backEnabled, + backEnabled = isUserSwipeEnabled && background != null, ) } } diff --git a/wear/tiles/tiles-tooling-preview/api/current.ignore b/wear/tiles/tiles-tooling-preview/api/current.ignore index 7aaac90094310..9a16325187757 100644 --- a/wear/tiles/tiles-tooling-preview/api/current.ignore +++ b/wear/tiles/tiles-tooling-preview/api/current.ignore @@ -15,3 +15,15 @@ RemovedFromKotlin: androidx.wear.tiles.tooling.preview.TilePreviewData#getOnTile Source breaking change: method androidx.wear.tiles.tooling.preview.TilePreviewData.getOnTileResourceRequest() can no longer be resolved from Kotlin source RemovedFromKotlin: androidx.wear.tiles.tooling.preview.TilePreviewData#getPlatformDataValues(): Source breaking change: method androidx.wear.tiles.tooling.preview.TilePreviewData.getPlatformDataValues() can no longer be resolved from Kotlin source + + +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#device: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#device +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#fontScale: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#fontScale +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#group: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#group +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#locale: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#locale +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#name: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#name diff --git a/wear/tiles/tiles-tooling-preview/api/restricted_current.ignore b/wear/tiles/tiles-tooling-preview/api/restricted_current.ignore index 7aaac90094310..9a16325187757 100644 --- a/wear/tiles/tiles-tooling-preview/api/restricted_current.ignore +++ b/wear/tiles/tiles-tooling-preview/api/restricted_current.ignore @@ -15,3 +15,15 @@ RemovedFromKotlin: androidx.wear.tiles.tooling.preview.TilePreviewData#getOnTile Source breaking change: method androidx.wear.tiles.tooling.preview.TilePreviewData.getOnTileResourceRequest() can no longer be resolved from Kotlin source RemovedFromKotlin: androidx.wear.tiles.tooling.preview.TilePreviewData#getPlatformDataValues(): Source breaking change: method androidx.wear.tiles.tooling.preview.TilePreviewData.getPlatformDataValues() can no longer be resolved from Kotlin source + + +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#device: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#device +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#fontScale: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#fontScale +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#group: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#group +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#locale: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#locale +RemovedProperty: androidx.wear.tiles.tooling.preview.Preview.Container#name: + Source breaking change: Removed property androidx.wear.tiles.tooling.preview.Preview.Container#name diff --git a/wear/tiles/tiles/api/current.txt b/wear/tiles/tiles/api/current.txt index c43997feee225..a74f61b6aceb3 100644 --- a/wear/tiles/tiles/api/current.txt +++ b/wear/tiles/tiles/api/current.txt @@ -782,6 +782,8 @@ package androidx.wear.tiles { public abstract class Material3TileService extends androidx.wear.tiles.TileService { ctor public Material3TileService(); + ctor public Material3TileService(optional boolean allowDynamicTheme); + ctor public Material3TileService(optional boolean allowDynamicTheme, optional androidx.wear.protolayout.material3.ColorScheme defaultColorScheme); ctor public Material3TileService(optional boolean allowDynamicTheme, optional androidx.wear.protolayout.material3.ColorScheme defaultColorScheme, optional kotlinx.coroutines.CoroutineScope? serviceScope); ctor @BytecodeOnly public Material3TileService(boolean, androidx.wear.protolayout.material3.ColorScheme!, kotlinx.coroutines.CoroutineScope!, int, kotlin.jvm.internal.DefaultConstructorMarker!); method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected final void onTileEnterEvent(androidx.wear.tiles.EventBuilders.TileEnterEvent requestParams); diff --git a/wear/tiles/tiles/api/restricted_current.txt b/wear/tiles/tiles/api/restricted_current.txt index c43997feee225..a74f61b6aceb3 100644 --- a/wear/tiles/tiles/api/restricted_current.txt +++ b/wear/tiles/tiles/api/restricted_current.txt @@ -782,6 +782,8 @@ package androidx.wear.tiles { public abstract class Material3TileService extends androidx.wear.tiles.TileService { ctor public Material3TileService(); + ctor public Material3TileService(optional boolean allowDynamicTheme); + ctor public Material3TileService(optional boolean allowDynamicTheme, optional androidx.wear.protolayout.material3.ColorScheme defaultColorScheme); ctor public Material3TileService(optional boolean allowDynamicTheme, optional androidx.wear.protolayout.material3.ColorScheme defaultColorScheme, optional kotlinx.coroutines.CoroutineScope? serviceScope); ctor @BytecodeOnly public Material3TileService(boolean, androidx.wear.protolayout.material3.ColorScheme!, kotlinx.coroutines.CoroutineScope!, int, kotlin.jvm.internal.DefaultConstructorMarker!); method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected final void onTileEnterEvent(androidx.wear.tiles.EventBuilders.TileEnterEvent requestParams); diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/Material3TileService.kt b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/Material3TileService.kt index 787a4a276e64e..88aa7ef86a3e0 100644 --- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/Material3TileService.kt +++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/Material3TileService.kt @@ -60,7 +60,9 @@ import kotlinx.coroutines.CoroutineScope * a main thread will be used. * @sample androidx.wear.tiles.snippet_samples.material3TileServiceHelloWorld */ -public abstract class Material3TileService( +public abstract class Material3TileService +@JvmOverloads // For hilt support +public constructor( private val allowDynamicTheme: Boolean = true, private val defaultColorScheme: ColorScheme = ColorScheme(), private val serviceScope: CoroutineScope? = null, diff --git a/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/persistentanchors/PersistentAnchorsActivity.kt b/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/persistentanchors/PersistentAnchorsActivity.kt index a3ab157052ca1..d0d1effd30609 100644 --- a/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/persistentanchors/PersistentAnchorsActivity.kt +++ b/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/persistentanchors/PersistentAnchorsActivity.kt @@ -97,6 +97,7 @@ class PersistentAnchorsActivity : ComponentActivity() { private val movableEntityOffset = Pose(Vector3(0f, 0.75f, -1.3f)) private val uuids = MutableStateFlow>(emptyList()) private var anchorOffset = MutableStateFlow(0f) + private lateinit var arDevice: ArDevice private lateinit var renderViewpoints: List private val panelInViewStatus = MutableStateFlow>>(emptyList()) @@ -112,6 +113,7 @@ class PersistentAnchorsActivity : ComponentActivity() { ), onSessionAvailable = { session -> this.session = session + this.arDevice = ArDevice.getInstance(session) this.renderViewpoints = buildList { RenderViewpoint.left(session)?.let { add(it) } RenderViewpoint.right(session)?.let { add(it) } @@ -134,9 +136,12 @@ class PersistentAnchorsActivity : ComponentActivity() { startPanelInViewStatusUpdates() lifecycleScope.launch { - ArDevice.getInstance(session).state.collect { arDeviceState -> - updatePlaneEntity(arDeviceState) - } + arDevice.state.collect { arDeviceState -> updatePanelEntity(arDeviceState) } + } + + session.scene.activitySpace.addOnSpaceUpdatedListener { + updatePanelEntity(arDevice.state.value) + updatePanelInViewStatusUpdates(renderViewpoints.map { it.state.value }) } }, ) @@ -148,38 +153,42 @@ class PersistentAnchorsActivity : ComponentActivity() { lifecycleScope.launch { combine(cameraStateFlows) { cameraStates -> - val mainPanelEntity = session.scene.mainPanelEntity - val panelPoseInActivitySpace = mainPanelEntity.getPose() - val panelPoseInPerceptionSpace = - session.scene.activitySpace.transformPoseTo( - panelPoseInActivitySpace, - session.scene.perceptionSpace, - ) - val panelSizeInMeters = mainPanelEntity.size - val newStatus = - cameraStates.mapIndexed { index, cameraState -> - val isInView = - isPanelInView( - cameraPoseInPerceptionSpace = cameraState.pose, - cameraFov = cameraState.fieldOfView, - panelPoseInPerceptionSpace = panelPoseInPerceptionSpace, - panelSizeInMeters = panelSizeInMeters, - ) - val cameraName = - when { - renderViewpoints.size == 1 -> "CameraView" - index == 0 -> "Left Eye CameraView" - index == 1 -> "Right Eye CameraView" - else -> "CameraView ${index + 1}" - } - cameraName to isInView - } - panelInViewStatus.value = newStatus + updatePanelInViewStatusUpdates(cameraStates.asList()) } .collect {} } } + private fun updatePanelInViewStatusUpdates(cameraStates: List) { + val mainPanelEntity = session.scene.mainPanelEntity + val panelPoseInActivitySpace = mainPanelEntity.getPose() + val panelPoseInPerceptionSpace = + session.scene.activitySpace.transformPoseTo( + panelPoseInActivitySpace, + session.scene.perceptionSpace, + ) + val panelSizeInMeters = mainPanelEntity.size + val newStatus = + cameraStates.mapIndexed { index, cameraState -> + val isInView = + isPanelInView( + cameraPoseInPerceptionSpace = cameraState.pose, + cameraFov = cameraState.fieldOfView, + panelPoseInPerceptionSpace = panelPoseInPerceptionSpace, + panelSizeInMeters = panelSizeInMeters, + ) + val cameraName = + when { + renderViewpoints.size == 1 -> "CameraView" + index == 0 -> "Left Eye CameraView" + index == 1 -> "Right Eye CameraView" + else -> "CameraView ${index + 1}" + } + cameraName to isInView + } + panelInViewStatus.value = newStatus + } + private fun createTargetPanel() { val composeView = ComposeView(this) composeView.setContent { TargetPanel() } @@ -195,7 +204,7 @@ class PersistentAnchorsActivity : ComponentActivity() { configureComposeView(composeView, this) } - private fun updatePlaneEntity(arDeviceState: ArDevice.State) { + private fun updatePanelEntity(arDeviceState: ArDevice.State) { arDeviceState.devicePose.let { headPose -> val headScenePose = session.scene.perceptionSpace.getScenePoseFromPerceptionPose(headPose) diff --git a/xr/compose/compose/api/restricted_current.txt b/xr/compose/compose/api/restricted_current.txt index d4adda068cf2e..237f9dca12e2e 100644 --- a/xr/compose/compose/api/restricted_current.txt +++ b/xr/compose/compose/api/restricted_current.txt @@ -331,12 +331,6 @@ package androidx.xr.compose.subspace { field public static final androidx.xr.compose.subspace.SpatialCurvedRowDefaults INSTANCE; } - @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SpatialExternalSurfaceDefaults { - method @BytecodeOnly public float getSphereRadius-D9Ej5fM(); - property public androidx.compose.ui.unit.Dp sphereRadius; - field public static final androidx.xr.compose.subspace.SpatialExternalSurfaceDefaults INSTANCE; - } - public final class SpatialExternalSurfaceKt { method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialExternalSurface(androidx.xr.compose.subspace.StereoMode stereoMode, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialFeatheringEffect featheringEffect, optional androidx.xr.compose.subspace.SurfaceProtection surfaceProtection, optional androidx.xr.compose.subspace.DragPolicy? dragPolicy, optional androidx.xr.compose.subspace.ResizePolicy? resizePolicy, optional androidx.xr.compose.subspace.layout.InteractionPolicy? interactionPolicy, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @Deprecated @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialExternalSurface-3SPOwJ0(androidx.xr.scenecore.SurfaceEntity.StereoMode!, androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.compose.subspace.layout.SpatialFeatheringEffect!, androidx.xr.scenecore.SurfaceEntity.SurfaceProtection!, androidx.xr.compose.subspace.DragPolicy!, androidx.xr.compose.subspace.ResizePolicy!, androidx.xr.compose.subspace.layout.InteractionPolicy!, kotlin.jvm.functions.Function3!, androidx.compose.runtime.Composer!, int, int); diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt index e60b5dc71e45d..be004c4c453ea 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt @@ -17,7 +17,6 @@ package androidx.xr.compose.subspace import android.view.Surface -import androidx.annotation.RestrictTo import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -46,11 +45,10 @@ import androidx.xr.scenecore.SurfaceEntity import androidx.xr.scenecore.scene /** Contains default values used by SpatialExternalSurface. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -public object SpatialExternalSurfaceDefaults { +internal object SpatialExternalSurfaceDefaults { /** Default radius for spheres. */ - public val sphereRadius: Dp = Meter(15f).toDp() + internal val sphereRadius: Dp = Meter(15f).toDp() } /** diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/common/MediaHelper.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/common/MediaHelper.kt index a15c18604707d..5591b7ff34c88 100644 --- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/common/MediaHelper.kt +++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/common/MediaHelper.kt @@ -16,8 +16,10 @@ package androidx.xr.compose.testapp.common +import android.media.MediaCodecInfo import android.media.MediaCodecList import androidx.annotation.OptIn +import androidx.media3.common.C import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @@ -50,3 +52,50 @@ fun isMvHevcSupported(): Boolean { // No decoder found that supports the MV-HEVC profile. return false } + +@OptIn(UnstableApi::class) // For MimeTypes like VP9 +/** + * Checks if the device supports Widevine DRM and has a secure decoder. + * + * This prevents crashes on devices that might report partial DRM support but lack the secure + * rendering path required for SurfaceEntity.SurfaceProtection.PROTECTED. + */ +public fun isDrmSupported(): Boolean { + // 1. Check if the Widevine scheme is supported by the device. + if (!android.media.MediaDrm.isCryptoSchemeSupported(C.WIDEVINE_UUID)) { + return false + } + + // 2. Check if a secure decoder is available. + // For example, the emulator might support the scheme (L3) but fail to create a protected + // surface if no secure decoder is present. + val mimeTypesToCheck = + listOf( + MimeTypes.VIDEO_H264, + MimeTypes.VIDEO_H265, + MimeTypes.VIDEO_VP9, + MimeTypes.VIDEO_AV1, + MimeTypes.VIDEO_MP4, + ) + val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS) + + for (mimeType in mimeTypesToCheck) { + for (info in mediaCodecList.codecInfos) { + if (info.isEncoder) continue + try { + val caps = info.getCapabilitiesForType(mimeType) + if ( + caps != null && + caps.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback + ) + ) { + return true + } + } catch (e: IllegalArgumentException) { + // MIME type not supported by this codec, continue searching. + } + } + } + return false +} diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/fragments/VideoPlayerFragment.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/fragments/VideoPlayerFragment.kt index d05a1da0db757..4872e84bcaabe 100644 --- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/fragments/VideoPlayerFragment.kt +++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/fragments/VideoPlayerFragment.kt @@ -22,6 +22,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -30,6 +32,8 @@ 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.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp @@ -56,6 +60,7 @@ import androidx.xr.compose.subspace.layout.fillMaxSize import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.offset import androidx.xr.compose.subspace.layout.width +import androidx.xr.compose.testapp.common.isDrmSupported /** A Fragment using spatial UI. */ class VideoPlayerFragment : Fragment() { @@ -92,6 +97,7 @@ class VideoPlayerFragment : Fragment() { private fun VideoInSpatialExternalSurface(stereoMode: StereoMode) { var videoWidth by remember { mutableStateOf(600.dp) } var videoHeight by remember { mutableStateOf(600.dp) } + val isDrmSupported = remember { isDrmSupported() } SpatialExternalSurface( modifier = SubspaceModifier.width( @@ -156,8 +162,16 @@ class VideoPlayerFragment : Fragment() { } Orbiter(position = ContentEdge.Bottom, offset = 48.dp) { - Button(onClick = { useDrmState.value = !useDrmState.value }) { - Text(text = if (useDrmState.value) "Use non-drm video" else "Use drm video") + Row(verticalAlignment = Alignment.CenterVertically) { + Button(onClick = { useDrmState.value = !useDrmState.value }) { + Text(text = if (useDrmState.value) "Use non-drm video" else "Use drm video") + } + if (!isDrmSupported) { + Text( + text = "DRM is not supported on this device", + modifier = Modifier.padding(start = 16.dp), + ) + } } } } diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt index 5b98569e13ff9..7a567b2d3f0c8 100644 --- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt +++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt @@ -82,7 +82,6 @@ import androidx.xr.compose.subspace.SpatialColumn import androidx.xr.compose.subspace.SpatialExternalSurface import androidx.xr.compose.subspace.SpatialExternalSurface180Hemisphere import androidx.xr.compose.subspace.SpatialExternalSurface360Sphere -import androidx.xr.compose.subspace.SpatialExternalSurfaceDefaults import androidx.xr.compose.subspace.SpatialMainPanel import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.SpatialSpacer @@ -99,8 +98,10 @@ import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.offset import androidx.xr.compose.subspace.layout.onPointSourceParamsAvailable import androidx.xr.compose.subspace.layout.width +import androidx.xr.compose.testapp.common.isDrmSupported import androidx.xr.compose.testapp.common.isMvHevcSupported import androidx.xr.compose.testapp.ui.components.CommonTestScaffold +import androidx.xr.compose.unit.Meter import androidx.xr.runtime.Config import androidx.xr.runtime.Session import androidx.xr.runtime.SessionCreateSuccess @@ -238,7 +239,7 @@ class SpatialComposeVideoPlayer : ComponentActivity() { val animatedOffset = remember { Animatable(initialValue = -1000f) } LaunchedEffect(Unit) { animatedRadius.animateTo( - targetValue = SpatialExternalSurfaceDefaults.sphereRadius.value, + targetValue = Meter(15f).toDp().value, animationSpec = tween(durationMillis = 2000, easing = FastOutLinearInEasing), ) } @@ -451,6 +452,7 @@ class SpatialComposeVideoPlayer : ComponentActivity() { VideoMenuState.VIDEO_IN_SPATIAL_EXTERNAL_SURFACE -> { val scrollState = rememberScrollState() + val isDrmSupported = remember { isDrmSupported() } Column( modifier = Modifier.verticalScroll(scrollState).padding(24.dp) @@ -474,13 +476,21 @@ class SpatialComposeVideoPlayer : ComponentActivity() { } } - Button( - onClick = { useDrmState.value = !useDrmState.value } - ) { - if (useDrmState.value) { - Text("Use picker video uri") - } else { - Text("Use drm video uri") + Row(verticalAlignment = Alignment.CenterVertically) { + Button( + onClick = { useDrmState.value = !useDrmState.value } + ) { + if (useDrmState.value) { + Text("Use picker video uri") + } else { + Text("Use drm video uri") + } + } + if (!isDrmSupported) { + Text( + "Drm is not supported on this device.", + modifier = Modifier.padding(start = 16.dp), + ) } } @@ -751,6 +761,7 @@ class SpatialComposeVideoPlayer : ComponentActivity() { SpatialBox(modifier = SubspaceModifier.fillMaxSize()) { val interactionSource = remember { MutableInteractionSource() } val isPanelHovered by interactionSource.collectIsHoveredAsState() + val isDrmSupported = remember { isDrmSupported() } // Having an alpha helps reduce depth perception issues with stereo video. SpatialPanel( @@ -780,8 +791,16 @@ class SpatialComposeVideoPlayer : ComponentActivity() { Text("Play/Pause") } Spacer(modifier = Modifier.weight(1f)) - Button(onClick = { useDrmState.value = !useDrmState.value }) { - Text(text = if (useDrmState.value) "Use non-drm video" else "Use drm video") + Column { + Button(onClick = { useDrmState.value = !useDrmState.value }) { + Text( + text = + if (useDrmState.value) "Use non-drm video" else "Use drm video" + ) + } + if (!isDrmSupported) { + Text("Drm is not supported on this device.", color = Color.White) + } } Spacer(modifier = Modifier.weight(1f)) Button(onClick = { videoPlayingState.value = false }) { Text("End Video") } @@ -1022,6 +1041,7 @@ class SpatialComposeVideoPlayer : ComponentActivity() { fun SurfaceEntityUI(session: Session) { val movableComponentMP = remember { mutableStateOf(null) } val videoPaused = remember { mutableStateOf(false) } + val isDrmSupported = remember { isDrmSupported() } movableComponentMP.value = MovableComponent.createSystemMovable(session) @Suppress("UNUSED_VARIABLE") val unused = session.scene.mainPanelEntity.addComponent(movableComponentMP.value!!) @@ -1037,11 +1057,19 @@ class SpatialComposeVideoPlayer : ComponentActivity() { Button(onClick = { menuState.value = VideoMenuState.HOME }) { Text("Main Menu") } - Button(onClick = { useDrmState.value = !useDrmState.value }) { - if (useDrmState.value) { - Text("Use picker video uri") - } else { - Text("Use drm video uri") + Row(verticalAlignment = Alignment.CenterVertically) { + Button(onClick = { useDrmState.value = !useDrmState.value }) { + if (useDrmState.value) { + Text("Use picker video uri") + } else { + Text("Use drm video uri") + } + } + if (!isDrmSupported) { + Text( + "Drm is not supported on this device.", + modifier = Modifier.padding(start = 16.dp), + ) } } LaunchSurfaceEntityButton() diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt index 8d2bd2fdb27da..694091b40bc92 100644 --- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt +++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt @@ -80,6 +80,7 @@ import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.size import androidx.xr.compose.testapp.R +import androidx.xr.compose.testapp.common.isDrmSupported import androidx.xr.compose.testapp.common.isMvHevcSupported import androidx.xr.compose.testapp.ui.components.CommonTestScaffold import androidx.xr.compose.unit.DpVolumeSize @@ -453,6 +454,9 @@ class VideoPlayerActivity : ComponentActivity() { if (!isMvHevcSupported()) { ApiText(text = "MV-HEVC is not supported on this device") } + if (!isDrmSupported()) { + ApiText(text = "DRM is not supported on this device") + } } } } diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/MediaHelper.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/MediaHelper.kt index 5daf3b4d76881..d225fef1175d9 100644 --- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/MediaHelper.kt +++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/MediaHelper.kt @@ -16,8 +16,10 @@ package androidx.xr.scenecore.testapp.common +import android.media.MediaCodecInfo import android.media.MediaCodecList import androidx.annotation.OptIn +import androidx.media3.common.C import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @@ -50,3 +52,50 @@ fun isMvHevcSupported(): Boolean { // No decoder found that supports the MV-HEVC profile. return false } + +@OptIn(UnstableApi::class) // For MimeTypes like VP9 +/** + * Checks if the device supports Widevine DRM and has a secure decoder. + * + * This prevents crashes on devices that might report partial DRM support but lack the secure + * rendering path required for SurfaceEntity.SurfaceProtection.PROTECTED. + */ +public fun isDrmSupported(): Boolean { + // 1. Check if the Widevine scheme is supported by the device. + if (!android.media.MediaDrm.isCryptoSchemeSupported(C.WIDEVINE_UUID)) { + return false + } + + // 2. Check if a secure decoder is available. + // For example, the emulator might support the scheme (L3) but fail to create a protected + // surface if no secure decoder is present. + val mimeTypesToCheck = + listOf( + MimeTypes.VIDEO_H264, + MimeTypes.VIDEO_H265, + MimeTypes.VIDEO_VP9, + MimeTypes.VIDEO_AV1, + MimeTypes.VIDEO_MP4, + ) + val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS) + + for (mimeType in mimeTypesToCheck) { + for (info in mediaCodecList.codecInfos) { + if (info.isEncoder) continue + try { + val caps = info.getCapabilitiesForType(mimeType) + if ( + caps != null && + caps.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback + ) + ) { + return true + } + } catch (e: IllegalArgumentException) { + // MIME type not supported by this codec, continue searching. + } + } + } + return false +} diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/surfaceplayback/SurfaceEntityPlaybackActivity.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/surfaceplayback/SurfaceEntityPlaybackActivity.kt index db0f256a568ae..f0f62a4c53d32 100644 --- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/surfaceplayback/SurfaceEntityPlaybackActivity.kt +++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/surfaceplayback/SurfaceEntityPlaybackActivity.kt @@ -94,6 +94,7 @@ import androidx.xr.scenecore.PanelEntity import androidx.xr.scenecore.SurfaceEntity import androidx.xr.scenecore.Texture import androidx.xr.scenecore.scene +import androidx.xr.scenecore.testapp.common.isDrmSupported import androidx.xr.scenecore.testapp.common.isMvHevcSupported import java.io.File import java.nio.ByteBuffer @@ -992,7 +993,7 @@ class SurfaceEntityPlaybackActivity : ComponentActivity() { shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)), buttonText = "[DRM] Play MVHEVC Left Primary", buttonColor = VideoButtonColors.DRM, - enabled = enabled, + enabled = isDrmSupported(), loop = loop, protected = true, ) @@ -1144,6 +1145,13 @@ class SurfaceEntityPlaybackActivity : ComponentActivity() { activity, isMvHevcSupported(), ) + if (!isDrmSupported()) { + Text( + text = "DRM is not supported on this device.", + fontSize = 18.sp, + color = Color.Red, + ) + } HDRVideoPlaybackButton(session, arDevice, activity) SingleViewRotated270HalfWidthButton(session, arDevice, activity) MVHEVCLeftPrimaryRotated180Button( diff --git a/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt b/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt index f902b471bb21c..620e332a5eeb1 100644 --- a/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt +++ b/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt @@ -21,6 +21,9 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.media.MediaCodecInfo +import android.media.MediaCodecList +import android.media.MediaDrm import android.os.Bundle import android.os.Environment import android.util.Log @@ -29,6 +32,7 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -58,9 +62,11 @@ import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.DrmConfiguration +import androidx.media3.common.MimeTypes import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.VideoSize +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner @@ -507,6 +513,7 @@ class VideoPlayerDrmTestActivity : ComponentActivity() { @Composable fun DrmVideoButton(session: Session, activity: Activity) { + val isDrmSupported: Boolean = remember { isDrmSupported() } val videoUri = Environment.getExternalStorageDirectory().getPath() + "/Download/sdr_singleview_protected.mp4" @@ -527,9 +534,9 @@ class VideoPlayerDrmTestActivity : ComponentActivity() { videoUri = videoUri, buttonText = if (!videoPlaying) { - "Play Drm Video" + if (!isDrmSupported) "Drm Not Supported (Play anyway)" else "Play Drm Video" } else { - "Queue Drm Video" + if (!isDrmSupported) "Drm Not Supported (Queue anyway)" else "Queue Drm Video" }, onClick = { if (!videoPlaying) { @@ -636,3 +643,51 @@ class VideoPlayerDrmTestActivity : ComponentActivity() { } } } + +// TODO: b/473040355 - deduplicate this logic and the other MediaHelper.kt files. +@OptIn(UnstableApi::class) // For MimeTypes like VP9 +/** + * Checks if the device supports Widevine DRM and has a secure decoder. + * + * This prevents crashes on devices that might report partial DRM support but lack the secure + * rendering path required for SurfaceEntity.SurfaceProtection.PROTECTED. + */ +private fun isDrmSupported(): Boolean { + // 1. Check if the Widevine scheme is supported by the device. + if (!android.media.MediaDrm.isCryptoSchemeSupported(C.WIDEVINE_UUID)) { + return false + } + + // 2. Check if a secure decoder is available. + // For example, the emulator might support the scheme (L3) but fail to create a protected + // surface if no secure decoder is present. + val mimeTypesToCheck = + listOf( + MimeTypes.VIDEO_H264, + MimeTypes.VIDEO_H265, + MimeTypes.VIDEO_VP9, + MimeTypes.VIDEO_AV1, + MimeTypes.VIDEO_MP4, + ) + val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS) + + for (mimeType in mimeTypesToCheck) { + for (info in mediaCodecList.codecInfos) { + if (info.isEncoder) continue + try { + val caps = info.getCapabilitiesForType(mimeType) + if ( + caps != null && + caps.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback + ) + ) { + return true + } + } catch (e: IllegalArgumentException) { + // MIME type not supported by this codec, continue searching. + } + } + } + return false +}