Merge "Fix closing of javadoc/kdocs" into androidx-main
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
index 70780ce..fcc1a92 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
@@ -46,7 +46,6 @@
 import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
-import javax.lang.model.util.ElementFilter;
 import javax.lang.model.util.Elements;
 import javax.lang.model.util.Types;
 
@@ -76,11 +75,10 @@
     // The name of the original class annotated with @Document
     private final String mQualifiedDocumentClassName;
     private String mSchemaName;
-    private Set<TypeElement> mParentTypes = new LinkedHashSet<>();
+    private final Set<TypeElement> mParentTypes = new LinkedHashSet<>();
     // Warning: if you change this to a HashSet, we may choose different getters or setters from
     // run to run, causing the generated code to bounce.
     private final Set<ExecutableElement> mAllMethods = new LinkedHashSet<>();
-    private final boolean mIsAutoValueDocument;
     // Key: Name of the element which is accessed through the getter method.
     // Value: ExecutableElement of the getter method.
     private final Map<String, ExecutableElement> mGetterMethods = new HashMap<>();
@@ -119,34 +117,16 @@
         mClass = clazz;
         mTypeUtil = env.getTypeUtils();
         mElementUtil = env.getElementUtils();
+        mQualifiedDocumentClassName = generatedAutoValueElement != null
+                ? generatedAutoValueElement.getQualifiedName().toString()
+                : clazz.getQualifiedName().toString();
 
-        if (generatedAutoValueElement != null) {
-            mIsAutoValueDocument = true;
-            // Scan factory methods from AutoValue class.
-            Set<ExecutableElement> creationMethods = new LinkedHashSet<>();
-            for (Element child : ElementFilter.methodsIn(mClass.getEnclosedElements())) {
-                ExecutableElement method = (ExecutableElement) child;
-                if (isFactoryMethod(method)) {
-                    creationMethods.add(method);
-                }
-            }
-            mAllMethods.addAll(
-                    ElementFilter.methodsIn(generatedAutoValueElement.getEnclosedElements()));
-
-            mQualifiedDocumentClassName = generatedAutoValueElement.getQualifiedName().toString();
-            scanFields(generatedAutoValueElement);
-            scanCreationMethods(creationMethods);
-        } else {
-            mIsAutoValueDocument = false;
-            // Scan methods and constructors. We will need this info when processing fields to
-            // make sure the fields can be get and set.
-            Set<ExecutableElement> creationMethods = extractCreationMethods(mClass);
-            addAllMethods(mClass, mAllMethods);
-
-            mQualifiedDocumentClassName = clazz.getQualifiedName().toString();
-            scanFields(mClass);
-            scanCreationMethods(creationMethods);
-        }
+        addAllMethods(mClass, mAllMethods);
+        scanFields(mClass);
+        // Scan methods and constructors. We will need this info when processing fields to
+        // make sure the fields can be get and set.
+        Set<ExecutableElement> potentialCreationMethods = extractCreationMethods(clazz);
+        chooseCreationMethod(potentialCreationMethods);
     }
 
     private Set<ExecutableElement> extractCreationMethods(TypeElement typeElement) {
@@ -298,9 +278,6 @@
     public AnnotationMirror getPropertyAnnotation(@NonNull Element element)
             throws ProcessingException {
         Objects.requireNonNull(element);
-        if (mIsAutoValueDocument) {
-            element = getGetterForElement(element.getSimpleName().toString());
-        }
         Set<String> propertyClassPaths = new HashSet<>();
         for (PropertyClass propertyClass : PropertyClass.values()) {
             propertyClassPaths.add(propertyClass.getClassFullPath());
@@ -334,12 +311,9 @@
     /**
      * Scan the annotations of a field to determine the fields type and handle it accordingly
      *
-     * @param classElements all the field elements of a class, annotated and non-annotated
      * @param childElement the member of class elements currently being scanned
-     * @throws ProcessingException
      */
-    private void scanAnnotatedField(@NonNull List<? extends Element> classElements,
-            @NonNull Element childElement) throws ProcessingException {
+    private void scanAnnotatedField(@NonNull Element childElement) throws ProcessingException {
         String fieldName = childElement.getSimpleName().toString();
 
         // a property field shouldn't be able to override a special field
@@ -355,22 +329,16 @@
             if (!annotationFq.startsWith(DOCUMENT_ANNOTATION_CLASS)) {
                 continue;
             }
-            Element child;
-            if (mIsAutoValueDocument) {
-                child = findFieldForFunctionWithSameName(classElements, childElement);
-            } else {
-                if (childElement.getKind() == ElementKind.CLASS) {
-                    continue;
-                } else {
-                    child = childElement;
-                }
+            if (childElement.getKind() == ElementKind.CLASS) {
+                continue;
             }
 
             switch (annotationFq) {
                 case IntrospectionHelper.ID_CLASS:
                     if (mSpecialFieldNames.containsKey(SpecialField.ID)) {
                         throw new ProcessingException(
-                                "Class hierarchy contains multiple fields annotated @Id", child);
+                                "Class hierarchy contains multiple fields annotated @Id",
+                                childElement);
                     }
                     mSpecialFieldNames.put(SpecialField.ID, fieldName);
                     break;
@@ -378,14 +346,14 @@
                     if (mSpecialFieldNames.containsKey(SpecialField.NAMESPACE)) {
                         throw new ProcessingException(
                                 "Class hierarchy contains multiple fields annotated @Namespace",
-                                child);
+                                childElement);
                     }
                     mSpecialFieldNames.put(SpecialField.NAMESPACE, fieldName);
                     break;
                 case IntrospectionHelper.CREATION_TIMESTAMP_MILLIS_CLASS:
                     if (mSpecialFieldNames.containsKey(SpecialField.CREATION_TIMESTAMP_MILLIS)) {
                         throw new ProcessingException("Class hierarchy contains multiple fields "
-                                + "annotated @CreationTimestampMillis", child);
+                                + "annotated @CreationTimestampMillis", childElement);
                     }
                     mSpecialFieldNames.put(
                             SpecialField.CREATION_TIMESTAMP_MILLIS, fieldName);
@@ -394,14 +362,15 @@
                     if (mSpecialFieldNames.containsKey(SpecialField.TTL_MILLIS)) {
                         throw new ProcessingException(
                                 "Class hierarchy contains multiple fields annotated @TtlMillis",
-                                child);
+                                childElement);
                     }
                     mSpecialFieldNames.put(SpecialField.TTL_MILLIS, fieldName);
                     break;
                 case IntrospectionHelper.SCORE_CLASS:
                     if (mSpecialFieldNames.containsKey(SpecialField.SCORE)) {
                         throw new ProcessingException(
-                                "Class hierarchy contains multiple fields annotated @Score", child);
+                                "Class hierarchy contains multiple fields annotated @Score",
+                                childElement);
                     }
                     mSpecialFieldNames.put(SpecialField.SCORE, fieldName);
                     break;
@@ -412,7 +381,7 @@
                         //   1. be unique
                         //   2. override a property from the Java parent while maintaining the same
                         //      AppSearch property name
-                        checkFieldTypeForPropertyAnnotation(child, propertyClass);
+                        checkFieldTypeForPropertyAnnotation(childElement, propertyClass);
                         // It's assumed that parent types, in the context of Java's type system,
                         // are always visited before child types, so existingProperty must come
                         // from the parent type. To make this assumption valid, the result
@@ -420,20 +389,24 @@
                         // types.
                         Element existingProperty = mPropertyElements.get(fieldName);
                         if (existingProperty != null) {
-                            if (!mTypeUtil.isSameType(existingProperty.asType(), child.asType())) {
+                            if (!mTypeUtil.isSameType(
+                                    existingProperty.asType(), childElement.asType())) {
                                 throw new ProcessingException(
-                                        "Cannot override a property with a different type", child);
+                                        "Cannot override a property with a different type",
+                                        childElement);
                             }
-                            if (!getPropertyName(existingProperty).equals(getPropertyName(child))) {
+                            if (!getPropertyName(existingProperty).equals(getPropertyName(
+                                    childElement))) {
                                 throw new ProcessingException(
-                                        "Cannot override a property with a different name", child);
+                                        "Cannot override a property with a different name",
+                                        childElement);
                             }
                         }
-                        mPropertyElements.put(fieldName, child);
+                        mPropertyElements.put(fieldName, childElement);
                     }
             }
 
-            mAllAppSearchElements.put(fieldName, child);
+            mAllAppSearchElements.put(fieldName, childElement);
         }
     }
 
@@ -463,20 +436,12 @@
             }
         }
 
