Merge "Replacing NestedScroll Sources." into androidx-main
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 90c2c5d..12c29ee 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -25,6 +25,7 @@
 import android.os.HandlerThread
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
 import androidx.camera.camera2.pipe.config.CameraGraphConfigModule
 import androidx.camera.camera2.pipe.config.CameraPipeComponent
 import androidx.camera.camera2.pipe.config.CameraPipeConfigModule
@@ -116,6 +117,16 @@
     }
 
     /**
+     * This gets and sets the global [AudioRestrictionMode] tracked by [AudioRestrictionController].
+     */
+    var globalAudioRestrictionMode: AudioRestrictionMode
+        get(): AudioRestrictionMode =
+            component.cameraAudioRestrictionController().globalAudioRestrictionMode
+        set(value: AudioRestrictionMode) {
+            component.cameraAudioRestrictionController().globalAudioRestrictionMode = value
+        }
+
+    /**
      * Application level configuration for [CameraPipe]. Nullable values are optional and reasonable
      * defaults will be provided if values are not specified.
      */
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
index 0d5c122..98dc196 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
@@ -32,6 +32,8 @@
 import androidx.camera.camera2.pipe.CameraPipe.CameraMetadataConfig
 import androidx.camera.camera2.pipe.CameraSurfaceManager
 import androidx.camera.camera2.pipe.compat.AndroidDevicePolicyManagerWrapper
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionControllerImpl
 import androidx.camera.camera2.pipe.compat.DevicePolicyManagerWrapper
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.SystemTimeSource
@@ -71,6 +73,7 @@
     fun cameraGraphComponentBuilder(): CameraGraphComponent.Builder
     fun cameras(): CameraDevices
     fun cameraSurfaceManager(): CameraSurfaceManager
+    fun cameraAudioRestrictionController(): AudioRestrictionController
 }
 
 @Module(includes = [ThreadConfigModule::class], subcomponents = [CameraGraphComponent::class])
@@ -165,5 +168,9 @@
         @Singleton
         @Provides
         fun provideCameraSurfaceManager() = CameraSurfaceManager()
+
+        @Singleton
+        @Provides
+        fun provideAudioRestrictionController() = AudioRestrictionControllerImpl()
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
index c61b2b4..e64e886 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
@@ -22,7 +22,7 @@
 
 import android.annotation.SuppressLint;
 import android.content.pm.PackageManager;
-import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CameraCharacteristics;
 import android.os.Build;
 import android.os.Bundle;
 import android.view.MotionEvent;
@@ -38,8 +38,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraInfo;
@@ -50,6 +50,7 @@
 import androidx.camera.core.MeteringPoint;
 import androidx.camera.core.Preview;
 import androidx.camera.core.UseCaseGroup;
+import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration;
 import androidx.camera.lifecycle.ProcessCameraProvider;
 import androidx.camera.view.PreviewView;
 import androidx.core.app.ActivityCompat;
@@ -90,8 +91,8 @@
     private boolean mIsConcurrentModeOn = false;
     private boolean mIsLayoutPiP = true;
     private boolean mIsFrontPrimary = true;
-
     private boolean mIsDualSelfieEnabled = false;
+    private boolean mIsCameraPipeEnabled = false;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -129,6 +130,8 @@
                 mIsLayoutPiP = true;
                 bindPreviewForSingle(mCameraProvider);
                 mIsConcurrentModeOn = false;
+                mIsDualSelfieEnabled = false;
+                mDualSelfieButton.setChecked(false);
             } else {
                 mIsLayoutPiP = true;
                 bindPreviewForPiP(mCameraProvider);
@@ -170,7 +173,13 @@
         }
     }
 
