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
}