-        List<TypeElement> hierarchy = generateClassHierarchy(element, mIsAutoValueDocument);
+        List<TypeElement> hierarchy = generateClassHierarchy(element);
 
         for (TypeElement clazz : hierarchy) {
             List<? extends Element> enclosedElements = clazz.getEnclosedElements();
-            for (int i = 0; i < enclosedElements.size(); i++) {
-                Element childElement = enclosedElements.get(i);
-
-                // The only fields relevant to @Document in an AutoValue class are the abstract
-                // accessor methods
-                if (mIsAutoValueDocument && childElement.getKind() != ElementKind.METHOD) {
-                    continue;
-                }
-
-                scanAnnotatedField(enclosedElements, childElement);
+            for (Element childElement : enclosedElements) {
+                scanAnnotatedField(childElement);
             }
         }
 
@@ -501,21 +466,6 @@
         }
     }
 
-    @NonNull
-    private Element findFieldForFunctionWithSameName(
-            @NonNull List<? extends Element> elements,
-            @NonNull Element functionElement) throws ProcessingException {
-        String fieldName = functionElement.getSimpleName().toString();
-        for (Element field : ElementFilter.fieldsIn(elements)) {
-            if (fieldName.equals(field.getSimpleName().toString())) {
-                return field;
-            }
-        }
-        throw new ProcessingException(
-                "Cannot find the corresponding field for the annotated function",
-                functionElement);
-    }
-
     /**
      * Checks whether property's data type matches the {@code androidx.appsearch.annotation
      * .Document} property annotation's requirement.
@@ -627,7 +577,7 @@
         }
     }
 
-    private void scanCreationMethods(Set<ExecutableElement> creationMethods)
+    private void chooseCreationMethod(Set<ExecutableElement> creationMethods)
             throws ProcessingException {
         // Maps field name to Element.
         // If this is changed to a HashSet, we might report errors to the developer in a different
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index f289cb3..42bcf93 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -15,12 +15,13 @@
  */
 package androidx.appsearch.compiler;
 
+import static com.google.auto.common.MoreTypes.asTypeElement;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 
-import com.google.auto.common.MoreTypes;
 import com.google.auto.value.AutoValue;
 import com.squareup.javapoet.ClassName;
 
@@ -237,17 +238,14 @@
      */
     @NonNull
     public static List<TypeElement> generateClassHierarchy(
-            @NonNull TypeElement element, boolean isAutoValueDocument)
-            throws ProcessingException {
+            @NonNull TypeElement element) throws ProcessingException {
         Deque<TypeElement> hierarchy = new ArrayDeque<>();
-        if (isAutoValueDocument) {
+        if (element.getAnnotation(AutoValue.class) != null) {
             // We don't allow classes annotated with both Document and AutoValue to extend classes.
             // Because of how AutoValue is set up, there is no way to add a constructor to
             // populate fields of super classes.
             // There should just be the generated class and the original annotated class
-            TypeElement superClass = MoreTypes.asTypeElement(
-                    MoreTypes.asTypeElement(element.getSuperclass()).getSuperclass());
-
+            TypeElement superClass = asTypeElement(element.getSuperclass());
             if (!superClass.getQualifiedName().contentEquals(Object.class.getCanonicalName())) {
                 throw new ProcessingException(
                         "A class annotated with AutoValue and Document cannot have a superclass",
@@ -289,11 +287,11 @@
         TypeMirror superclass = currentClass.getSuperclass();
         // If currentClass is an interface, then superclass could be NONE.
         if (superclass.getKind() != TypeKind.NONE) {
-            generateClassHierarchyHelper(leafElement, MoreTypes.asTypeElement(superclass),
+            generateClassHierarchyHelper(leafElement, asTypeElement(superclass),
                     hierarchy, visited);
         }
         for (TypeMirror implementedInterface : currentClass.getInterfaces()) {
-            generateClassHierarchyHelper(leafElement, MoreTypes.asTypeElement(implementedInterface),
+            generateClassHierarchyHelper(leafElement, asTypeElement(implementedInterface),
                     hierarchy, visited);
         }
     }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index bef5649..248d2a9 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -17,6 +17,7 @@
 package androidx.bluetooth.integration.testapp.ui.advertiser
 
 // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once Gatt Server API is in place
+// TODO(ofy) Migrate to androidx.bluetooth.GattService
 import android.Manifest
 import android.annotation.SuppressLint
 import android.bluetooth.BluetoothGattCharacteristic
@@ -44,6 +45,8 @@
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
 import com.google.android.material.tabs.TabLayout
 import java.util.UUID
 import kotlinx.coroutines.CoroutineScope
@@ -61,7 +64,7 @@
 
     private lateinit var bluetoothLe: BluetoothLe
 
-    // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once scan API is in place
+    // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once openGattServer API is in place
     private lateinit var bluetoothLeExperimental: BluetoothLeExperimental
 
     private var advertiseDataAdapter: AdvertiseDataAdapter? = null
@@ -91,6 +94,8 @@
             _binding?.viewRecyclerViewOverlay?.isVisible = value
         }
 
+    private var gattServerServicesAdapter: GattServerServicesAdapter? = null
+
     private var isGattServerOpen: Boolean = false
         set(value) {
             field = value
@@ -182,6 +187,20 @@
             }
         }
 
+        binding.buttonAddService.setOnClickListener {
+            addGattService()
+        }
+
+        gattServerServicesAdapter =
+            GattServerServicesAdapter(
+                viewModel.gattServerServices,
+                ::addGattCharacteristic
+            )
+        binding.recyclerViewGattServerServices.adapter = gattServerServicesAdapter
+        binding.recyclerViewGattServerServices.addItemDecoration(
+            DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
+        )
+
         binding.buttonGattServer.setOnClickListener {
             if (gattServerJob?.isActive == true) {
                 isGattServerOpen = false
@@ -340,26 +359,48 @@
         }
     }
 
-    private fun openGattServer() {
-        Log.d(TAG, "openGattServer() called")
+    private fun addGattService() {
+        // TODO(ofy) Show dialog for Service customization and replace sampleService
 
         val sampleService =
             BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY)
+
+        viewModel.gattServerAddService(sampleService)
+
+        gattServerServicesAdapter?.notifyItemInserted(viewModel.gattServerServices.size - 1)
+    }
+
+    private fun addGattCharacteristic(bluetoothGattService: BluetoothGattService) {
+        // TODO(ofy) Show dialog for Characteristic customization and replace sampleCharacteristic
+
         val sampleCharacteristic = BluetoothGattCharacteristic(
             UUID.randomUUID(),
             BluetoothGattCharacteristic.PROPERTY_READ,
             BluetoothGattCharacteristic.PERMISSION_READ
         )
-        sampleService.addCharacteristic(sampleCharacteristic)
 
-        val services = listOf(sampleService)
+        bluetoothGattService.addCharacteristic(sampleCharacteristic)
+
+        gattServerServicesAdapter?.notifyItemChanged(
+            viewModel.gattServerServices.indexOf(
+                bluetoothGattService
+            )
+        )
+    }
+
+    private fun openGattServer() {
+        Log.d(TAG, "openGattServer() called")
 
         gattServerJob = gattServerScope.launch {
             isGattServerOpen = true
 
-            bluetoothLeExperimental.openGattServer(services).collect { gattServerCallback ->
-                Log.d(TAG, "openGattServer() called with: gattServerCallback = $gattServerCallback")
-            }
+            bluetoothLeExperimental.openGattServer(viewModel.gattServerServices)
+                .collect { gattServerCallback ->
+                    Log.d(
+                        TAG,
+                        "openGattServer() called with: gattServerCallback = $gattServerCallback"
+                    )
+                }
         }
     }
 }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
index f0e4cdd..2109813 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
@@ -16,6 +16,8 @@
 
 package androidx.bluetooth.integration.testapp.ui.advertiser
 
+// TODO(ofy) Migrate to androidx.bluetooth.GattService
+import android.bluetooth.BluetoothGattService
 import androidx.bluetooth.AdvertiseParams
 import androidx.lifecycle.ViewModel
 import java.util.UUID
@@ -60,6 +62,8 @@
             serviceUuids
         )
 
+    val gattServerServices = mutableListOf<BluetoothGattService>()
+
     fun removeAdvertiseDataAtIndex(index: Int) {
         val manufacturerDataSize = manufacturerDatas.size
         val serviceDataSize = serviceDatas.size
@@ -72,4 +76,8 @@
             serviceUuids.removeAt(index - manufacturerDataSize - serviceDataSize)
         }
     }
+
+    fun gattServerAddService(bluetoothGattService: BluetoothGattService) {
+        gattServerServices.add(bluetoothGattService)
+    }
 }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerCharacteristicsAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerCharacteristicsAdapter.kt