+    @SuppressLint("NullAnnotationGroup")
+    @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     private void startCamera() {
+        if (mIsCameraPipeEnabled) {
+            ProcessCameraProvider.configureInstance(CameraPipeConfig.defaultConfig());
+        }
+
         final ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
                 ProcessCameraProvider.getInstance(this);
         cameraProviderFuture.addListener(() -> {
@@ -265,7 +274,8 @@
     }
 
     @SuppressLint("NullAnnotationGroup")
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = {ExperimentalCamera2Interop.class,
+            androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class})
     private void bindToLifecycleForConcurrentCamera(
             @NonNull ProcessCameraProvider cameraProvider,
             @NonNull LifecycleOwner lifecycleOwner,
@@ -287,13 +297,18 @@
             String innerPhysicalCameraId = null;
             String outerPhysicalCameraId = null;
             for (CameraInfo info : cameraInfoPrimary.getPhysicalCameraInfos()) {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-                    if (Camera2CameraInfo.from(info).getCameraCharacteristic(LENS_POSE_REFERENCE)
-                            == CameraMetadata.LENS_POSE_REFERENCE_PRIMARY_CAMERA) {
-                        innerPhysicalCameraId = Camera2CameraInfo.from(info).getCameraId();
-                    } else {
-                        outerPhysicalCameraId = Camera2CameraInfo.from(info).getCameraId();
-                    }
+                if (isPrimaryCamera(info)) {
+                    innerPhysicalCameraId = mIsCameraPipeEnabled
+                            ? androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+                                    .from(info).getCameraId()
+                            : androidx.camera.camera2.interop.Camera2CameraInfo
+                                    .from(info).getCameraId();
+                } else {
+                    outerPhysicalCameraId = mIsCameraPipeEnabled
+                            ? androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+                                    .from(info).getCameraId()
+                            : androidx.camera.camera2.interop.Camera2CameraInfo
+                                    .from(info).getCameraId();
                 }
             }
 
@@ -379,6 +394,24 @@
         }
     }
 
+    @SuppressLint("NullAnnotationGroup")
+    @OptIn(markerClass = { ExperimentalCamera2Interop.class,
+            androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class })
+    private boolean isPrimaryCamera(@NonNull CameraInfo info) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+            return true;
+        }
+        if (mIsCameraPipeEnabled) {
+            return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(info)
+                    .getCameraCharacteristic(LENS_POSE_REFERENCE)
+                    == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA;
+        } else {
+            return androidx.camera.camera2.interop.Camera2CameraInfo.from(info)
+                    .getCameraCharacteristic(LENS_POSE_REFERENCE)
+                    == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA;
+        }
+    }
+
     private void setupZoomAndTapToFocus(Camera camera, PreviewView previewView) {
         ScaleGestureDetector scaleDetector = new ScaleGestureDetector(this,
                 new ScaleGestureDetector.SimpleOnScaleGestureListener() {
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index d44514f..d359734 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -435,7 +435,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
-    method public float calculateScrollDistance(float offset, float size, float containerSize);
+    method public default float calculateScrollDistance(float offset, float size, float containerSize);
     method public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> getScrollAnimationSpec();
     property public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> scrollAnimationSpec;
     field public static final androidx.compose.foundation.gestures.BringIntoViewSpec.Companion Companion;
@@ -446,6 +446,11 @@
     property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> DefaultScrollAnimationSpec;
   }
 
+  public final class BringIntoViewSpec_androidKt {
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> getLocalBringIntoViewSpec();
+    property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> LocalBringIntoViewSpec;
+  }
+
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface Drag2DScope {
     method public void dragBy(long pixels);
   }
@@ -544,7 +549,7 @@
   }
 
   public final class ScrollableDefaults {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
+    method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
     method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
@@ -552,7 +557,7 @@
   }
 
   public final class ScrollableKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec? bringIntoViewSpec);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 01da236..de43720 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -437,7 +437,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
-    method public float calculateScrollDistance(float offset, float size, float containerSize);
+    method public default float calculateScrollDistance(float offset, float size, float containerSize);
     method public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> getScrollAnimationSpec();
     property public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> scrollAnimationSpec;
     field public static final androidx.compose.foundation.gestures.BringIntoViewSpec.Companion Companion;
@@ -448,6 +448,11 @@
     property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> DefaultScrollAnimationSpec;
   }
 
+  public final class BringIntoViewSpec_androidKt {
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> getLocalBringIntoViewSpec();
+    property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> LocalBringIntoViewSpec;
+  }
+
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface Drag2DScope {
     method public void dragBy(long pixels);
   }