new file mode 100644
index 0000000..57608e1
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerCharacteristicsAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.integration.testapp.ui.advertiser
+
+import android.bluetooth.BluetoothGattCharacteristic
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.bluetooth.GattCharacteristic
+import androidx.bluetooth.integration.testapp.R
+import androidx.recyclerview.widget.RecyclerView
+
+class GattServerCharacteristicsAdapter(
+    private val characteristics: List<BluetoothGattCharacteristic>
+) : RecyclerView.Adapter<GattServerCharacteristicsAdapter.ViewHolder>() {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_gatt_server_characteristic, parent, false)
+        return ViewHolder(view)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        holder.bind(characteristics[position])
+    }
+
+    override fun getItemCount(): Int {
+        return characteristics.size
+    }
+
+    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+        private val textViewUuid: TextView = itemView.findViewById(R.id.text_view_uuid)
+        private val textViewProperties: TextView = itemView.findViewById(R.id.text_view_properties)
+
+        fun bind(characteristic: BluetoothGattCharacteristic) {
+            textViewUuid.text = characteristic.uuid.toString()
+
+            val properties = characteristic.properties
+            val context = itemView.context
+
+            val propertiesList = mutableListOf<String>()
+            // TODO(ofy) Update these with GattCharacteristic.isReadable, isWriteable, ...
+            if (properties.and(GattCharacteristic.PROPERTY_INDICATE) != 0) {
+                propertiesList.add(context.getString(R.string.indicate))
+            }
+            if (properties.and(GattCharacteristic.PROPERTY_NOTIFY) != 0) {
+                propertiesList.add(context.getString(R.string.notify))
+            }
+            val isReadable = properties.and(GattCharacteristic.PROPERTY_READ) != 0
+            if (isReadable) {
+                propertiesList.add(context.getString(R.string.read))
+            }
+            val isWriteable = (properties.and(GattCharacteristic.PROPERTY_WRITE) != 0 ||
+                properties.and(GattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0 ||
+                properties.and(GattCharacteristic.PROPERTY_SIGNED_WRITE) != 0)
+            if (isWriteable) {
+                propertiesList.add(context.getString(R.string.write))
+            }
+            textViewProperties.text = propertiesList.joinToString()
+        }
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerServicesAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerServicesAdapter.kt
new file mode 100644
index 0000000..d2fd6c2
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/GattServerServicesAdapter.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.integration.testapp.ui.advertiser
+
+import android.bluetooth.BluetoothGattService
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.TextView
+import androidx.bluetooth.integration.testapp.R
+import androidx.recyclerview.widget.RecyclerView
+
+class GattServerServicesAdapter(
+    private val services: List<BluetoothGattService>,
+    private val onClickAddCharacteristic: (BluetoothGattService) -> Unit
+) : RecyclerView.Adapter<GattServerServicesAdapter.ViewHolder>() {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(parent.context)
+            .inflate(R.layout.item_gatt_server_service, parent, false)
+        return ViewHolder(view)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        holder.bind(services[position])
+    }
+
+    override fun getItemCount(): Int {
+        return services.size
+    }
+
+    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+        private val textViewUuid: TextView = itemView.findViewById(R.id.text_view_uuid)
+        private val buttonAddCharacteristic: Button =
+            itemView.findViewById(R.id.button_add_characteristic)
+
+        private val recyclerViewServiceCharacteristic: RecyclerView =
+            itemView.findViewById(R.id.recycler_view_service_characteristic)
+
+        private var currentBluetoothGattService: BluetoothGattService? = null
+
+        init {
+            buttonAddCharacteristic.setOnClickListener {
+                currentBluetoothGattService?.let(onClickAddCharacteristic)
+            }
+        }
+
+        fun bind(bluetoothGattService: BluetoothGattService) {
+            currentBluetoothGattService = bluetoothGattService
+
+            textViewUuid.text = bluetoothGattService.uuid.toString()
+
+            recyclerViewServiceCharacteristic.adapter = GattServerCharacteristicsAdapter(
+                bluetoothGattService.characteristics
+            )
+        }
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
index 024d732..4181f87 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
@@ -164,6 +164,37 @@
         app:layout_constraintTop_toBottomOf="@+id/tab_layout"
         tools:visibility="visible">
 
+        <androidx.core.widget.NestedScrollView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintTop_toBottomOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/recycler_view_gatt_server_services"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    app:layoutManager="LinearLayoutManager"
+                    tools:itemCount="1"
+                    tools:listitem="@layout/item_gatt_server_service" />
+
+                <Button
+                    android:id="@+id/button_add_service"
+                    style="@style/Widget.MaterialComponents.Button.TextButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_margin="16dp"
+                    android:text="@string/add_service" />
+
+            </LinearLayout>
+
+        </androidx.core.widget.NestedScrollView>
+
         <Button
             android:id="@+id/button_gatt_server"
             android:layout_width="wrap_content"
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_characteristic.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_characteristic.xml
new file mode 100644
index 0000000..64bb680
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_characteristic.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="8dp">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/uuid"
+            android:textAllCaps="true" />
+
+        <TextView
+            android:id="@+id/text_view_uuid"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:textColor="@color/black"
+            tools:text="0x1800" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/properties" />
+
+        <TextView
+            android:id="@+id/text_view_properties"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:textAllCaps="true"
+            android:textColor="@color/black"
+            tools:text="@string/read" />
+
+    </LinearLayout>
+
+    <!-- TODO(ofy) Add Descriptor -->
+
+    <!--    <Button-->
+    <!--        android:id="@+id/button_add_descriptor"-->
+    <!--        style="@style/Widget.MaterialComponents.Button.TextButton"-->
+    <!--        android:layout_width="wrap_content"-->
+    <!--        android:layout_height="wrap_content"-->
+    <!--        android:layout_marginStart="16dp"-->
+    <!--        android:text="@string/add_descriptor" />-->
+
+</LinearLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_service.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_service.xml
new file mode 100644
index 0000000..bbb6557
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/item_gatt_server_service.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="8dp">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/generic_attribute"
+        android:textColor="@color/black"
+        android:textStyle="bold" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/uuid"
+            android:textAllCaps="true" />
+
+        <TextView
+            android:id="@+id/text_view_uuid"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:textColor="@color/black"
+            tools:text="0x1800" />
+
+    </LinearLayout>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/primary_service"
+        android:textAllCaps="true" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler_view_service_characteristic"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        app:layoutManager="LinearLayoutManager"
+        tools:itemCount="3"
+        tools:listitem="@layout/item_gatt_server_characteristic" />
+
+    <Button
+        android:id="@+id/button_add_characteristic"
+        style="@style/Widget.MaterialComponents.Button.TextButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:text="@string/add_characteristic" />
+
+</LinearLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 06dffe7..fa1ad57 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -67,6 +67,9 @@
 
     <!-- GATT Server -->
     <string name="gatt_server">Gatt Server</string>
+    <string name="add_service">Add Service</string>
+    <string name="add_characteristic">Add Characteristic</string>
+    <string name="add_descriptor">Add Descriptor</string>
     <string name="open_gatt_server">Open Gatt Server</string>
     <string name="stop_gatt_server">Stop Gatt Server</string>
 </resources>
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index 416b0e4..4d083da 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -35,7 +35,6 @@
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.ImageCaptureConfig
 import androidx.camera.core.impl.ImageOutputConfig
-import androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR
 import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.OptionsBundle
 import androidx.camera.core.impl.PreviewConfig
@@ -43,8 +42,6 @@
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
-import androidx.camera.core.resolutionselector.ResolutionSelector
-import androidx.camera.core.resolutionselector.ResolutionStrategy
 
 /**
  * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
@@ -136,15 +133,6 @@
                 ImageOutputConfig.OPTION_MAX_RESOLUTION,
                 previewSize
             )
-            mutableConfig.insertOption(
-                OPTION_RESOLUTION_SELECTOR,
-                ResolutionSelector.Builder().setResolutionStrategy(
-                    ResolutionStrategy(
-                        previewSize,
-                        ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
-                    )
-                ).build()
-            )
         }
 
         mutableConfig.insertOption(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
index 3d271cd..0010019 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
@@ -17,7 +17,6 @@
 package androidx.camera.camera2.internal;
 
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
@@ -37,8 +36,6 @@
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.resolutionselector.ResolutionSelector;
-import androidx.camera.core.resolutionselector.ResolutionStrategy;
 
 /**
  * Implementation of UseCaseConfigFactory to provide the default camera2 configurations for use
@@ -86,11 +83,6 @@
         if (captureType == CaptureType.PREVIEW) {
             Size previewSize = mDisplayInfoManager.getPreviewSize();
             mutableConfig.insertOption(OPTION_MAX_RESOLUTION, previewSize);
-            ResolutionStrategy resolutionStrategy = new ResolutionStrategy(previewSize,
-                    ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER);
-            mutableConfig.insertOption(OPTION_RESOLUTION_SELECTOR,
-                    new ResolutionSelector.Builder().setResolutionStrategy(
-                            resolutionStrategy).build());
         }
 
         // The default rotation value should be determined by the max non-state-off display.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 6661ecb..e36507c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -1080,10 +1080,8 @@
          * size match to the device's screen resolution, or to 1080p (1920x1080), whichever is
          * smaller. See the
          * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
-         * section in {@link android.hardware.camera2.CameraDevice}'. {@link Preview} has a
-         * default {@link ResolutionStrategy} with the {@code PREVIEW} bound size and
-         * {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER} to achieve this. Applications
-         * can override this default strategy with a different resolution strategy.
+         * section in {@link android.hardware.camera2.CameraDevice}'. Applications can set any
+         * {@link ResolutionStrategy} to override it.
          *
          * <p>Note that due to compatibility reasons, CameraX may select a resolution that is
          * larger than the default screen resolution on certain devices.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 396e646..a002de7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -19,6 +19,7 @@
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
@@ -226,6 +227,17 @@
             }
         }
 
+        // Removes the default max resolution setting if application sets any ResolutionStrategy
+        // to override it.
+        if (mUseCaseConfig.containsOption(OPTION_RESOLUTION_SELECTOR)
+                && mergedConfig.containsOption(OPTION_MAX_RESOLUTION)) {
+            ResolutionSelector resolutionSelector =
+                    mUseCaseConfig.retrieveOption(OPTION_RESOLUTION_SELECTOR);
+            if (resolutionSelector.getResolutionStrategy() != null) {
+                mergedConfig.removeOption(OPTION_MAX_RESOLUTION);
+            }
+        }
+
         // If any options need special handling, this is the place to do it. For now we'll just copy
         // over all options.
         for (Option<?> opt : mUseCaseConfig.listOptions()) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
index 7bbe75e..be65059 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -40,6 +40,7 @@
 import androidx.camera.core.impl.utils.AspectRatioUtil;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.core.internal.utils.SizeUtil;
 import androidx.camera.core.resolutionselector.AspectRatioStrategy;
 import androidx.camera.core.resolutionselector.ResolutionFilter;
 import androidx.camera.core.resolutionselector.ResolutionSelector;
@@ -216,6 +217,13 @@
                 applyAspectRatioStrategy(resolutionCandidateList,
                         resolutionSelector.getAspectRatioStrategy());
 
+
+        // Applies the max resolution setting
+        Size maxResolution = ((ImageOutputConfig) useCaseConfig).getMaxResolution(null);
+        if (maxResolution != null) {
+            applyMaxResolutionRestriction(aspectRatioSizeListMap, maxResolution);
+        }
+
         // Applies the resolution strategy onto the resolution candidate list.
         applyResolutionStrategy(aspectRatioSizeListMap, resolutionSelector.getResolutionStrategy());
 
@@ -436,6 +444,32 @@
     }
 
     /**
+     * Applies the max resolution restriction.
+     *
+     * <p>Filters out the output sizes that exceed the max resolution in area size.
+     *
+     * @param sortedAspectRatioSizeListMap the aspect ratio to size list linked hash map. The
+     *                                     entries order should not be changed.
+     * @param maxResolution                the max resolution size.
+     */
+    private static void applyMaxResolutionRestriction(
+            @NonNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap,
+            @NonNull Size maxResolution) {
+        int maxResolutionAreaSize = SizeUtil.getArea(maxResolution);
+        for (Rational key : sortedAspectRatioSizeListMap.keySet()) {
+            List<Size> supportedSizesList = sortedAspectRatioSizeListMap.get(key);
+            List<Size> filteredResultList = new ArrayList<>();
+            for (Size size : supportedSizesList) {
+                if (SizeUtil.getArea(size) <= maxResolutionAreaSize) {
+                    filteredResultList.add(size);
+                }
+            }
+            supportedSizesList.clear();
+            supportedSizesList.addAll(filteredResultList);
+        }
+    }
+
+    /**
      * Applies the resolution filtered to the sorted output size list.
      *
      * @param sizeList         the supported size list which has been filtered and sorted by the
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
index 3cb7587..e79c482 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -540,6 +540,28 @@
         supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
     }
 
+    @Test
+    fun canKeepFhdResolution_whenMaxResolutionHasShorterEdgeButLargerArea() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            maxResolution = Size(2244, 1008),
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(1920, 1080), // 16:9, this can be kept even the max resolution
+                                               // setting has a shorter edge of 1008
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        )
+    }
+
     private fun verifySupportedOutputSizesWithResolutionSelectorSettings(
         outputSizesSorter: SupportedOutputSizesSorter = supportedOutputSizesSorter,
         captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
@@ -551,6 +573,7 @@
         resolutionFilter: ResolutionFilter? = null,
         allowedResolutionMode: Int = ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION,
         highResolutionForceDisabled: Boolean = false,
+        maxResolution: Size? = null,
         expectedList: List<Size> = Collections.emptyList(),
     ) {
         val useCaseConfig = createUseCaseConfig(
@@ -562,7 +585,8 @@
             resolutionFallbackRule,
             resolutionFilter,
             allowedResolutionMode,
-            highResolutionForceDisabled
+            highResolutionForceDisabled,
+            maxResolution,
         )
         val resultList = outputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
         assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
@@ -578,6 +602,7 @@
         resolutionFilter: ResolutionFilter? = null,
         allowedResolutionMode: Int = ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION,
         highResolutionForceDisabled: Boolean = false,
+        maxResolution: Size? = null,
     ): UseCaseConfig<*> {
         val useCaseConfigBuilder = FakeUseCaseConfig.Builder(captureType, ImageFormat.JPEG)
         val resolutionSelectorBuilder = ResolutionSelector.Builder()
@@ -616,6 +641,9 @@
         // Sets the high resolution force disabled setting
         useCaseConfigBuilder.setHighResolutionDisabled(highResolutionForceDisabled)
 
+        // Sets the max resolution setting
+        maxResolution?.let { useCaseConfigBuilder.setMaxResolution(it) }
+
         return useCaseConfigBuilder.useCaseConfig
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index 00a4ae3..569a333 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -35,6 +35,7 @@
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+import androidx.camera.core.resolutionselector.ResolutionStrategy
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -583,6 +584,39 @@
         Truth.assertThat(surfaceFutureSemaphore!!.tryAcquire(10, TimeUnit.SECONDS)).isTrue()
     }
 
+    @Test
+    fun defaultMaxResolutionCanBeKept_whenResolutionStrategyIsNotSet() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        val useCase = Preview.Builder().build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context!!,
+            CameraSelector.DEFAULT_BACK_CAMERA, useCase
+        )
+        Truth.assertThat(
+            useCase.currentConfig.containsOption(
+                ImageOutputConfig.OPTION_MAX_RESOLUTION
+            )
+        ).isTrue()
+    }
+
+    @Test
+    fun defaultMaxResolutionCanBeRemoved_whenResolutionStrategyIsSet() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        val useCase = Preview.Builder().setResolutionSelector(
+            ResolutionSelector.Builder()
+                .setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build()
+        ).build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context!!,
+            CameraSelector.DEFAULT_BACK_CAMERA, useCase
+        )
+        Truth.assertThat(
+            useCase.currentConfig.containsOption(
+                ImageOutputConfig.OPTION_MAX_RESOLUTION
+            )
+        ).isFalse()
+    }
+
     private val workExecutorWithNamedThread: Executor
         get() {
             val threadFactory =
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ScrollDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ScrollDemos.kt
index edd9b9c..e3f0bfe 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ScrollDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ScrollDemos.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.demos.text.TagLine
+import androidx.compose.foundation.demos.text.fontSize8
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -29,12 +30,18 @@
 import androidx.compose.foundation.text2.input.TextFieldLineLimits.MultiLine
 import androidx.compose.foundation.text2.input.TextFieldLineLimits.SingleLine
 import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.material.Button
 import androidx.compose.material.Slider
+import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.coerceIn
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import kotlin.math.roundToInt
@@ -72,6 +79,11 @@
             TagLine(tag = "Shared Hoisted ScrollState")
             SharedHoistedScroll()
         }
+
+        item {
+            TagLine(tag = "Selectable with no interaction")
+            SelectionWithNoInteraction()
+        }
     }
 }
 
@@ -200,4 +212,47 @@
             lineLimits = SingleLine
         )
     }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun SelectionWithNoInteraction() {
+    val state =
+        remember { TextFieldState("Hello, World!", initialSelectionInChars = TextRange(1, 5)) }
+    val focusRequester = remember { FocusRequester() }
+    Column {
+        Button(onClick = { focusRequester.requestFocus() }) {
+            Text("Focus")
+        }
+        Button(onClick = {
+            state.edit {
+                selectCharsIn(
+                    TextRange(
+                        state.text.selectionInChars.start - 1,
+                        state.text.selectionInChars.end
+                    ).coerceIn(0, state.text.length)
+                )
+            }
+        }) {
+            Text("Increase Selection to Left")
+        }
+        Button(onClick = {
+            state.edit {
+                selectCharsIn(
+                    TextRange(
+                        state.text.selectionInChars.start,
+                        state.text.selectionInChars.end + 1
+                    ).coerceIn(0, state.text.length)
+                )
+            }
+        }) {
+            Text("Increase Selection to Right")
+        }
+        BasicTextField2(
+            state = state,
+            modifier = demoTextFieldModifiers.focusRequester(focusRequester),
+            textStyle = TextStyle(fontSize = fontSize8),
+            lineLimits = SingleLine
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index 7096695..2c1821a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -19,6 +19,8 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
@@ -73,6 +75,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.width
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -374,7 +377,9 @@
             Column {
                 BasicText(
                     AnnotatedString(longText),
-                    Modifier.fillMaxWidth().testTag(tag1),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
                     style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
                     maxLines = 1
                 )
@@ -398,7 +403,9 @@
             Column {
                 BasicText(
                     AnnotatedString(longText),
-                    Modifier.fillMaxWidth().testTag(tag1),
+                    Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
                     style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
                     maxLines = 1,
                     overflow = TextOverflow.Ellipsis
@@ -417,6 +424,78 @@
     }
 
     @Test
+    fun selectionIncludes_noHeightText() {
+        lateinit var clipboardManager: ClipboardManager
+        createSelectionContainer {
+            clipboardManager = LocalClipboardManager.current
+            clipboardManager.setText(AnnotatedString("Clipboard content at start of test."))
+            Column {
+                BasicText(
+                    text = "Hello",
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                )
+                BasicText(
+                    text = "THIS SHOULD NOT CAUSE CRASH",
+                    modifier = Modifier.height(0.dp)
+                )
+                BasicText(
+                    text = "World",
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(tag2),
+                )
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(
+            handle = Handle.SelectionEnd,
+            offset = characterBox(tag2, 4).bottomRight
+        )
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = 5, selectableId = 3)
+    }
+
+    @Test
+    fun selectionIncludes_noWidthText() {
+        lateinit var clipboardManager: ClipboardManager
+        createSelectionContainer {
+            clipboardManager = LocalClipboardManager.current
+            clipboardManager.setText(AnnotatedString("Clipboard content at start of test."))
+            Column {
+                BasicText(
+                    text = "Hello",
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                )
+                BasicText(
+                    text = "THIS SHOULD NOT CAUSE CRASH",
+                    modifier = Modifier.width(0.dp)
+                )
+                BasicText(
+                    text = "World",
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(tag2),
+                )
+            }
+        }
+
+        startSelection(tag1)
+        dragHandleTo(
+            handle = Handle.SelectionEnd,
+            offset = characterBox(tag2, 4).bottomRight
+        )
+
+        assertAnchorInfo(selection.value?.start, offset = 0, selectableId = 1)
+        assertAnchorInfo(selection.value?.end, offset = 5, selectableId = 3)
+    }
+
+    @Test
     @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
     fun selection_doesCopy_whenCopyKeyEventSent() {
         lateinit var clipboardManager: ClipboardManager
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index 3a4a843..1c512ca 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -59,3 +59,13 @@
             .isWithin(5f).of(expectedY.value)
     }
 }
+
+internal fun SemanticsNodeInteraction.assertHandleAnchorMatches(
+    anchor: SelectionHandleAnchor
+) {
+    val node = fetchSemanticsNode()
+    val actualAnchor = node.config[SelectionHandleInfoKey].anchor
+    val message = "Expected anchor ($anchor), " +
+        "but found ($actualAnchor)"
+    assertWithMessage(message).that(actualAnchor).isEqualTo(anchor)
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt
new file mode 100644
index 0000000..35850a5
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text2.selection
+
+import android.os.Build
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.Handle
+import androidx.compose.foundation.text.TEST_FONT_FAMILY
+import androidx.compose.foundation.text.selection.SelectionHandleAnchor
+import androidx.compose.foundation.text.selection.assertHandleAnchorMatches
+import androidx.compose.foundation.text.selection.assertHandlePositionMatches
+import androidx.compose.foundation.text.selection.isSelectionHandle
+import androidx.compose.foundation.text2.BasicTextField2
+import androidx.compose.foundation.text2.input.TextFieldLineLimits
+import androidx.compose.foundation.text2.input.TextFieldState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasSetTextAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+class TextFieldSelectionHandlesTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var state: TextFieldState
+
+    private val TAG = "BasicTextField2"
+
+    private val fontSize = 10.sp
+
+    @Test
+    fun selectionHandles_doNotShow_whenFieldNotFocused() {
+        state = TextFieldState("hello, world", initialSelectionInChars = TextRange(2, 5))
+        rule.setContent {
+            BasicTextField2(
+                state,
+                textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(100.dp)
+            )
+        }
+
+        assertHandlesNotExist()
+    }
+
+    @Test
+    fun selectionHandles_appears_whenFieldGetsFocused() {
+        state = TextFieldState("hello, world", initialSelectionInChars = TextRange(2, 5))
+        rule.setContent {
+            BasicTextField2(
+                state,
+                textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(100.dp)
+            )
+        }
+
+        focusAndWait()
+        assertHandlesDisplayed()
+    }
+
+    @Test
+    fun selectionHandles_disappear_whenFieldLosesFocus() {
+        state = TextFieldState("hello, world", initialSelectionInChars = TextRange(2, 5))
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            Column {
+                Box(
+                    Modifier
+                        .size(100.dp)
+                        .focusRequester(focusRequester)
+                        .focusable())
+                BasicTextField2(
+                    state,
+                    textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                    modifier = Modifier
+                        .testTag(TAG)
+                        .width(100.dp)
+                )
+            }
+        }
+
+        focusAndWait()
+        assertHandlesDisplayed()
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+        assertHandlesNotExist()
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun selectionHandles_locatedAtTheRightPosition_ltr_ltr() {
+        state = TextFieldState("hello, world", initialSelectionInChars = TextRange(2, 5))
+        rule.setContent {
+            BasicTextField2(
+                state,
+                textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(100.dp)
+            )
+        }
+
+        focusAndWait()
+
+        with(rule.onNode(isSelectionHandle(Handle.SelectionStart))) {
+            assertHandlePositionMatches(
+                (2 * fontSize.value).dp,
+                fontSize.value.dp
+            )
+            assertHandleAnchorMatches(SelectionHandleAnchor.Left)
+        }
+
+        with(rule.onNode(isSelectionHandle(Handle.SelectionEnd))) {
+            assertHandlePositionMatches(
+                (5 * fontSize.value).dp,
+                fontSize.value.dp
+            )
+            assertHandleAnchorMatches(SelectionHandleAnchor.Right)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun selectionHandles_locatedAtTheRightPosition_ltr_rtl() {
+        state = TextFieldState("abc \u05D0\u05D1\u05D2", initialSelectionInChars = TextRange(1, 6))
+        rule.setContent {
+            BasicTextField2(
+                state,
+                textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(100.dp)
+            )
+        }
+
+        focusAndWait()
+
+        with(rule.onNode(isSelectionHandle(Handle.SelectionStart))) {
+            assertHandlePositionMatches(
+                (1 * fontSize.value).dp,
+                fontSize.value.dp
+            )
+            assertHandleAnchorMatches(SelectionHandleAnchor.Left)
+        }
+
+        with(rule.onNode(isSelectionHandle(Handle.SelectionEnd))) {
+            assertHandlePositionMatches(
+                (5 * fontSize.value).dp,
+                fontSize.value.dp
+            )
+            assertHandleAnchorMatches(SelectionHandleAnchor.Left)
+        }
+    }
+
+    @Test
+    fun selectionHandlesDisappear_whenScrolledOutOfView_horizontally() {
+        // make it scrollable
+        state = TextFieldState("hello ".repeat(10), initialSelectionInChars = TextRange(1, 2))
+        rule.setContent {
+            BasicTextField2(
+                state,
+                textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                lineLimits = TextFieldLineLimits.SingleLine,
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(100.dp)
+            )
+        }
+
+        focusAndWait()
+        assertHandlesDisplayed()
+
+        rule.onNodeWithTag(TAG).performTouchInput { swipeLeft() }
+        assertHandlesNotExist()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+
+        rule.onNodeWithTag(TAG).performTouchInput { swipeRight() }
+        assertHandlesDisplayed()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+    }
+
+    @Test
+    fun selectionHandlesDisappear_whenScrolledOutOfView_vertically() {
+        // make it scrollable
+        state = TextFieldState("hello ".repeat(10), initialSelectionInChars = TextRange(1, 2))
+        rule.setContent {
+            BasicTextField2(
+                state,
+                textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 2),
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(100.dp)
+            )
+        }
+
+        focusAndWait()
+        assertHandlesDisplayed()
+
+        rule.onNodeWithTag(TAG).performTouchInput {
+            swipeUp()
+        }
+        assertHandlesNotExist()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+
+        rule.onNodeWithTag(TAG).performTouchInput {
+            swipeDown()
+        }
+        assertHandlesDisplayed()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+    }
+
+    @Test
+    fun selectionHandlesDisappear_whenScrolledOutOfView_horizontally_inContainer() {
+        // make it scrollable
+        val containerTag = "container"
+        state = TextFieldState("hello", initialSelectionInChars = TextRange(1, 2))
+        rule.setContent {
+            Row(modifier = Modifier
+                .width(200.dp)
+                .horizontalScroll(rememberScrollState())
+                .testTag(containerTag)
+            ) {
+                BasicTextField2(
+                    state,
+                    textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                    modifier = Modifier
+                        .testTag(TAG)
+                        .width(100.dp)
+                )
+                Box(modifier = Modifier
+                    .height(12.dp)
+                    .width(400.dp))
+            }
+        }
+
+        focusAndWait()
+        assertHandlesDisplayed()
+
+        rule.onNodeWithTag(containerTag).performTouchInput {
+            swipeLeft()
+        }
+        assertHandlesNotExist()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+
+        rule.onNodeWithTag(containerTag).performTouchInput {
+            swipeRight()
+        }
+        assertHandlesDisplayed()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+    }
+
+    @Test
+    fun selectionHandlesDisappear_whenScrolledOutOfView_vertically_inContainer() {
+        // make it scrollable
+        val containerTag = "container"
+        state = TextFieldState("hello", initialSelectionInChars = TextRange(1, 2))
+        rule.setContent {
+            Column(modifier = Modifier
+                .height(200.dp)
+                .verticalScroll(rememberScrollState())
+                .testTag(containerTag)
+            ) {
+                BasicTextField2(
+                    state,
+                    textStyle = TextStyle(fontSize = fontSize, fontFamily = TEST_FONT_FAMILY),
+                    modifier = Modifier
+                        .testTag(TAG)
+                        .height(100.dp)
+                )
+                Box(modifier = Modifier
+                    .width(12.dp)
+                    .height(400.dp))
+            }
+        }
+
+        focusAndWait()
+        assertHandlesDisplayed()
+
+        rule.onNodeWithTag(containerTag).performTouchInput {
+            swipeUp()
+        }
+        assertHandlesNotExist()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+
+        rule.onNodeWithTag(containerTag).performTouchInput {
+            swipeDown()
+        }
+        assertHandlesDisplayed()
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(1, 2))
+        }
+    }
+
+    private fun focusAndWait() {
+        rule.onNode(hasSetTextAction()).performSemanticsAction(SemanticsActions.RequestFocus)
+    }
+
+    private fun assertHandlesDisplayed(
+        assertStartHandle: Boolean = true,
+        assertEndHandle: Boolean = true
+    ) {
+        if (assertStartHandle) {
+            rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
+        }
+        if (assertEndHandle) {
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
+        }
+    }
+
+    private fun assertHandlesNotExist(
+        assertStartHandle: Boolean = true,
+        assertEndHandle: Boolean = true
+    ) {
+        if (assertStartHandle) {
+            rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertDoesNotExist()
+        }
+        if (assertEndHandle) {
+            rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertDoesNotExist()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
index 70000af..b4f0669 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
@@ -78,7 +78,12 @@
                             } else {
                                 Handle.SelectionEnd
                             },
-                            position = position
+                            position = position,
+                            anchor = if (isLeft) {
+                                SelectionHandleAnchor.Left
+                            } else {
+                                SelectionHandleAnchor.Right
+                            }
                         )
                     },
                 isStartHandle = isStartHandle,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 2967ba6..02a4fdb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.foundation.text.selection.SelectionHandleAnchor
 import androidx.compose.foundation.text.selection.SelectionHandleInfo
 import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
 import androidx.compose.foundation.text.selection.SimpleLayout
@@ -1104,7 +1105,8 @@
                 .semantics {
                     this[SelectionHandleInfoKey] = SelectionHandleInfo(
                         handle = Handle.Cursor,
-                        position = position
+                        position = position,
+                        anchor = SelectionHandleAnchor.Middle
                     )
                 },
             content = null
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index 3791520..3061137 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -55,8 +55,11 @@
                         // if final visible line's top is equal to or larger than text layout
                         // result's height, we need to check above lines one by one until we find
                         // a line that fits in boundaries.
-                        while (getLineTop(finalVisibleLine) >= size.height) finalVisibleLine--
-                        finalVisibleLine
+                        while (
+                            finalVisibleLine >= 0 &&
+                            getLineTop(finalVisibleLine) >= size.height
+                        ) finalVisibleLine--
+                        finalVisibleLine.coerceAtLeast(0)
                     }
                 }
                 _previousLastVisibleOffset = getLineEnd(lastVisibleLine, true)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
index 97a367d..348f37e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.kt
@@ -44,9 +44,23 @@
  */
 internal data class SelectionHandleInfo(
     val handle: Handle,
-    val position: Offset
+    val position: Offset,
+    val anchor: SelectionHandleAnchor
 )
 
+/**
+ * How the selection handle is anchored to its position
+ *
+ * In a regular text selection, selection start is anchored to left.
+ * Only cursor handle is always anchored at the middle.
+ * In a regular text selection, selection end is anchored to right.
+ */
+internal enum class SelectionHandleAnchor {
+    Left,
+    Middle,
+    Right
+}
+
 @Composable
 internal expect fun SelectionHandle(
     position: Offset,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
index 40c2d5e..d609cf2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
@@ -32,6 +32,8 @@
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.text.heightInLines
+import androidx.compose.foundation.text.selection.SelectionHandle
+import androidx.compose.foundation.text.selection.SelectionHandleAnchor
 import androidx.compose.foundation.text.selection.SelectionHandleInfo
 import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
 import androidx.compose.foundation.text.textFieldMinSize
@@ -256,10 +258,16 @@
                     )
                 )
 