@@ -546,7 +551,7 @@
   }
 
   public final class ScrollableDefaults {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
+    method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
     method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
@@ -554,7 +559,7 @@
   }
 
   public final class ScrollableKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec? bringIntoViewSpec);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 2c7ae2c..245b962 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -18,6 +18,7 @@
 package androidx.compose.foundation.demos
 
 import android.annotation.SuppressLint
+import android.content.res.Configuration
 import androidx.compose.animation.core.AnimationConstants
 import androidx.compose.animation.core.AnimationState
 import androidx.compose.animation.core.animateTo
@@ -27,6 +28,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.FlingBehavior
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.animateScrollBy
@@ -46,8 +48,10 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyListScope
@@ -90,7 +94,10 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.graphics.Color.Companion.White
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.tooling.preview.Preview
@@ -130,6 +137,7 @@
     ComposableDemo("Grid drag and drop") { LazyGridDragAndDropDemo() },
     ComposableDemo("Staggered grid") { LazyStaggeredGridDemo() },
     ComposableDemo("Animate item placement") { AnimateItemPlacementDemo() },
+    ComposableDemo("Focus Scrolling") { BringIntoViewDemo() },
     PagingDemos
 )
 
@@ -349,7 +357,8 @@
             Spacer(
                 Modifier
                     .fillParentMaxSize()
-                    .background(it))
+                    .background(it)
+            )
         }
     }
 }
@@ -555,6 +564,7 @@
                 }
             }
         }
+
         val lazyContent: LazyListScope.() -> Unit = {
             items(count) {
                 item1(it)
@@ -1037,9 +1047,11 @@
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun AnimateItemPlacementDemo() {
-    val items = remember { mutableStateListOf<Int>().apply {
-        repeat(20) { add(it) }
-    } }
+    val items = remember {
+        mutableStateListOf<Int>().apply {
+            repeat(20) { add(it) }
+        }
+    }
     val selectedIndexes = remember { mutableStateMapOf<Int, Boolean>() }
     var reverse by remember { mutableStateOf(false) }
     Column {
@@ -1068,7 +1080,8 @@
         LazyColumn(
             Modifier
                 .fillMaxWidth()
-                .weight(1f), reverseLayout = reverse) {
+                .weight(1f), reverseLayout = reverse
+        ) {
             items(items, key = { it }) { item ->
                 val selected = selectedIndexes.getOrDefault(item, false)
                 val modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
@@ -1089,7 +1102,8 @@
                     Spacer(
                         Modifier
                             .width(16.dp)
-                            .height(height))
+                            .height(height)
+                    )
                     Text("Item $item")
                 }
             }
@@ -1105,3 +1119,31 @@
                 })
     }
 }
+
+@Preview(uiMode = Configuration.UI_MODE_TYPE_TELEVISION)
+@Composable
+private fun BringIntoViewDemo() {
+    LazyRow(
+        modifier = Modifier
+            .fillMaxWidth()
+            .wrapContentHeight()
+    ) {
+        items(100) {
+            var color by remember { mutableStateOf(Color.White) }
+            Box(
+                modifier = Modifier
+                    .size(100.dp)
+                    .padding(4.dp)
+                    .background(Color.Gray)
+                    .onFocusChanged {
+                        color = if (it.isFocused) Red else White
+                    }
+                    .border(5.dp, color)
+                    .focusable(),
+                contentAlignment = Alignment.Center
+            ) {
+                Text(text = it.toString())
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
index 1645cb76..ae7c7ec 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
@@ -17,15 +17,26 @@
 package androidx.compose.foundation.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.BringIntoViewSpec
+import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.rememberScrollableState
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.Icon
@@ -34,15 +45,20 @@
 import androidx.compose.material.icons.filled.KeyboardArrowDown
 import androidx.compose.material.icons.filled.KeyboardArrowUp
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import kotlin.math.abs
 import kotlin.math.roundToInt
 
 @Sampled
@@ -113,3 +129,68 @@
         )
     }
 }
+
+@ExperimentalFoundationApi
+@Sampled
+@Composable
+fun FocusScrollingInLazyRowSample() {
+    // a bring into view spec that pivots around the center of the scrollable container
+    val customBringIntoViewSpec = object : BringIntoViewSpec {
+        val customAnimationSpec = tween<Float>(easing = LinearEasing)
+        override val scrollAnimationSpec: AnimationSpec<Float>
+            get() = customAnimationSpec
+
+        override fun calculateScrollDistance(
+            offset: Float,
+            size: Float,
+            containerSize: Float
+        ): Float {
+            val trailingEdgeOfItemRequestingFocus = offset + size
+
+            val sizeOfItemRequestingFocus =
+                abs(trailingEdgeOfItemRequestingFocus - offset)
+            val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
+            val initialTargetForLeadingEdge =
+                containerSize / 2f - (sizeOfItemRequestingFocus / 2f)
+            val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
+
+            val targetForLeadingEdge =
+                if (childSmallerThanParent &&
+                    spaceAvailableToShowItem < sizeOfItemRequestingFocus
+                ) {
+                    containerSize - sizeOfItemRequestingFocus
+                } else {
+                    initialTargetForLeadingEdge
+                }
+
+            return offset - targetForLeadingEdge
+        }
+    }
+
+    // LocalBringIntoViewSpec will apply to all scrollables in the hierarchy.
+    CompositionLocalProvider(LocalBringIntoViewSpec provides customBringIntoViewSpec) {
+        LazyRow(
+            modifier = Modifier
+                .fillMaxWidth()
+                .wrapContentHeight()
+        ) {
+            items(100) {
+                var color by remember { mutableStateOf(Color.White) }
+                Box(
+                    modifier = Modifier
+                        .size(100.dp)
+                        .padding(4.dp)
+                        .background(Color.Gray)
+                        .onFocusChanged {
+                            color = if (it.isFocused) Color.Red else Color.White
+                        }
+                        .border(5.dp, color)
+                        .focusable(),
+                    contentAlignment = Alignment.Center
+                ) {
+                    Text(text = it.toString())
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt
new file mode 100644
index 0000000..482a642
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.BringIntoViewSpec.Companion.DefaultBringIntoViewSpec
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.ui.platform.LocalContext
+import kotlin.math.abs
+
+/**
+ * A composition local to customize the focus scrolling behavior used by some scrollable containers.
+ * [LocalBringIntoViewSpec] has a platform defined behavior. If the App is running on a TV device,
+ * the scroll behavior will pivot around 30% of the container size. For other platforms, the scroll
+ * behavior will move the least to bring the requested region into view.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
+    compositionLocalWithComputedDefaultOf {
+        val hasTvFeature =
+            LocalContext.currentValue.packageManager.hasSystemFeature(FEATURE_LEANBACK)
+        if (!hasTvFeature) {
+            DefaultBringIntoViewSpec
+        } else {
+            PivotBringIntoViewSpec
+        }
+    }
+
+@OptIn(ExperimentalFoundationApi::class)
+internal val PivotBringIntoViewSpec = object : BringIntoViewSpec {
+    val parentFraction = 0.3f
+    val childFraction = 0f
+    override val scrollAnimationSpec: AnimationSpec<Float> = tween<Float>(
+        durationMillis = 125,
+        easing = CubicBezierEasing(0.25f, 0.1f, .25f, 1f)
+    )
+
+    override fun calculateScrollDistance(
+        offset: Float,
+        size: Float,
+        containerSize: Float
+    ): Float {
+        val leadingEdgeOfItemRequestingFocus = offset
+        val trailingEdgeOfItemRequestingFocus = offset + size
+
+        val sizeOfItemRequestingFocus =
+            abs(trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus)
+        val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
+        val initialTargetForLeadingEdge =
+            parentFraction * containerSize -
+                (childFraction * sizeOfItemRequestingFocus)
+        val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
+
+        val targetForLeadingEdge =
+            if (childSmallerThanParent &&
+                spaceAvailableToShowItem < sizeOfItemRequestingFocus
+            ) {
+                containerSize - sizeOfItemRequestingFocus
+            } else {
+                initialTargetForLeadingEdge
+            }
+
+        return leadingEdgeOfItemRequestingFocus - targetForLeadingEdge
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
index 0504ecc..244dc7a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
@@ -37,9 +37,10 @@
     reverseScrolling: Boolean,
     flingBehavior: FlingBehavior?,
     interactionSource: MutableInteractionSource?,
-    bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+    bringIntoViewSpec: BringIntoViewSpec? = null
 ): Modifier {
     val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
     return clipScrollableContainer(orientation)
         .overscroll(overscrollEffect)
         .scrollable(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt
new file mode 100644
index 0000000..f030e6a
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import kotlin.math.abs
+
+/**
+ * A composition local to customize the focus scrolling behavior used by some scrollable containers.
+ * [LocalBringIntoViewSpec] has a platform defined default behavior.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+expect val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec>
+
+/**
+ * The configuration of how a scrollable reacts to bring into view requests.
+ *
+ * Note: API shape and naming are still being refined, therefore API is marked as experimental.
+ *
+ * Check the following sample for a use case usage of this API:
+ * @sample androidx.compose.foundation.samples.FocusScrollingInLazyRowSample
+ */
+@ExperimentalFoundationApi
+@Stable
+interface BringIntoViewSpec {
+
+    /**
+     * An Animation Spec to be used as the animation to run to fulfill the BringIntoView requests.
+     */
+    val scrollAnimationSpec: AnimationSpec<Float> get() = DefaultScrollAnimationSpec
+
+    /**
+     * Calculate the offset needed to bring one of the scrollable container's child into view.
+     * This will be called for every frame of the scrolling animation. This means that, as the
+     * animation progresses, the offset will naturally change to fulfill the scroll request.
+     *
+     * All distances below are represented in pixels.
+     * @param offset from the side closest to the start of the container.
+     * @param size is the child size.
+     * @param containerSize Is the main axis size of the scrollable container.
+     *
+     * @return The necessary amount to scroll to satisfy the bring into view request.
+     * Returning zero from here means that the request was satisfied and the scrolling animation
+     * should stop.
+     */
+    fun calculateScrollDistance(
+        offset: Float,
+        size: Float,
+        containerSize: Float
+    ): Float = defaultCalculateScrollDistance(offset, size, containerSize)
+
+    companion object {
+
+        /**
+         * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
+         */
+        val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
+
+        internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {}
+
+        internal fun defaultCalculateScrollDistance(
+            offset: Float,
+            size: Float,
+            containerSize: Float
+        ): Float {
+            val trailingEdge = offset + size
+            @Suppress("UnnecessaryVariable") val leadingEdge = offset
+            return when {
+
+                // If the item is already visible, no need to scroll.
+                leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
+
+                // If the item is visible but larger than the parent, we don't scroll.
+                leadingEdge < 0 && trailingEdge > containerSize -> 0f
+
+                // Find the minimum scroll needed to make one of the edges coincide with the parent's
+                // edge.
+                abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
+                else -> trailingEdge - containerSize
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
index 9976dc1..18ce61c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
@@ -26,7 +26,9 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
 import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.currentValueOf
 import androidx.compose.ui.node.requireLayoutCoordinates
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.toSize
@@ -65,8 +67,9 @@
     private var orientation: Orientation,
     private var scrollState: ScrollableState,
     private var reverseDirection: Boolean,
-    private var bringIntoViewSpec: BringIntoViewSpec
-) : Modifier.Node(), BringIntoViewResponder, LayoutAwareModifierNode {
+    private var bringIntoViewSpec: BringIntoViewSpec?
+) : Modifier.Node(), BringIntoViewResponder, LayoutAwareModifierNode,
+    CompositionLocalConsumerModifierNode {
 
     /**
      * Ongoing requests from [bringChildIntoView], with the invariant that it is always sorted by
@@ -111,6 +114,10 @@
         return computeDestination(localRect, viewportSize)
     }
 
+    private fun requireBringIntoViewSpec(): BringIntoViewSpec {
+        return bringIntoViewSpec ?: currentValueOf(LocalBringIntoViewSpec)
+    }
+
     override suspend fun bringChildIntoView(localRect: () -> Rect?) {
         // Avoid creating no-op requests and no-op animations if the request does not require
         // scrolling or returns null.
@@ -171,6 +178,7 @@
     }
 
     private fun launchAnimation() {
+        val bringIntoViewSpec = requireBringIntoViewSpec()
         check(!isAnimationRunning) { "launchAnimation called when previous animation was running" }
 
         if (DEBUG) println("[$TAG] launchAnimation")
@@ -182,7 +190,7 @@
             try {
                 isAnimationRunning = true
                 scrollState.scroll {
-                    animationState.value = calculateScrollDelta()
+                    animationState.value = calculateScrollDelta(bringIntoViewSpec)
                     if (DEBUG) println(
                         "[$TAG] Starting scroll animation down from ${animationState.value}…"
                     )
@@ -247,7 +255,7 @@
 
                             // Compute a new scroll target taking into account any resizes,
                             // replacements, or added/removed requests since the last frame.
-                            animationState.value = calculateScrollDelta()
+                            animationState.value = calculateScrollDelta(bringIntoViewSpec)
                             if (DEBUG) println(
                                 "[$TAG] scroll target after frame: ${animationState.value}"
                             )
@@ -286,7 +294,7 @@
      * Calculates how far we need to scroll to satisfy all existing BringIntoView requests and the
      * focused child tracking.
      */
-    private fun calculateScrollDelta(): Float {
+    private fun calculateScrollDelta(bringIntoViewSpec: BringIntoViewSpec): Float {
         if (viewportSize == IntSize.Zero) return 0f
 
         val rectangleToMakeVisible: Rect = findBringIntoViewRequest()
@@ -358,7 +366,7 @@
         return when (orientation) {
             Vertical -> Offset(
                 x = 0f,
-                y = bringIntoViewSpec.calculateScrollDistance(
+                y = requireBringIntoViewSpec().calculateScrollDistance(
                     childBounds.top,
                     childBounds.bottom - childBounds.top,
                     size.height
@@ -366,7 +374,7 @@
             )
 
             Horizontal -> Offset(
-                x = bringIntoViewSpec.calculateScrollDistance(
+                x = requireBringIntoViewSpec().calculateScrollDistance(
                     childBounds.left,
                     childBounds.right - childBounds.left,
                     size.width
@@ -390,7 +398,7 @@
         orientation: Orientation,
         state: ScrollableState,
         reverseDirection: Boolean,
-        bringIntoViewSpec: BringIntoViewSpec
+        bringIntoViewSpec: BringIntoViewSpec?
     ) {
         this.orientation = orientation
         this.scrollState = state
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 72c2b20..14d7aee 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -16,11 +16,9 @@
 
 package androidx.compose.foundation.gestures
 
-import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.AnimationState
 import androidx.compose.animation.core.DecayAnimationSpec
 import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.spring
 import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.animation.splineBasedDecay
 import androidx.compose.foundation.ExperimentalFoundationApi
@@ -154,7 +152,9 @@
  * @param interactionSource [MutableInteractionSource] that will be used to emit
  * drag events when this scrollable is being dragged.
  * @param bringIntoViewSpec The configuration that this scrollable should use to perform
- * scrolling when scroll requests are received from the focus system.
+ * scrolling when scroll requests are received from the focus system. If null is provided the
+ * system will use the behavior provided by [LocalBringIntoViewSpec] which by default has a
+ * platform dependent implementation.
  *
  * Note: This API is experimental as it brings support for some experimental features:
  * [overscrollEffect] and [bringIntoViewSpec].
@@ -169,7 +169,7 @@
     reverseDirection: Boolean = false,
     flingBehavior: FlingBehavior? = null,
     interactionSource: MutableInteractionSource? = null,
-    bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+    bringIntoViewSpec: BringIntoViewSpec? = null
 ) = this then ScrollableElement(
     state,
     orientation,
@@ -190,7 +190,7 @@
     val reverseDirection: Boolean,
     val flingBehavior: FlingBehavior?,
     val interactionSource: MutableInteractionSource?,
-    val bringIntoViewSpec: BringIntoViewSpec
+    val bringIntoViewSpec: BringIntoViewSpec?
 ) : ModifierNodeElement<ScrollableNode>() {
     override fun create(): ScrollableNode {
         return ScrollableNode(
@@ -269,7 +269,7 @@
     enabled: Boolean,
     reverseDirection: Boolean,
     interactionSource: MutableInteractionSource?,
-    bringIntoViewSpec: BringIntoViewSpec
+    private val bringIntoViewSpec: BringIntoViewSpec?
 ) : DragGestureNode(
     canDrag = CanDragCalculation,
     enabled = enabled,
@@ -354,7 +354,7 @@
         reverseDirection: Boolean,
         flingBehavior: FlingBehavior?,
         interactionSource: MutableInteractionSource?,
-        bringIntoViewSpec: BringIntoViewSpec
+        bringIntoViewSpec: BringIntoViewSpec?
     ) {
 
         if (this.enabled != enabled) { // enabled changed
@@ -490,80 +490,6 @@
 }
 
 /**
- * The configuration of how a scrollable reacts to bring into view requests.
- *
- * Note: API shape and naming are still being refined, therefore API is marked as experimental.
- */
-@ExperimentalFoundationApi
-@Stable
-interface BringIntoViewSpec {
-
-    /**
-     * A retargetable Animation Spec to be used as the animation to run to fulfill the
-     * BringIntoView requests.
-     */
-    val scrollAnimationSpec: AnimationSpec<Float> get() = DefaultScrollAnimationSpec
-
-    /**
-     * Calculate the offset needed to bring one of the scrollable container's child into view.
-     *
-     * @param offset from the side closest to the origin (For the x-axis this is 'left',
-     * for the y-axis this is 'top').
-     * @param size is the child size.
-     * @param containerSize Is the main axis size of the scrollable container.
-     *
-     * All distances above are represented in pixels.
-     *
-     * @return The necessary amount to scroll to satisfy the bring into view request.
-     * Returning zero from here means that the request was satisfied and the scrolling animation
-     * should stop.
-     *
-     * This will be called for every frame of the scrolling animation. This means that, as the
-     * animation progresses, the offset will naturally change to fulfill the scroll request.
-     */
-    fun calculateScrollDistance(
-        offset: Float,
-        size: Float,
-        containerSize: Float
-    ): Float
-
-    companion object {
-
-        /**
-         * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
-         */
-        val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
-
-        internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {
-
-            override val scrollAnimationSpec: AnimationSpec<Float> = DefaultScrollAnimationSpec
-
-            override fun calculateScrollDistance(
-                offset: Float,
-                size: Float,
-                containerSize: Float
-            ): Float {
-                val trailingEdge = offset + size
-                @Suppress("UnnecessaryVariable") val leadingEdge = offset
-                return when {
-
-                    // If the item is already visible, no need to scroll.
-                    leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
-
-                    // If the item is visible but larger than the parent, we don't scroll.
-                    leadingEdge < 0 && trailingEdge > containerSize -> 0f
-
-                    // Find the minimum scroll needed to make one of the edges coincide with the parent's
-                    // edge.
-                    abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
-                    else -> trailingEdge - containerSize
-                }
-            }
-        }
-    }
-}
-
-/**
  * Contains the default values used by [scrollable]
  */
 object ScrollableDefaults {
@@ -619,6 +545,13 @@
      * A default implementation for [BringIntoViewSpec] that brings a child into view
      * using the least amount of effort.
      */
+    @Deprecated(
+        "This has been replaced by composition locals LocalBringIntoViewSpec",
+        replaceWith = ReplaceWith(
+            "LocalBringIntoView.current",
+            "androidx.compose.foundation.gestures.LocalBringIntoViewSpec"
+        )
+    )
     @ExperimentalFoundationApi
     fun bringIntoViewSpec(): BringIntoViewSpec = DefaultBringIntoViewSpec
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index 0de86a8..8771eed 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -22,9 +22,9 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.BringIntoViewSpec
 import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableDefaults
 import androidx.compose.foundation.gestures.TargetedFlingBehavior
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.awaitFirstDown
@@ -134,7 +134,7 @@
         PagerWrapperFlingBehavior(flingBehavior, state)
     }
 
-    val defaultBringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+    val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
     val pagerBringIntoViewSpec = remember(state, defaultBringIntoViewSpec) {
         PagerBringIntoViewSpec(
             state,
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
new file mode 100644
index 0000000..061d648
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/*
+* A composition local to customize the focus scrolling behavior used by some scrollable containers.
+* [LocalBringIntoViewSpec] has a platform defined behavior. The scroll default behavior will move
+* the least to bring the requested region into view.
+*/
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
+    staticCompositionLocalOf {
+        BringIntoViewSpec.DefaultBringIntoViewSpec
+    }
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
index 811c9f7..0da1391 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
+++ b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
@@ -30,7 +30,6 @@
 import androidx.datastore.core.twoWayIpc.IpcUnit
 import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
 import androidx.datastore.testing.TestMessageProto.FooProto
-import androidx.kruth.assertThrows
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
@@ -565,12 +564,11 @@
         delay(100)
 
         // process A (could be another thread than 1.) waits to hold file lock 2 (still held by B)
-        assertThrows<IOException> {
+        val localUpdate2 = async {
             datastore2.updateData {
                 it.toBuilder().setInteger(4).build()
             }
-        }.hasMessageThat()
-            .contains("Resource deadlock would occur")
+        }
 
         blockWrite.complete(Unit)
         commitWriteLatch1.complete(subject2, IpcUnit)
@@ -579,9 +577,11 @@
         setTextAction1.await()
         setTextAction2.await()
         localUpdate1.await()
+        localUpdate2.await()
 
         assertThat(datastore1.data.first().text).isEqualTo("remoteValue")
         assertThat(datastore1.data.first().integer).isEqualTo(3)
         assertThat(datastore2.data.first().text).isEqualTo("remoteValue")
+        assertThat(datastore2.data.first().integer).isEqualTo(4)
     }
 }
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
index 3d3edff..e596d66 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
@@ -25,6 +25,7 @@
 import java.nio.channels.FileLock
 import kotlin.contracts.ExperimentalContracts
 import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
@@ -43,7 +44,7 @@
             FileOutputStream(lockFile).use { lockFileStream ->
                 var lock: FileLock? = null
                 try {
-                    lock = lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+                    lock = getExclusiveFileLockWithRetryIfDeadlock(lockFileStream)
                     return block()
                 } finally {
                     lock?.release()
@@ -78,7 +79,8 @@
                         // will throw an IOException with EAGAIN error, instead of returning null as
                         // specified in {@link FileChannel#tryLock}. We only continue if the error
                         // message is EAGAIN, otherwise just throw it.
-                        if (ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) {
+                        if ((ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) &&
+                            (ex.message?.startsWith(DEADLOCK_ERROR_MESSAGE) != true)) {
                             throw ex
                         }
                     }
@@ -162,6 +164,32 @@
             }
         }
     }
+
+    companion object {
+        // Retry with exponential backoff to get file lock if it hits "Resource deadlock would
+        // occur" error until the backoff reaches [MAX_WAIT_MILLIS].
+        private suspend fun getExclusiveFileLockWithRetryIfDeadlock(
+            lockFileStream: FileOutputStream
+        ): FileLock {
+            var backoff = INITIAL_WAIT_MILLIS
+            while (backoff <= MAX_WAIT_MILLIS) {
+                try {
+                    return lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+                } catch (ex: IOException) {
+                    if (ex.message?.contains(DEADLOCK_ERROR_MESSAGE) != true) {
+                        throw ex
+                    }
+                    delay(backoff)
+                    backoff *= 2
+                }
+            }
+            return lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+        }
+
+        private val DEADLOCK_ERROR_MESSAGE = "Resource deadlock would occur"
+        private val INITIAL_WAIT_MILLIS: Long = 10
+        private val MAX_WAIT_MILLIS: Long = 60000
+    }
 }
 
 /**