-                if (enabled && isFocused && !readOnly && textFieldSelectionState.isInTouchMode) {
-                    TextFieldCursorHandle(
+                if (enabled && isFocused && textFieldSelectionState.isInTouchMode) {
+                    TextFieldSelectionHandles(
+                        textFieldState = state,
                         selectionState = textFieldSelectionState
                     )
+                    if (!readOnly) {
+                        TextFieldCursorHandle(
+                            selectionState = textFieldSelectionState
+                        )
+                    }
                 }
             }
         }
@@ -268,14 +276,16 @@
 
 @Composable
 internal fun TextFieldCursorHandle(selectionState: TextFieldSelectionState) {
-    if (selectionState.cursorHandleVisible) {
+    val cursorHandleState = selectionState.cursorHandle
+    if (cursorHandleState.visible) {
         CursorHandle(
-            handlePosition = selectionState.cursorRect.bottomCenter,
+            handlePosition = cursorHandleState.position,
             modifier = Modifier
                 .semantics {
                     this[SelectionHandleInfoKey] = SelectionHandleInfo(
                         handle = Handle.Cursor,
-                        position = selectionState.cursorRect.bottomCenter
+                        position = cursorHandleState.position,
+                        anchor = SelectionHandleAnchor.Middle
                     )
                 }
                 .pointerInput(selectionState) {
@@ -284,4 +294,35 @@
             content = null
         )
     }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun TextFieldSelectionHandles(
+    textFieldState: TextFieldState,
+    selectionState: TextFieldSelectionState
+) {
+    val startHandleState = selectionState.startSelectionHandle
+    if (startHandleState.visible) {
+        SelectionHandle(
+            position = startHandleState.position,
+            isStartHandle = true,
+            direction = startHandleState.direction,
+            handlesCrossed = textFieldState.text.selectionInChars.reversed,
+            modifier = Modifier,
+            content = null
+        )
+    }
+
+    val endHandleState = selectionState.endSelectionHandle
+    if (endHandleState.visible) {
+        SelectionHandle(
+            position = endHandleState.position,
+            isStartHandle = false,
+            direction = endHandleState.direction,
+            handlesCrossed = textFieldState.text.selectionInChars.reversed,
+            modifier = Modifier,
+            content = null
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldHandleState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldHandleState.kt
new file mode 100644
index 0000000..b643c0d
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldHandleState.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text2.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.text.style.ResolvedTextDirection
+
+/**
+ * Defines how to render a selection or cursor handle on a TextField.
+ */
+internal data class TextFieldHandleState(
+    val visible: Boolean,
+    val position: Offset,
+    val direction: ResolvedTextDirection
+) {
+    companion object {
+        val Hidden = TextFieldHandleState(
+            visible = false,
+            position = Offset.Unspecified,
+            direction = ResolvedTextDirection.Ltr
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt
index 74589cc..de3fc19 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt
@@ -47,6 +47,7 @@
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.TextToolbarStatus
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -87,17 +88,9 @@
     var isInTouchMode: Boolean by mutableStateOf(true)
 
     /**
-     * The gesture detector state, to indicate whether to show the appropriate handles for current
-     * selection or just the cursor.
-     *
-     * In the false state, no selection or cursor handle is shown, only the cursor is shown.
-     * TextField is initially in this state. To enter this state, input anything from the
-     * keyboard and modify the text.
-     *
-     * In the true state, either selection or cursor handle is shown according to current selection
-     * state of the TextField.
+     * Whether to show the cursor handle below cursor indicator when the TextField is focused.
      */
-    var showHandles by mutableStateOf(false)
+    var showCursorHandle by mutableStateOf(false)
 
     /**
      * Whether cursor handle is currently being dragged.
@@ -111,15 +104,14 @@
     private var textToolbarVisible by mutableStateOf(false)
 
     /**
-     * True if the position of the cursor is within a visible part of the window (i.e. not scrolled
-     * out of view) and the handle should be drawn.
+     * State of the cursor handle that includes its visibility and position.
      */
-    val cursorHandleVisible: Boolean by derivedStateOf {
-        val existsCondition = showHandles && textFieldState.text.selectionInChars.collapsed
-        if (!existsCondition) return@derivedStateOf false
+    val cursorHandle by derivedStateOf {
+        if (!showCursorHandle || !textFieldState.text.selectionInChars.collapsed)
+            return@derivedStateOf TextFieldHandleState.Hidden
 
         // either cursor is dragging or inside visible bounds.
-        return@derivedStateOf isCursorDragging ||
+        val visible = isCursorDragging ||
             textLayoutState.innerTextFieldCoordinates
                 ?.visibleBounds()
                 // Visibility of cursor handle should only be decided by changes to showHandles and
@@ -127,6 +119,15 @@
                 // handle may start flickering while moving and scrolling the text field.
                 ?.containsInclusive(Snapshot.withoutReadObservation { cursorRect.bottomCenter })
             ?: false
+
+        if (!visible) return@derivedStateOf TextFieldHandleState.Hidden
+
+        // text direction is useless for cursor handle, any value is fine.
+        TextFieldHandleState(
+            visible = true,
+            position = cursorRect.bottomCenter,
+            direction = ResolvedTextDirection.Ltr
+        )
     }
 
     /**
@@ -166,12 +167,57 @@
         )
     }
 
+    val startSelectionHandle by derivedStateOf {
+        val layoutResult = textLayoutState.layoutResult
+            ?: return@derivedStateOf TextFieldHandleState.Hidden
+
+        val selection = textFieldState.text.selectionInChars
+
+        if (selection.collapsed) return@derivedStateOf TextFieldHandleState.Hidden
+
+        var position = getHandlePosition(true)
+
+        val visible = textLayoutState.innerTextFieldCoordinates
+                ?.visibleBounds()
+                // Visibility of cursor handle should only be decided by changes to showHandles and
+                // innerTextFieldCoordinates. If we also react to position changes of cursor, cursor
+                // handle may start flickering while moving and scrolling the text field.
+                ?.containsInclusive(position)
+            ?: false
+
+        val direction = layoutResult.getBidiRunDirection(selection.start)
+        TextFieldHandleState(visible, position, direction)
+    }
+
+    val endSelectionHandle by derivedStateOf {
+        val layoutResult = textLayoutState.layoutResult
+            ?: return@derivedStateOf TextFieldHandleState.Hidden
+
+        val selection = textFieldState.text.selectionInChars
+
+        if (selection.collapsed)
+            return@derivedStateOf TextFieldHandleState.Hidden
+
+        var position = getHandlePosition(false)
+
+        val visible = textLayoutState.innerTextFieldCoordinates
+                ?.visibleBounds()
+                // Visibility of cursor handle should only be decided by changes to showHandles and
+                // innerTextFieldCoordinates. If we also react to position changes of cursor, cursor
+                // handle may start flickering while moving and scrolling the text field.
+                ?.containsInclusive(position)
+            ?: false
+
+        val direction = layoutResult.getBidiRunDirection(max(selection.end - 1, 0))
+        TextFieldHandleState(visible, position, direction)
+    }
+
     /**
      * Responsible for responding to tap events on TextField.
      */
     fun onTapTextField(offset: Offset) {
         if (textFieldState.text.isNotEmpty()) {
-            showHandles = true
+            showCursorHandle = true
         }
 
         textToolbarVisible = false
@@ -228,7 +274,7 @@
                 launch { observeTextToolbarVisibility() }
             }
         } finally {
-            showHandles = false
+            showCursorHandle = false
             if (textToolbarVisible) {
                 hideTextToolbar()
             }
@@ -315,7 +361,7 @@
             // first value needs to be dropped because it cannot be compared to a prior value
             .drop(1)
             .collect {
-                showHandles = false
+                showCursorHandle = false
                 textToolbarVisible = false
             }
     }
@@ -457,7 +503,7 @@
             selectCharsIn(TextRange(selection.min + clipboardText.length))
         }
 
-        showHandles = false
+        showCursorHandle = false
         // TODO(halilibo): undoManager force snapshot
     }
 
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuite.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuite.kt
index f9d5b6e..c631111 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuite.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuite.kt
@@ -20,14 +20,24 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.DrawerDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarDefaults
 import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemColors
+import androidx.compose.material3.NavigationBarItemDefaults
 import androidx.compose.material3.NavigationDrawerItem
+import androidx.compose.material3.NavigationDrawerItemColors
+import androidx.compose.material3.NavigationDrawerItemDefaults
 import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailDefaults
 import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.NavigationRailItemColors
+import androidx.compose.material3.NavigationRailItemDefaults
 import androidx.compose.material3.PermanentDrawerSheet
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
@@ -60,7 +70,7 @@
  * @param containerColor the color used for the background of the navigation suite. Use
  * [Color.Transparent] to have no color
  * @param contentColor the preferred color for content inside the navigation suite. Defaults to
- * either the matching content color for [containerColor], or to the current LocalContentColor if
+ * either the matching content color for [containerColor], or to the current [LocalContentColor] if
  * [containerColor] is not a color from the theme
  * @param content the content of your screen
  *
@@ -176,6 +186,9 @@
  *
  * @param adaptiveInfo the current [WindowAdaptiveInfo] of the [NavigationSuite]
  * @param modifier the [Modifier] to be applied to the navigation component
+ * @params colors [NavigationSuiteColors] that will be used to determine the container (background)
+ * color of the navigation component and the preferred color for content inside the navigation
+ * component
  * @param layoutTypeProvider the current [NavigationLayoutTypeProvider] of the [NavigationSuite]
  * @param content the content inside the current navigation component, typically
  * [navigationSuiteItem]s
@@ -188,14 +201,18 @@
     adaptiveInfo: WindowAdaptiveInfo,
     modifier: Modifier = Modifier,
     layoutTypeProvider: NavigationLayoutTypeProvider = NavigationSuiteDefaults.layoutTypeProvider,
-    // TODO: Add container and content color params.
+    colors: NavigationSuiteColors = NavigationSuiteDefaults.colors(),
     content: NavigationSuiteComponentScope.() -> Unit
 ) {
     val scope by rememberStateOfItems(content)
 
     when (layoutTypeProvider.calculateFromAdaptiveInfo(adaptiveInfo)) {
         NavigationLayoutType.NavigationBar -> {
-            NavigationBar(modifier = modifier) {
+            NavigationBar(
+                modifier = modifier,
+                containerColor = colors.navigationBarContainerColor,
+                contentColor = colors.navigationBarContentColor
+            ) {
                 scope.itemList.forEach {
                     NavigationBarItem(
                         modifier = it.modifier,
@@ -205,14 +222,20 @@
                         enabled = it.enabled,
                         label = it.label,
                         alwaysShowLabel = it.alwaysShowLabel,
-                        interactionSource = it.interactionSource()
+                        colors = it.colors?.navigationBarItemColors
+                            ?: NavigationBarItemDefaults.colors(),
+                        interactionSource = it.interactionSource
                     )
                 }
             }
         }
 
         NavigationLayoutType.NavigationRail -> {
-            NavigationRail(modifier = modifier) {
+            NavigationRail(
+                modifier = modifier,
+                containerColor = colors.navigationRailContainerColor,
+                contentColor = colors.navigationRailContentColor
+            ) {
                 scope.itemList.forEach {
                     NavigationRailItem(
                         modifier = it.modifier,
@@ -222,14 +245,20 @@
                         enabled = it.enabled,
                         label = it.label,
                         alwaysShowLabel = it.alwaysShowLabel,
-                        interactionSource = it.interactionSource()
+                        colors = it.colors?.navigationRailItemColors
+                            ?: NavigationRailItemDefaults.colors(),
+                        interactionSource = it.interactionSource
                     )
                 }
             }
         }
 
         NavigationLayoutType.NavigationDrawer -> {
-            PermanentDrawerSheet(modifier = modifier) {
+            PermanentDrawerSheet(
+                modifier = modifier,
+                drawerContainerColor = colors.navigationDrawerContainerColor,
+                drawerContentColor = colors.navigationDrawerContentColor
+            ) {
                 scope.itemList.forEach {
                     NavigationDrawerItem(
                         modifier = it.modifier,
@@ -238,7 +267,9 @@
                         icon = it.icon,
                         badge = it.badge,
                         label = { it.label?.invoke() ?: Text("") },
-                        interactionSource = it.interactionSource()
+                        colors = it.colors?.navigationDrawerItemColors
+                            ?: NavigationDrawerItemDefaults.colors(),
+                        interactionSource = it.interactionSource
                     )
                 }
             }
@@ -364,8 +395,8 @@
         label: @Composable (() -> Unit)?,
         alwaysShowLabel: Boolean,
         badge: (@Composable () -> Unit)?,
-        // TODO: Add colors params.
-        interactionSource: MutableInteractionSource?
+        colors: NavigationSuiteItemColors?,
+        interactionSource: MutableInteractionSource
     )
 }
 
@@ -388,6 +419,8 @@
  * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
  * only be shown when this item is selected. Note: for [NavigationDrawerItem] this is always `true`
  * @param badge optional badge to show on this item
+ * @param colors [NavigationSuiteItemColors] that will be used to resolve the colors used for this
+ * item in different states.
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this item. You can create and pass in your own `remember`ed instance to observe
  * [Interaction]s and customize the appearance / behavior of this item in different states
@@ -403,8 +436,8 @@
     label: @Composable (() -> Unit)? = null,
     alwaysShowLabel: Boolean = true,
     badge: (@Composable () -> Unit)? = null,
-    // TODO: Add colors params.
-    interactionSource: MutableInteractionSource? = null
+    colors: NavigationSuiteItemColors? = null,
+    interactionSource: MutableInteractionSource = MutableInteractionSource()
 ) {
     item(
         selected = selected,
@@ -415,6 +448,7 @@
         label = label,
         badge = badge,
         alwaysShowLabel = alwaysShowLabel,
+        colors = colors,
         interactionSource = interactionSource
     )
 }
@@ -442,23 +476,111 @@
  *
  * TODO: Remove "internal".
  */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 internal object NavigationSuiteDefaults {
 
     /** The default implementation of the [NavigationLayoutTypeProvider]. */
-    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
     val layoutTypeProvider = NavigationLayoutTypeProvider { adaptiveInfo ->
         with(adaptiveInfo) {
-                if (posture.isTabletop || windowSizeClass.heightSizeClass == Compact) {
-                    NavigationLayoutType.NavigationBar
-                } else if (windowSizeClass.widthSizeClass == Expanded) {
-                    NavigationLayoutType.NavigationRail
-                } else {
-                    NavigationLayoutType.NavigationBar
-                }
+            if (posture.isTabletop || windowSizeClass.heightSizeClass == Compact) {
+                NavigationLayoutType.NavigationBar
+            } else if (windowSizeClass.widthSizeClass == Expanded) {
+                NavigationLayoutType.NavigationRail
+            } else {
+                NavigationLayoutType.NavigationBar
             }
+        }
     }
+
+    /**
+     * Creates a [NavigationSuiteColors] with the provided colors for the container color, according
+     * to the Material specification.
+     *
+     * Use [Color.Transparent] for the navigation*ContainerColor to have no color. The
+     * navigation*ContentColor will default to either the matching content color for
+     * navigation*ContainerColor, or to the current [LocalContentColor] if navigation*ContainerColor
+     * is not a color from the theme.
+     *
+     * @param navigationBarContainerColor the default container color for the [NavigationBar]
+     * @param navigationBarContentColor the default content color for the [NavigationBar]
+     * @param navigationRailContainerColor the default container color for the [NavigationRail]
+     * @param navigationRailContentColor the default content color for the [NavigationRail]
+     * @param navigationDrawerContainerColor the default container color for the
+     * [PermanentDrawerSheet]
+     * @param navigationDrawerContentColor the default content color for the [PermanentDrawerSheet]
+     */
+    @Composable
+    fun colors(
+        navigationBarContainerColor: Color = NavigationBarDefaults.containerColor,
+        navigationBarContentColor: Color = contentColorFor(navigationBarContainerColor),
+        navigationRailContainerColor: Color = NavigationRailDefaults.ContainerColor,
+        navigationRailContentColor: Color = contentColorFor(navigationRailContainerColor),
+        navigationDrawerContainerColor: Color = DrawerDefaults.containerColor,
+        navigationDrawerContentColor: Color = contentColorFor(navigationDrawerContainerColor),
+    ): NavigationSuiteColors =
+        NavigationSuiteColors(
+            navigationBarContainerColor = navigationBarContainerColor,
+            navigationBarContentColor = navigationBarContentColor,
+            navigationRailContainerColor = navigationRailContainerColor,
+            navigationRailContentColor = navigationRailContentColor,
+            navigationDrawerContainerColor = navigationDrawerContainerColor,
+            navigationDrawerContentColor = navigationDrawerContentColor
+        )
 }
 
+/**
+ * Represents the colors of a [NavigationSuiteComponent].
+ *
+ * For specifics about each navigation component colors see [NavigationBarDefaults],
+ * [NavigationRailDefaults], and [DrawerDefaults].
+ *
+ * @param navigationBarContainerColor the container color for the [NavigationBar] of the
+ * [NavigationSuiteComponent]
+ * @param navigationBarContentColor the content color for the [NavigationBar] of the
+ * [NavigationSuiteComponent]
+ * @param navigationRailContainerColor the container color for the [NavigationRail] of the
+ * [NavigationSuiteComponent]
+ * @param navigationRailContentColor the content color for the [NavigationRail] of the
+ * [NavigationSuiteComponent]
+ * @param navigationDrawerContainerColor the container color for the [PermanentDrawerSheet] of the
+ * [NavigationSuiteComponent]
+ * @param navigationDrawerContentColor the content color for the [PermanentDrawerSheet] of the
+ * [NavigationSuiteComponent]
+ *
+ * TODO: Remove "internal".
+ */
+internal class NavigationSuiteColors
+internal constructor(
+    val navigationBarContainerColor: Color,
+    val navigationBarContentColor: Color,
+    val navigationRailContainerColor: Color,
+    val navigationRailContentColor: Color,
+    val navigationDrawerContainerColor: Color,
+    val navigationDrawerContentColor: Color
+)
+
+/**
+ * Represents the colors of a [navigationSuiteItem].
+ *
+ * For specifics about each navigation item colors see [NavigationBarItemColors],
+ * [NavigationRailItemColors], and [NavigationDrawerItemColors].
+ *
+ * @param navigationBarItemColors the [NavigationBarItemColors] associated with the
+ * [NavigationBarItem] of the [navigationSuiteItem]
+ * @param navigationRailItemColors the [NavigationRailItemColors] associated with the
+ * [NavigationRailItem] of the [navigationSuiteItem]
+ * @param navigationDrawerItemColors the [NavigationDrawerItemColors] associated with the
+ * [NavigationDrawerItem] of the [navigationSuiteItem]
+ *
+ * TODO: Remove "internal".
+ */
+internal class NavigationSuiteItemColors
+internal constructor(
+    val navigationBarItemColors: NavigationBarItemColors,
+    val navigationRailItemColors: NavigationRailItemColors,
+    val navigationDrawerItemColors: NavigationDrawerItemColors,
+)
+
 private interface NavigationSuiteItemProvider {
     val itemsCount: Int
     val itemList: MutableVector<NavigationSuiteItem>
@@ -473,15 +595,9 @@
     val label: @Composable (() -> Unit)?,
     val alwaysShowLabel: Boolean,
     val badge: (@Composable () -> Unit)?,
-    // TODO: Add colors params.
-    val interactionSource: MutableInteractionSource?
-) {
-
-    @Composable
-    fun interactionSource(): MutableInteractionSource {
-        return interactionSource ?: remember { MutableInteractionSource() }
-    }
-}
+    val colors: NavigationSuiteItemColors?,
+    val interactionSource: MutableInteractionSource
+)
 
 private class NavigationSuiteComponentScopeImpl : NavigationSuiteComponentScope,
     NavigationSuiteItemProvider {
@@ -495,20 +611,23 @@
         label: @Composable (() -> Unit)?,
         alwaysShowLabel: Boolean,
         badge: (@Composable () -> Unit)?,
-        // TODO: Add colors params.
-        interactionSource: MutableInteractionSource?
+        colors: NavigationSuiteItemColors?,
+        interactionSource: MutableInteractionSource
     ) {
-        itemList.add(NavigationSuiteItem(
-            selected = selected,
-            onClick = onClick,
-            icon = icon,
-            modifier = modifier,
-            enabled = enabled,
-            label = label,
-            alwaysShowLabel = alwaysShowLabel,
-            badge = badge,
-            interactionSource = interactionSource
-        ))
+        itemList.add(
+            NavigationSuiteItem(
+                selected = selected,
+                onClick = onClick,
+                icon = icon,
+                modifier = modifier,
+                enabled = enabled,
+                label = label,
+                alwaysShowLabel = alwaysShowLabel,
+                badge = badge,
+                colors = colors,
+                interactionSource = interactionSource
+            )
+        )
     }
 
     override val itemList: MutableVector<NavigationSuiteItem> = mutableVectorOf()
diff --git a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/Atomic.kt b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/Atomic.kt
index 5f17470..b883c4c 100644
--- a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/Atomic.kt
+++ b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/Atomic.kt
@@ -38,6 +38,6 @@
  *
  * @see withLock
  */
-internal expect class Synchronizer {
+internal expect class Synchronizer() {
     inline fun <T> withLock(crossinline block: () -> T): T
 }