Merge "Fix duplicate names androidx.room in various test classes." into androidx-main
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
index 7333e3e..1d866df 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
@@ -32,10 +32,11 @@
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
+import kotlin.random.Random;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
-import java.util.Random;
/**
* A registry that stores {@link ActivityResultCallback activity result callbacks} for
@@ -57,14 +58,11 @@
"KEY_COMPONENT_ACTIVITY_LAUNCHED_KEYS";
private static final String KEY_COMPONENT_ACTIVITY_PENDING_RESULTS =
"KEY_COMPONENT_ACTIVITY_PENDING_RESULT";
- private static final String KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT =
- "KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT";
private static final String LOG_TAG = "ActivityResultRegistry";
// Use upper 16 bits for request codes
private static final int INITIAL_REQUEST_CODE_VALUE = 0x00010000;
- private Random mRandom = new Random();
private final Map<Integer, String> mRcToKey = new HashMap<>();
final Map<String, Integer> mKeyToRc = new HashMap<>();
@@ -311,7 +309,6 @@
new ArrayList<>(mLaunchedKeys));
outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS,
(Bundle) mPendingResults.clone());
- outState.putSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT, mRandom);
}
/**
@@ -333,7 +330,6 @@
}
mLaunchedKeys =
savedInstanceState.getStringArrayList(KEY_COMPONENT_ACTIVITY_LAUNCHED_KEYS);
- mRandom = (Random) savedInstanceState.getSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT);
mPendingResults.putAll(
savedInstanceState.getBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS));
for (int i = 0; i < keys.size(); i++) {
@@ -442,10 +438,10 @@
* @return the number
*/
private int generateRandomNumber() {
- int number = mRandom.nextInt((Integer.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE) + 1)
+ int number = Random.Default.nextInt((Integer.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE) + 1)
+ INITIAL_REQUEST_CODE_VALUE;
while (mRcToKey.containsKey(number)) {
- number = mRandom.nextInt((Integer.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE) + 1)
+ number = Random.Default.nextInt((Integer.MAX_VALUE - INITIAL_REQUEST_CODE_VALUE) + 1)
+ INITIAL_REQUEST_CODE_VALUE;
}
return number;
diff --git a/annotation/annotation-experimental/api/current.txt b/annotation/annotation-experimental/api/current.txt
index cd79bbc..117beb3 100644
--- a/annotation/annotation-experimental/api/current.txt
+++ b/annotation/annotation-experimental/api/current.txt
@@ -24,7 +24,7 @@
@Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface Experimental {
method @Deprecated public abstract androidx.annotation.experimental.Experimental.Level level() default androidx.annotation.experimental.Experimental.Level.ERROR;
- property public abstract androidx.annotation.experimental.Experimental.Level level;
+ property @Deprecated public abstract androidx.annotation.experimental.Experimental.Level level;
}
@Deprecated public enum Experimental.Level {
@@ -36,7 +36,7 @@
@Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface UseExperimental {
method @Deprecated public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass();
- property public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass;
+ property @Deprecated public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass;
}
}
diff --git a/annotation/annotation-experimental/api/public_plus_experimental_current.txt b/annotation/annotation-experimental/api/public_plus_experimental_current.txt
index cd79bbc..117beb3 100644
--- a/annotation/annotation-experimental/api/public_plus_experimental_current.txt
+++ b/annotation/annotation-experimental/api/public_plus_experimental_current.txt
@@ -24,7 +24,7 @@
@Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface Experimental {
method @Deprecated public abstract androidx.annotation.experimental.Experimental.Level level() default androidx.annotation.experimental.Experimental.Level.ERROR;
- property public abstract androidx.annotation.experimental.Experimental.Level level;
+ property @Deprecated public abstract androidx.annotation.experimental.Experimental.Level level;
}
@Deprecated public enum Experimental.Level {
@@ -36,7 +36,7 @@
@Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface UseExperimental {
method @Deprecated public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass();
- property public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass;
+ property @Deprecated public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass;
}
}
diff --git a/annotation/annotation-experimental/api/restricted_current.txt b/annotation/annotation-experimental/api/restricted_current.txt
index cd79bbc..117beb3 100644
--- a/annotation/annotation-experimental/api/restricted_current.txt
+++ b/annotation/annotation-experimental/api/restricted_current.txt
@@ -24,7 +24,7 @@
@Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface Experimental {
method @Deprecated public abstract androidx.annotation.experimental.Experimental.Level level() default androidx.annotation.experimental.Experimental.Level.ERROR;
- property public abstract androidx.annotation.experimental.Experimental.Level level;
+ property @Deprecated public abstract androidx.annotation.experimental.Experimental.Level level;
}
@Deprecated public enum Experimental.Level {
@@ -36,7 +36,7 @@
@Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface UseExperimental {
method @Deprecated public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass();
- property public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass;
+ property @Deprecated public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass;
}
}
diff --git a/appactions/interaction/interaction-capabilities-communication/api/current.txt b/appactions/interaction/interaction-capabilities-communication/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-communication/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-communication/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-capabilities-communication/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-communication/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-communication/api/res-current.txt b/appactions/interaction/interaction-capabilities-communication/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-communication/api/res-current.txt
diff --git a/appactions/interaction/interaction-capabilities-communication/api/restricted_current.txt b/appactions/interaction/interaction-capabilities-communication/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-communication/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-communication/build.gradle b/appactions/interaction/interaction-capabilities-communication/build.gradle
new file mode 100644
index 0000000..d73076d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-communication/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ implementation("androidx.annotation:annotation:1.1.0")
+}
+
+android {
+ namespace "androidx.appactions.interaction.capabilities.communication"
+}
+
+androidx {
+ name = "androidx.appactions.interaction:interaction-capabilities-communication"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2023"
+ description = "Capability library for communication apps integrating with virtual assistant."
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/package-info.java b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/package-info.java
new file mode 100644
index 0000000..4c3216d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.appactions.interaction.capabilities.communication;
+
+import androidx.annotation.RestrictTo;
diff --git a/appactions/interaction/interaction-capabilities-core/build.gradle b/appactions/interaction/interaction-capabilities-core/build.gradle
index 6c5b2e1..258ef4f 100644
--- a/appactions/interaction/interaction-capabilities-core/build.gradle
+++ b/appactions/interaction/interaction-capabilities-core/build.gradle
@@ -29,7 +29,7 @@
api(libs.autoValueAnnotations)
implementation(libs.guavaListenableFuture)
- implementation(libs.kotlinCoroutinesGuava)
+ implementation(libs.kotlinCoroutinesCore)
implementation(libs.kotlinStdlib)
implementation("androidx.concurrent:concurrent-futures:1.1.0")
@@ -57,8 +57,7 @@
}
lintOptions {
- // TODO(b/266849030): Remove when updating library.
- disable("UnknownNullness", "SyntheticAccessor")
+ disable("UnknownNullness")
}
namespace "androidx.appactions.interaction.capabilities.core"
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt
new file mode 100644
index 0000000..186b31c
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.appactions.builtintypes.properties
+
+import java.time.LocalDate
+
+class EndDate(localDate: LocalDate) {
+ val localDate: LocalDate? = localDate
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt
new file mode 100644
index 0000000..8b94d93
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.appactions.builtintypes.properties
+
+import java.time.LocalDate
+
+class StartDate(localDate: LocalDate) {
+ val localDate: LocalDate? = localDate
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt
new file mode 100644
index 0000000..5afb6b6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.appactions.builtintypes.types
+
+import androidx.appactions.builtintypes.properties.Name
+
+interface Alarm : Thing {
+ override fun toBuilder(): Builder<*>
+
+ companion object {
+ @JvmStatic
+ fun Builder(): Builder<*> = AlarmBuilderImpl()
+ }
+
+ interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
+ override fun build(): Alarm
+ }
+}
+
+private class AlarmBuilderImpl : Alarm.Builder<AlarmBuilderImpl> {
+
+ private var identifier: String? = null
+ private var name: Name? = null
+
+ override fun build() = AlarmImpl(identifier, name)
+
+ override fun setIdentifier(text: String?): AlarmBuilderImpl = apply { identifier = text }
+
+ override fun setName(text: String): AlarmBuilderImpl = apply { name = Name(text) }
+
+ override fun setName(name: Name?): AlarmBuilderImpl = apply { this.name = name }
+
+ override fun clearName(): AlarmBuilderImpl = apply { name = null }
+}
+
+private class AlarmImpl(override val identifier: String?, override val name: Name?) : Alarm {
+ override fun toBuilder(): Alarm.Builder<*> =
+ AlarmBuilderImpl().setIdentifier(identifier).setName(name)
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/CalendarEvent.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/CalendarEvent.kt
new file mode 100644
index 0000000..abd0fe1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/CalendarEvent.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.appactions.builtintypes.types
+
+// TODO(b/271634410): Update Attendee references
+import androidx.appactions.builtintypes.properties.EndDate
+import androidx.appactions.builtintypes.properties.Name
+import androidx.appactions.builtintypes.properties.StartDate
+import androidx.appactions.interaction.capabilities.core.values.properties.Attendee
+import java.time.LocalDate
+
+interface CalendarEvent : Thing {
+ val startDate: StartDate?
+ val endDate: EndDate?
+ val attendeeList: List<Attendee>
+ override fun toBuilder(): Builder<*>
+
+ companion object {
+ @JvmStatic
+ fun Builder(): Builder<*> = CalendarEventBuilderImpl()
+ }
+
+ interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
+ fun setStartDate(startDate: StartDate?): Self
+ fun setStartDate(value: LocalDate): Self
+ fun setEndDate(endDate: EndDate?): Self
+ fun setEndDate(value: LocalDate): Self
+ fun addAttendee(attendee: Attendee): Self
+ fun addAllAttendee(value: Iterable<Attendee>): Self
+
+ override fun build(): CalendarEvent
+ }
+}
+
+private class CalendarEventBuilderImpl : CalendarEvent.Builder<CalendarEventBuilderImpl> {
+
+ private var identifier: String? = null
+ private var name: Name? = null
+ private var startDate: StartDate? = null
+ private var endDate: EndDate? = null
+ private var attendeeList = mutableListOf<Attendee>()
+
+ override fun build() = CalendarEventImpl(identifier, name, startDate, endDate, attendeeList)
+
+ override fun setStartDate(startDate: StartDate?): CalendarEventBuilderImpl = apply {
+ this.startDate = startDate
+ }
+
+ override fun setStartDate(value: LocalDate): CalendarEventBuilderImpl = apply {
+ startDate = StartDate(value)
+ }
+
+ override fun setEndDate(endDate: EndDate?): CalendarEventBuilderImpl = apply {
+ this.endDate = endDate
+ }
+
+ override fun setEndDate(value: LocalDate): CalendarEventBuilderImpl = apply {
+ endDate = EndDate(value)
+ }
+
+ override fun addAttendee(attendee: Attendee): CalendarEventBuilderImpl = apply {
+ attendeeList.add(attendee)
+ }
+
+ override fun addAllAttendee(value: Iterable<Attendee>): CalendarEventBuilderImpl = apply {
+ attendeeList.addAll(value)
+ }
+
+ override fun setIdentifier(text: String?): CalendarEventBuilderImpl =
+ apply { identifier = text }
+
+ override fun setName(text: String): CalendarEventBuilderImpl = apply { name = Name(text) }
+
+ override fun setName(name: Name?): CalendarEventBuilderImpl = apply { this.name = name }
+
+ override fun clearName(): CalendarEventBuilderImpl = apply { name = null }
+}
+
+private class CalendarEventImpl(
+ override val identifier: String?,
+ override val name: Name?,
+ override val startDate: StartDate?,
+ override val endDate: EndDate?,
+ override val attendeeList: List<Attendee>
+) :
+ CalendarEvent {
+ override fun toBuilder(): CalendarEvent.Builder<*> =
+ CalendarEventBuilderImpl()
+ .setIdentifier(identifier)
+ .setName(name)
+ .setStartDate(startDate)
+ .setEndDate(endDate)
+ .addAllAttendee(attendeeList)
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/SafetyCheck.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/SafetyCheck.kt
new file mode 100644
index 0000000..61c9d61
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/SafetyCheck.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.appactions.builtintypes.types
+
+import androidx.appactions.builtintypes.properties.Name
+import java.time.Duration
+import java.time.ZonedDateTime
+
+interface SafetyCheck : Thing {
+ val duration: Duration?
+ val checkInTime: ZonedDateTime?
+
+ override fun toBuilder(): Builder<*>
+
+ companion object {
+ @JvmStatic
+ fun Builder(): Builder<*> = SafetyCheckBuilderImpl()
+ }
+
+ interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
+ fun setDuration(duration: Duration?): Self
+ fun setCheckInTime(checkInTime: ZonedDateTime?): Self
+ override fun build(): SafetyCheck
+ }
+}
+
+private class SafetyCheckBuilderImpl : SafetyCheck.Builder<SafetyCheckBuilderImpl> {
+
+ private var identifier: String? = null
+ private var name: Name? = null
+ private var duration: Duration? = null
+ private var checkInTime: ZonedDateTime? = null
+
+ override fun build(): SafetyCheck = SafetyCheckImpl(identifier, name, duration, checkInTime)
+
+ override fun setDuration(duration: Duration?): SafetyCheckBuilderImpl =
+ apply { this.duration = duration }
+
+ override fun setCheckInTime(checkInTime: ZonedDateTime?): SafetyCheckBuilderImpl =
+ apply { this.checkInTime = checkInTime }
+
+ override fun setIdentifier(text: String?): SafetyCheckBuilderImpl = apply { identifier = text }
+
+ override fun setName(text: String): SafetyCheckBuilderImpl = apply { name = Name(text) }
+
+ override fun setName(name: Name?): SafetyCheckBuilderImpl = apply { this.name = name }
+
+ override fun clearName(): SafetyCheckBuilderImpl = apply { name = null }
+}
+
+private class SafetyCheckImpl(
+ override val identifier: String?,
+ override val name: Name?,
+ override val duration: Duration?,
+ override val checkInTime: ZonedDateTime?
+) : SafetyCheck {
+ override fun toBuilder(): SafetyCheck.Builder<*> =
+ SafetyCheckBuilderImpl()
+ .setIdentifier(identifier)
+ .setName(name)
+ .setDuration(duration)
+ .setCheckInTime(checkInTime)
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Thing.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Thing.kt
index 4f0ecb3..d9f957a 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Thing.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Thing.kt
@@ -26,7 +26,7 @@
companion object {
@JvmStatic
- fun builder(): Builder<*> = ThingBuilderImpl()
+ fun Builder(): Builder<*> = ThingBuilderImpl()
}
@Suppress("StaticFinalBuilder")
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Timer.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Timer.kt
new file mode 100644
index 0000000..5a25e18
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/builtintypes/types/Timer.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.appactions.builtintypes.types
+
+import androidx.appactions.builtintypes.properties.Name
+
+interface Timer : Thing {
+ override fun toBuilder(): Builder<*>
+
+ companion object {
+ @JvmStatic
+ fun Builder(): Builder<*> = TimerBuilderImpl()
+ }
+
+ interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
+ override fun build(): Timer
+ }
+}
+
+private class TimerBuilderImpl : Timer.Builder<TimerBuilderImpl> {
+
+ private var identifier: String? = null
+ private var name: Name? = null
+
+ override fun build() = TimerImpl(identifier, name)
+
+ override fun setIdentifier(text: String?): TimerBuilderImpl = apply { identifier = text }
+
+ override fun setName(text: String): TimerBuilderImpl = apply { name = Name(text) }
+
+ override fun setName(name: Name?): TimerBuilderImpl = apply { this.name = name }
+
+ override fun clearName(): TimerBuilderImpl = apply { name = null }
+}
+
+private class TimerImpl(override val identifier: String?, override val name: Name?) : Timer {
+ override fun toBuilder(): Timer.Builder<*> =
+ TimerBuilderImpl().setIdentifier(identifier).setName(name)
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt
index f941f5f..42bd9f4 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt
@@ -16,30 +16,18 @@
package androidx.appactions.interaction.capabilities.core
-import androidx.appactions.interaction.capabilities.core.impl.concurrent.ListenableFutureHelper
-import com.google.common.util.concurrent.ListenableFuture
-
/**
* An interface of executing the action.
+ *
+ * Actions are executed asynchronously using Kotlin coroutines.
+ * For a Future-based solution, see ActionExecutorAsync.
*/
-interface ActionExecutor<ArgumentT, OutputT> {
+fun interface ActionExecutor<ArgumentT, OutputT> {
/**
* Calls to execute the action.
*
* @param argument the argument for this action.
* @return the ExecutionResult
*/
- suspend fun execute(argument: ArgumentT): ExecutionResult<OutputT> {
- throw NotImplementedError()
- }
-
- /**
- * Calls to execute the action.
- *
- * @param argument the argument for this action.
- * @return A ListenableFuture containing the ExecutionResult
- */
- fun executeAsync(argument: ArgumentT): ListenableFuture<ExecutionResult<OutputT>> {
- return ListenableFutureHelper.convertToListenableFuture { execute(argument) }
- }
+ suspend fun execute(argument: ArgumentT): ExecutionResult<OutputT>
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt
new file mode 100644
index 0000000..05ae182
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.appactions.interaction.capabilities.core
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.ListenableFutureHelper
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * An ListenableFuture-based interface of executing an action.
+ */
+fun interface ActionExecutorAsync<ArgumentT, OutputT> {
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ val uiHandle: Any
+ get() = this
+
+ /**
+ * Calls to execute the action.
+ *
+ * @param argument the argument for this action.
+ * @return A ListenableFuture containing the ExecutionResult
+ */
+ fun execute(argument: ArgumentT): ListenableFuture<ExecutionResult<OutputT>>
+
+ companion object {
+ fun <ArgumentT, OutputT> ActionExecutor<ArgumentT, OutputT>
+ .toActionExecutorAsync(): ActionExecutorAsync<ArgumentT, OutputT> =
+ object : ActionExecutorAsync<ArgumentT, OutputT> {
+ override val uiHandle = this@toActionExecutorAsync
+ override fun execute(
+ argument: ArgumentT,
+ ): ListenableFuture<ExecutionResult<OutputT>> =
+ ListenableFutureHelper.convertToListenableFuture("ActionExecutor#execute") {
+ this@toActionExecutorAsync.execute(argument)
+ }
+ }
+ }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/BaseSession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/BaseSession.kt
index e4b20fe..2152815 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/BaseSession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/BaseSession.kt
@@ -16,10 +16,8 @@
package androidx.appactions.interaction.capabilities.core
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.ListenableFutureHelper
import com.google.common.util.concurrent.ListenableFuture
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.guava.future
/**
* Base interface for Session of all verticals.
*/
@@ -45,9 +43,8 @@
* @param argument the Argument instance containing data for fulfillment.
* @return a ListenableFuture containing an ExecutionResult instance.
*/
- @kotlin.OptIn(DelicateCoroutinesApi::class)
fun onFinishAsync(argument: ArgumentT): ListenableFuture<ExecutionResult<OutputT>> {
- return GlobalScope.future { onFinish(argument) }
+ return ListenableFutureHelper.convertToListenableFuture("onFinish") { onFinish(argument) }
}
/**
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt
index 328a9e0..3ceb482 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt
@@ -16,13 +16,14 @@
package androidx.appactions.interaction.capabilities.core
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync.Companion.toActionExecutorAsync
import androidx.appactions.interaction.capabilities.core.impl.SingleTurnCapabilityImpl
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
-import androidx.appactions.interaction.capabilities.core.task.impl.TaskCapabilityImpl
import androidx.appactions.interaction.capabilities.core.task.impl.SessionBridge
+import androidx.appactions.interaction.capabilities.core.task.impl.TaskCapabilityImpl
import java.util.function.Supplier
-import androidx.annotation.RestrictTo
/**
* An abstract Builder class for ActionCapability.
@@ -48,8 +49,9 @@
) {
private var id: String? = null
private var property: PropertyT? = null
- private var actionExecutor: ActionExecutor<ArgumentT, OutputT>? = null
+ private var actionExecutorAsync: ActionExecutorAsync<ArgumentT, OutputT>? = null
private var sessionFactory: SessionFactory<SessionT>? = null
+
/**
* The SessionBridge object, which is used to normalize Session instances to TaskHandler.
* see SessionBridge documentation for more information.
@@ -58,6 +60,7 @@
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
protected open val sessionBridge: SessionBridge<SessionT, ConfirmationT>? = null
+
/** The supplier of SessionUpdaterT instances. */
protected open val sessionUpdaterSupplier: Supplier<SessionUpdaterT>? = null
@@ -85,17 +88,35 @@
/**
* Sets the ActionExecutor for this capability.
*
- * setSessionFactory and setActionExecutor are mutually exclusive, so calling one will nullify the other.
+ * setSessionFactory and setExecutor are mutually exclusive, so calling one will nullify
+ * the other.
+ *
+ * This method accepts a coroutine-based ActionExecutor instance. There is also an overload
+ * which accepts the ActionExecutorAsync instead.
*/
- fun setActionExecutor(actionExecutor: ActionExecutor<ArgumentT, OutputT>) = asBuilder().apply {
- this.actionExecutor = actionExecutor
+ fun setExecutor(actionExecutor: ActionExecutor<ArgumentT, OutputT>) = asBuilder().apply {
+ this.actionExecutorAsync = actionExecutor.toActionExecutorAsync()
}
/**
- * Sets the SessionFactory instance which is used to create Session instaces for this
+ * Sets the ActionExecutorAsync for this capability.
+ *
+ * setSessionFactory and setExecutor are mutually exclusive, so calling one will nullify
+ * the other.
+ *
+ * This method accepts the ActionExecutorAsync interface which returns a ListenableFuture.
+ */
+ fun setExecutor(
+ actionExecutorAsync: ActionExecutorAsync<ArgumentT, OutputT>,
+ ) = asBuilder().apply {
+ this.actionExecutorAsync = actionExecutorAsync
+ }
+
+ /**
+ * Sets the SessionBuilder instance which is used to create Session instaces for this
* capability.
*
- * setSessionFactory and setActionExecutor are mutually exclusive, so calling one will nullify the other.
+ * setSessionFactory and setExecutor are mutually exclusive, so calling one will nullify the other.
*/
protected open fun setSessionFactory(
sessionFactory: SessionFactory<SessionT>,
@@ -107,12 +128,12 @@
open fun build(): ActionCapability {
val checkedId = requireNotNull(id, { "setId must be called before build" })
val checkedProperty = requireNotNull(property, { "property must not be null." })
- if (actionExecutor != null) {
+ if (actionExecutorAsync != null) {
return SingleTurnCapabilityImpl(
checkedId,
actionSpec,
checkedProperty,
- actionExecutor!!,
+ actionExecutorAsync!!,
)
} else {
return TaskCapabilityImpl(
@@ -121,7 +142,7 @@
checkedProperty,
requireNotNull(
sessionFactory,
- { "either setActionExecutor or setSessionFactory must be called before build" },
+ { "either setExecutor or setSessionFactory must be called before build" },
),
sessionBridge!!,
sessionUpdaterSupplier!!,
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt
index 94714e3..3eece1d 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt
@@ -47,20 +47,22 @@
)
}
- private fun createRequestMetadata(fulfillment: Fulfillment): RequestMetadata? {
+ internal fun createRequestMetadata(fulfillment: Fulfillment): RequestMetadata? {
return if (
fulfillment.type == Fulfillment.Type.UNKNOWN_TYPE ||
- fulfillment.type == Fulfillment.Type.UNRECOGNIZED
+ fulfillment.type == Fulfillment.Type.UNRECOGNIZED
) {
null
- } else RequestMetadata.newBuilder().setRequestType(fulfillment.type).build()
+ } else {
+ RequestMetadata.newBuilder().setRequestType(fulfillment.type).build()
+ }
}
@Suppress(
- "DEPRECATION"
+ "DEPRECATION",
) // Convert the deprecated "fp.valuesList" property to the new format.
- private fun convertToArgumentMap(
- fulfillment: Fulfillment
+ internal fun convertToArgumentMap(
+ fulfillment: Fulfillment,
): Map<String, List<FulfillmentValue>> {
val result = mutableMapOf<String, List<FulfillmentValue>>()
for (fp in fulfillment.paramsList) {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt
index c60acba..80e4af5 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt
@@ -16,15 +16,14 @@
package androidx.appactions.interaction.capabilities.core.impl
+import androidx.annotation.RestrictTo
import androidx.appactions.interaction.capabilities.core.ActionCapability
-import androidx.appactions.interaction.capabilities.core.ActionExecutor
+import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
import androidx.appactions.interaction.capabilities.core.HostProperties
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.proto.AppActionsContext.AppAction
import androidx.appactions.interaction.proto.TaskInfo
-import androidx.annotation.RestrictTo
-
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class SingleTurnCapabilityImpl<
@@ -35,7 +34,7 @@
override val id: String,
val actionSpec: ActionSpec<PropertyT, ArgumentT, OutputT>,
val property: PropertyT,
- val actionExecutor: ActionExecutor<ArgumentT, OutputT>,
+ val actionExecutorAsync: ActionExecutorAsync<ArgumentT, OutputT>,
) : ActionCapability {
override val supportsMultiTurnTask = false
@@ -49,7 +48,7 @@
override fun createSession(hostProperties: HostProperties): ActionCapabilitySession {
return SingleTurnCapabilitySession(
actionSpec,
- actionExecutor,
+ actionExecutorAsync,
)
}
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
index 9509c68..a2174be 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
@@ -16,9 +16,8 @@
package androidx.appactions.interaction.capabilities.core.impl
-import androidx.annotation.NonNull
import androidx.annotation.RestrictTo
-import androidx.appactions.interaction.capabilities.core.ActionExecutor
+import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
import androidx.appactions.interaction.capabilities.core.ExecutionResult
import androidx.appactions.interaction.capabilities.core.impl.concurrent.FutureCallback
import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures
@@ -37,9 +36,9 @@
internal class SingleTurnCapabilitySession<
ArgumentT,
OutputT,
->(
+ >(
val actionSpec: ActionSpec<*, ArgumentT, OutputT>,
- val actionExecutor: ActionExecutor<ArgumentT, OutputT>,
+ val actionExecutorAsync: ActionExecutorAsync<ArgumentT, OutputT>,
) : ActionCapabilitySession {
override val state: AppAction
get() {
@@ -50,9 +49,9 @@
throw UnsupportedOperationException()
}
- override fun destroy() {}
+ override val uiHandle: Any = actionExecutorAsync.uiHandle
- override val uiHandle: Any = actionExecutor
+ override fun destroy() {}
// single-turn capability does not have touch events
override fun setTouchEventCallback(callback: TouchEventCallback) {
@@ -60,12 +59,12 @@
}
override fun execute(
- @NonNull argumentsWrapper: ArgumentsWrapper,
- @NonNull callback: CallbackInternal,
+ argumentsWrapper: ArgumentsWrapper,
+ callback: CallbackInternal,
) {
val paramValuesMap: Map<String, List<ParamValue>> =
argumentsWrapper.paramValues.entries.associate {
- entry: Map.Entry<String, List<FulfillmentValue>> ->
+ entry: Map.Entry<String, List<FulfillmentValue>> ->
Pair(
entry.key,
entry.value.mapNotNull { fulfillmentValue: FulfillmentValue ->
@@ -75,7 +74,7 @@
}
val argument = actionSpec.buildArgument(paramValuesMap)
Futures.addCallback(
- actionExecutor.executeAsync(argument),
+ actionExecutorAsync.execute(argument),
object : FutureCallback<ExecutionResult<OutputT>> {
override fun onSuccess(executionResult: ExecutionResult<OutputT>) {
callback.onSuccess(convertToFulfillmentResponse(executionResult))
@@ -90,7 +89,7 @@
}
/** Converts typed {@link ExecutionResult} to {@link FulfillmentResponse} proto. */
- private fun convertToFulfillmentResponse(
+ internal fun convertToFulfillmentResponse(
executionResult: ExecutionResult<OutputT>,
): FulfillmentResponse {
val fulfillmentResponseBuilder =
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt
index 0bb99a4..d76a476 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt
@@ -1,60 +1,35 @@
package androidx.appactions.interaction.capabilities.core.impl.concurrent
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
+import androidx.concurrent.futures.CallbackToFutureAdapter
import com.google.common.util.concurrent.ListenableFuture
-import com.google.common.util.concurrent.SettableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
// TODO(b/269525385): merge this into Futures utility class once it's migrated to Kotlin.
internal class ListenableFutureHelper {
- companion object {
- fun <T> convertToListenableFuture(suspendFunction: suspend () -> T): ListenableFuture<T> {
- val scope = CoroutineScope(Dispatchers.Default)
- val future = SettableFuture.create<T>()
-
- scope.launch {
- try {
- val result = suspendFunction()
- future.set(result)
- } catch (t: Throwable) {
- future.setException(t)
+ companion object {
+ fun <T> convertToListenableFuture(
+ tag: String,
+ block: suspend CoroutineScope.() -> T,
+ ): ListenableFuture<T> {
+ val scope = CoroutineScope(Dispatchers.Default)
+ return CallbackToFutureAdapter.getFuture {
+ completer ->
+ val job = scope.launch {
+ try {
+ completer.set(scope.block())
+ } catch (t: Throwable) {
+ completer.setException(t)
+ }
+ }
+ completer.addCancellationListener(
+ { job.cancel() },
+ Runnable::run,
+ )
+ "ListenableFutureHelper#convertToListenableFuture for '$tag'"
+ }
}
- }
-
- return object : ListenableFuture<T> {
- override fun addListener(listener: Runnable, executor: Executor) {
- future.addListener(listener, executor)
- }
-
- override fun isDone(): Boolean {
- return future.isDone
- }
-
- override fun get(): T {
- return future.get()
- }
-
- override fun get(timeout: Long, unit: TimeUnit): T {
- return future.get(timeout, unit)
- }
-
- override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
- return future.cancel(mayInterruptIfRunning)
- }
-
- override fun isCancelled(): Boolean {
- return future.isCancelled
- }
-
- // Add a method to explicitly close the scope
- fun close() {
- scope.cancel()
- }
- }
}
- }
-}
\ No newline at end of file
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
index 3b610ad..272395f 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
@@ -646,7 +646,7 @@
return builder.setStructValue(Struct.newBuilder().putAllFields(fieldsMap).build()).build();
}
- private static String getStructType(Struct struct) throws StructConversionException {
+ static String getStructType(Struct struct) throws StructConversionException {
Map<String, Value> fieldsMap = struct.getFieldsMap();
if (!fieldsMap.containsKey(FIELD_NAME_TYPE)
|| fieldsMap.get(FIELD_NAME_TYPE).getStringValue().isEmpty()) {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java
index 1c427d1..5fcc17e 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.java
@@ -95,11 +95,14 @@
StructuredOutput.Builder outputBuilder = StructuredOutput.newBuilder();
for (Map.Entry<String, Function<OutputT, List<ParamValue>>> entry :
mOutputBindings.entrySet()) {
- outputBuilder.addOutputValues(
- StructuredOutput.OutputValue.newBuilder()
- .setName(entry.getKey())
- .addAllValues(entry.getValue().apply(output))
- .build());
+ List<ParamValue> values = entry.getValue().apply(output);
+ if (!values.isEmpty()) {
+ outputBuilder.addOutputValues(
+ StructuredOutput.OutputValue.newBuilder()
+ .setName(entry.getKey())
+ .addAllValues(values)
+ .build());
+ }
}
return outputBuilder.build();
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java
index 45842ba..d1388ad 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityListResolver.java
@@ -17,6 +17,7 @@
package androidx.appactions.interaction.capabilities.core.task;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.appactions.interaction.capabilities.core.values.SearchAction;
import com.google.common.util.concurrent.ListenableFuture;
@@ -27,8 +28,10 @@
* Similar to ValueListener, but also need to handle grounding of ungrounded values.
*
* @param <T>
+ * @hide
*/
-public interface AppEntityListResolver<T> extends ValueListenerAsync<List<T>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface AppEntityListResolver<T> extends ValueListener<List<T>> {
/**
* Given a search criteria, looks up the inventory during runtime, renders the search result
* within the app's own UI and then returns it to the Assistant so that the task can be kept in
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java
index c366e55..cda8917c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/AppEntityResolver.java
@@ -17,6 +17,7 @@
package androidx.appactions.interaction.capabilities.core.task;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.appactions.interaction.capabilities.core.values.SearchAction;
import com.google.common.util.concurrent.ListenableFuture;
@@ -25,8 +26,10 @@
* Similar to ValueListener, but also need to handle grounding of ungrounded values.
*
* @param <T>
+ * @hide
*/
-public interface AppEntityResolver<T> extends ValueListenerAsync<T> {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface AppEntityResolver<T> extends ValueListener<T> {
/**
* Given a search criteria, looks up the inventory during runtime, renders the search result
* within the app's own UI and then returns it to the Assistant so that the task can be kept in
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java
index a47893c..7b346381 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryListResolver.java
@@ -17,6 +17,7 @@
package androidx.appactions.interaction.capabilities.core.task;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
@@ -26,8 +27,10 @@
* Repeated version of {@code InventoryResolver}.
*
* @param <T>
+ * @hide
*/
-public interface InventoryListResolver<T> extends ValueListenerAsync<List<T>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface InventoryListResolver<T> extends ValueListener<List<T>> {
/**
* Renders the provided entities in the app UI for dismabiguation.
*
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java
index d2c5e39..8d149d0 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InventoryResolver.java
@@ -17,6 +17,7 @@
package androidx.appactions.interaction.capabilities.core.task;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
@@ -26,8 +27,10 @@
* Similar to ValueListener, but also need to handle entity rendering.
*
* @param <T>
+ * @hide
*/
-public interface InventoryResolver<T> extends ValueListenerAsync<T> {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface InventoryResolver<T> extends ValueListener<T> {
/**
* Renders the provided entities in the app UI for dismabiguation.
*
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.kt
index 281269a..0f25b8e 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListener.kt
@@ -16,12 +16,14 @@
package androidx.appactions.interaction.capabilities.core.task
-import kotlinx.coroutines.guava.await
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.ListenableFutureHelper
+
+import com.google.common.util.concurrent.ListenableFuture
/**
* Provides a mechanism for the app to listen to argument updates from Assistant.
*/
-fun interface ValueListener<T> {
+interface ValueListener<T> {
/**
* Invoked when Assistant reports that an argument value has changed. This method should be
* idempotent, as it may be called multiple times with the same input value, not only on the
@@ -36,13 +38,26 @@
*
* <p>Returns the ValidationResult.
*/
- suspend fun onReceived(value: T): ValidationResult
-
- companion object {
- @JvmStatic
- @JvmName("from")
- fun <T> ValueListenerAsync<T>.toValueListener() = ValueListener<T> {
- this.onReceived(it).await()
- }
+ suspend fun onReceived(value: T): ValidationResult {
+ return ValidationResult.newAccepted()
}
+
+ /**
+ * Invoked when Assistant reports that an argument value has changed. This method should be
+ * idempotent, as it may be called multiple times with the same input value, not only on the
+ * initial value change.
+ *
+ * <p>This method should:
+ *
+ * <ul>
+ * <li>1. validate the given argument value(s).
+ * <li>2. If the given values are valid, update app UI state if applicable.
+ * </ul>
+ *
+ * <p>Returns a ListenableFuture containing the ValidationResult.
+ */
+ fun onReceivedAsync(value: T): ListenableFuture<ValidationResult> =
+ ListenableFutureHelper.convertToListenableFuture("ValueListener#onReceived") {
+ onReceived(value)
+ }
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListenerAsync.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListenerAsync.kt
deleted file mode 100644
index 47c9a37..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/ValueListenerAsync.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task
-
-import androidx.appactions.interaction.capabilities.core.impl.concurrent.ListenableFutureHelper
-import com.google.common.util.concurrent.ListenableFuture
-
-/**
- * Provides a mechanism for the app to listen to argument updates from Assistant.
- */
-fun interface ValueListenerAsync<T> {
- /**
- * Invoked when Assistant reports that an argument value has changed. This method should be
- * idempotent, as it may be called multiple times with the same input value, not only on the
- * initial value change.
- *
- * <p>This method should:
- *
- * <ul>
- * <li>1. validate the given argument value(s).
- * <li>2. If the given values are valid, update app UI state if applicable.
- * </ul>
- *
- * <p>Returns a ListenableFuture containing the ValidationResult.
- */
- fun onReceived(value: T): ListenableFuture<ValidationResult>
-
- companion object {
- @JvmStatic
- @JvmName("from")
- fun <T> ValueListener<T>.toValueListenerAsync() = ValueListenerAsync<T> {
- ListenableFutureHelper.convertToListenableFuture {
- this.onReceived(it)
- }
- }
- }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.java
deleted file mode 100644
index 31570a2..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task.impl;
-
-import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper;
-import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
-
-import com.google.auto.value.AutoValue;
-
-/** Represents a fulfillment request coming from Assistant. */
-@AutoValue
-abstract class AssistantUpdateRequest {
-
- static AssistantUpdateRequest create(
- ArgumentsWrapper argumentsWrapper, CallbackInternal callbackInternal) {
- return new AutoValue_AssistantUpdateRequest(argumentsWrapper, callbackInternal);
- }
-
- /** The fulfillment request data. */
- abstract ArgumentsWrapper argumentsWrapper();
-
- /*
- * The callback to be report results from handling this request.
- */
- abstract CallbackInternal callbackInternal();
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.kt
new file mode 100644
index 0000000..4799845
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/AssistantUpdateRequest.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.appactions.interaction.capabilities.core.task.impl
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.impl.ArgumentsWrapper
+import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal
+
+/**
+ * Represents a fulfillment request coming from Assistant.
+ *
+ * @param argumentsWrapper The fulfillment request data.
+ * @param callbackInternal The callback to report results from handling this request.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+data class AssistantUpdateRequest(
+ val argumentsWrapper: ArgumentsWrapper,
+ val callbackInternal: CallbackInternal,
+)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
index 6c9ab49..7954fa2 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
@@ -26,7 +26,7 @@
import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver;
import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
-import androidx.appactions.interaction.capabilities.core.task.ValueListenerAsync;
+import androidx.appactions.interaction.capabilities.core.task.ValueListener;
import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException;
import androidx.appactions.interaction.capabilities.core.values.SearchAction;
import androidx.appactions.interaction.proto.ParamValue;
@@ -48,13 +48,13 @@
public abstract class GenericResolverInternal<ValueTypeT> {
@NonNull
public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListener(
- @NonNull ValueListenerAsync<ValueTypeT> valueListener) {
+ @NonNull ValueListener<ValueTypeT> valueListener) {
return AutoOneOf_GenericResolverInternal.value(valueListener);
}
@NonNull
public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListListener(
- @NonNull ValueListenerAsync<List<ValueTypeT>> valueListListener) {
+ @NonNull ValueListener<List<ValueTypeT>> valueListListener) {
return AutoOneOf_GenericResolverInternal.valueList(valueListListener);
}
@@ -87,9 +87,9 @@
@NonNull
public abstract Kind getKind();
- abstract ValueListenerAsync<ValueTypeT> value();
+ abstract ValueListener<ValueTypeT> value();
- abstract ValueListenerAsync<List<ValueTypeT>> valueList();
+ abstract ValueListener<List<ValueTypeT>> valueList();
abstract AppEntityResolver<ValueTypeT> appEntityResolver();
@@ -151,17 +151,19 @@
switch (getKind()) {
case VALUE:
- return value().onReceived(singularConverter.convert(paramValues));
+ return value().onReceivedAsync(singularConverter.convert(paramValues));
case VALUE_LIST:
- return valueList().onReceived(repeatedConverter.convert(paramValues));
+ return valueList().onReceivedAsync(repeatedConverter.convert(paramValues));
case APP_ENTITY_RESOLVER:
- return appEntityResolver().onReceived(singularConverter.convert(paramValues));
+ return appEntityResolver().onReceivedAsync(singularConverter.convert(paramValues));
case APP_ENTITY_LIST_RESOLVER:
- return appEntityListResolver().onReceived(repeatedConverter.convert(paramValues));
+ return appEntityListResolver()
+ .onReceivedAsync(repeatedConverter.convert(paramValues));
case INVENTORY_RESOLVER:
- return inventoryResolver().onReceived(singularConverter.convert(paramValues));
+ return inventoryResolver().onReceivedAsync(singularConverter.convert(paramValues));
case INVENTORY_LIST_RESOLVER:
- return inventoryListResolver().onReceived(repeatedConverter.convert(paramValues));
+ return inventoryListResolver()
+ .onReceivedAsync(repeatedConverter.convert(paramValues));
}
throw new IllegalStateException("unreachable");
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.java
deleted file mode 100644
index 6fa70e6..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task.impl;
-
-import androidx.appactions.interaction.proto.CurrentValue;
-
-import com.google.auto.value.AutoValue;
-
-import java.util.List;
-
-@SuppressWarnings("AutoValueImmutableFields")
-@AutoValue
-abstract class SlotProcessingResult {
- static SlotProcessingResult create(Boolean isSuccessful, List<CurrentValue> processedValues) {
- return new AutoValue_SlotProcessingResult(isSuccessful, processedValues);
- }
-
- /**
- * Whether or not the next slot should be processed.
- *
- * <p>This is true if the following conditions were met during processing.
- *
- * <ul>
- * <li>there are no ungroundedValues remaining (either rejected or disambig)
- * <li>listener#onReceived returned ACCEPTED for all grounded values (which could be empty
- * list)
- * </ul>
- */
- abstract Boolean isSuccessful();
-
- /** Processed CurrentValue objects. */
- abstract List<CurrentValue> processedValues();
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.kt
new file mode 100644
index 0000000..93f8e5d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/SlotProcessingResult.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.appactions.interaction.capabilities.core.task.impl
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.proto.CurrentValue
+
+/**
+ * @param isSuccessful Whether or not the next slot should be processed. This is true if the
+ * following conditions were met during processing.
+ * * there are no ungrounded values remaining (either rejected or disambig)
+ * * listener#onReceived returned ACCEPTED for all grounded values (which could be empty list)
+ *
+ * @param processedValues Processed CurrentValue objects.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+data class SlotProcessingResult
+constructor(val isSuccessful: Boolean, val processedValues: List<CurrentValue>)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilitySession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilitySession.kt
index 16280db..38d5d61 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilitySession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilitySession.kt
@@ -25,14 +25,14 @@
import androidx.appactions.interaction.capabilities.core.impl.concurrent.FutureCallback
import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
-import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.proto.AppActionsContext.AppAction
+import androidx.appactions.interaction.proto.ParamValue
internal class TaskCapabilitySession<
ArgumentT,
OutputT,
ConfirmationT,
- > (
+>(
val actionSpec: ActionSpec<*, ArgumentT, OutputT>,
val appAction: AppAction,
val taskHandler: TaskHandler<ConfirmationT>,
@@ -56,8 +56,12 @@
private val requestLock = Any()
/** Contains session state and request processing logic. */
- private val sessionOrchestrator: TaskOrchestrator<
- ArgumentT, OutputT, ConfirmationT,> =
+ private val sessionOrchestrator:
+ TaskOrchestrator<
+ ArgumentT,
+ OutputT,
+ ConfirmationT,
+ > =
TaskOrchestrator(
actionSpec,
appAction,
@@ -69,7 +73,7 @@
private var pendingTouchEventRequest: TouchEventUpdateRequest? = null
override fun execute(argumentsWrapper: ArgumentsWrapper, callback: CallbackInternal) {
- enqueueAssistantRequest(AssistantUpdateRequest.create(argumentsWrapper, callback))
+ enqueueAssistantRequest(AssistantUpdateRequest(argumentsWrapper, callback))
}
override fun updateParamValues(paramValuesMap: Map<String, List<ParamValue>>) {
@@ -89,7 +93,7 @@
*/
private fun enqueueAssistantRequest(request: AssistantUpdateRequest) {
synchronized(requestLock) {
- pendingAssistantRequest?.callbackInternal()?.onError(ErrorStatusInternal.CANCELLED)
+ pendingAssistantRequest?.callbackInternal?.onError(ErrorStatusInternal.CANCELLED)
pendingAssistantRequest = request
dispatchPendingRequestIfIdle()
}
@@ -108,23 +112,23 @@
}
/**
- * If sessionOrchestrator is idle, select the next request to dispatch to sessionOrchestrator (if
- * there are any pending requests).
+ * If sessionOrchestrator is idle, select the next request to dispatch to sessionOrchestrator
+ * (if there are any pending requests).
*
* <p>If sessionOrchestrator is not idle, do nothing, since this method will automatically be
* called when sessionOrchestrator becomes idle.
*/
- private fun dispatchPendingRequestIfIdle() {
+ internal fun dispatchPendingRequestIfIdle() {
synchronized(requestLock) {
if (!sessionOrchestrator.isIdle()) {
return
}
var nextRequest: UpdateRequest? = null
if (pendingAssistantRequest != null) {
- nextRequest = UpdateRequest.of(pendingAssistantRequest)
+ nextRequest = UpdateRequest(pendingAssistantRequest!!)
pendingAssistantRequest = null
} else if (pendingTouchEventRequest != null) {
- nextRequest = UpdateRequest.of(pendingTouchEventRequest)
+ nextRequest = UpdateRequest(pendingTouchEventRequest!!)
pendingTouchEventRequest = null
}
if (nextRequest != null) {
@@ -137,14 +141,13 @@
/**
* A fatal exception has occurred, cause by one of the following:
- *
* <ul>
- * <li>1. The developer listener threw some runtime exception
- * <li>2. The SDK encountered some uncaught internal exception
+ * <li>1. The developer listener threw some runtime exception
+ * <li>2. The SDK encountered some uncaught internal exception
* </ul>
*
- * <p>In both cases, this exception will be rethrown which will crash
- * the app.
+ * <p>In both cases, this exception will be rethrown which will crash the
+ * app.
*/
override fun onFailure(t: Throwable) {
throw IllegalStateException(
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
index ebe7122..7cb544d 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
@@ -24,7 +24,6 @@
import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver
import androidx.appactions.interaction.capabilities.core.task.InventoryResolver
import androidx.appactions.interaction.capabilities.core.task.ValueListener
-import androidx.appactions.interaction.capabilities.core.task.ValueListenerAsync.Companion.toValueListenerAsync
import androidx.appactions.interaction.proto.ParamValue
import java.util.function.Function
@@ -125,9 +124,7 @@
mutableTaskParamMap[paramName] = TaskParamBinding(
paramName,
GROUND_NEVER,
- GenericResolverInternal.fromValueListener(
- listener.toValueListenerAsync(),
- ),
+ GenericResolverInternal.fromValueListener(listener),
converter,
null,
null,
@@ -143,10 +140,7 @@
mutableTaskParamMap[paramName] = TaskParamBinding(
paramName,
GROUND_NEVER,
- GenericResolverInternal.fromValueListListener(
-
- listener.toValueListenerAsync(),
- ),
+ GenericResolverInternal.fromValueListListener(listener),
converter,
null,
null,
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
index 0dd26ab..7251089 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
@@ -157,17 +157,14 @@
}
mIsIdle = false;
ListenableFuture<Void> requestProcessingFuture;
- switch (updateRequest.getKind()) {
- case ASSISTANT:
- requestProcessingFuture =
- processAssistantUpdateRequest(updateRequest.assistant());
- break;
- case TOUCH_EVENT:
- requestProcessingFuture =
- processTouchEventUpdateRequest(updateRequest.touchEvent());
- break;
- default:
- throw new IllegalArgumentException("unknown UpdateRequest type");
+ if (updateRequest.getAssistantRequest() != null) {
+ requestProcessingFuture =
+ processAssistantUpdateRequest(updateRequest.getAssistantRequest());
+ } else if (updateRequest.getTouchEventRequest() != null) {
+ requestProcessingFuture =
+ processTouchEventUpdateRequest(updateRequest.getTouchEventRequest());
+ } else {
+ throw new IllegalArgumentException("unknown UpdateRequest type");
}
return Futures.transform(
requestProcessingFuture,
@@ -185,8 +182,8 @@
/** Processes an assistant update request. */
private ListenableFuture<Void> processAssistantUpdateRequest(
AssistantUpdateRequest assistantUpdateRequest) {
- ArgumentsWrapper argumentsWrapper = assistantUpdateRequest.argumentsWrapper();
- CallbackInternal callback = assistantUpdateRequest.callbackInternal();
+ ArgumentsWrapper argumentsWrapper = assistantUpdateRequest.getArgumentsWrapper();
+ CallbackInternal callback = assistantUpdateRequest.getCallbackInternal();
if (argumentsWrapper.getRequestMetadata() == null) {
callback.onError(ErrorStatusInternal.INVALID_REQUEST_TYPE);
@@ -443,7 +440,7 @@
private ListenableFuture<Void> processFulfillmentValues(
Map<String, List<FulfillmentValue>> fulfillmentValuesMap) {
ListenableFuture<SlotProcessingResult> currentFuture =
- Futures.immediateFuture(SlotProcessingResult.create(true, Collections.emptyList()));
+ Futures.immediateFuture(new SlotProcessingResult(true, Collections.emptyList()));
for (Map.Entry<String, List<FulfillmentValue>> entry : fulfillmentValuesMap.entrySet()) {
String name = entry.getKey();
List<FulfillmentValue> fulfillmentValues = entry.getValue();
@@ -477,7 +474,7 @@
return Futures.transform(
processSlot(slotKey, previousResult, pendingArgs),
currentResult -> {
- mCurrentValuesMap.put(slotKey, currentResult.processedValues());
+ mCurrentValuesMap.put(slotKey, currentResult.getProcessedValues());
return currentResult;
},
mExecutor,
@@ -493,7 +490,7 @@
private ListenableFuture<SlotProcessingResult> processSlot(
String name, SlotProcessingResult previousResult, List<CurrentValue> pendingArgs) {
if (!previousResult.isSuccessful()) {
- return Futures.immediateFuture(SlotProcessingResult.create(false, pendingArgs));
+ return Futures.immediateFuture(new SlotProcessingResult(false, pendingArgs));
}
return TaskSlotProcessor.processSlot(
name, pendingArgs, mTaskHandler.getTaskParamMap(), mExecutor);
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java
index ae78e97..a20e3c8 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.java
@@ -119,7 +119,7 @@
// TODO(b/234655571) use slot metadata to ensure that we never auto accept values for
// reference slots.
return Futures.immediateFuture(
- SlotProcessingResult.create(
+ new SlotProcessingResult(
Boolean.TRUE,
pendingArgs.stream()
.map(
@@ -163,8 +163,8 @@
(unused) -> {
if (groundedValues.isEmpty()) {
return Futures.immediateFuture(
- SlotProcessingResult.create(
- /** isSuccessful= */
+ new SlotProcessingResult(
+ /* isSuccessful= */
false, Collections.unmodifiableList(ungroundedValues)));
}
return Futures.transform(
@@ -328,7 +328,7 @@
break;
}
combinedValues.addAll(ungroundedValues);
- return SlotProcessingResult.create(
+ return new SlotProcessingResult(
/* isSuccessful= */ ungroundedValues.isEmpty()
&& (validationResult.getKind() == ValidationResult.Kind.ACCEPTED),
Collections.unmodifiableList(combinedValues));
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TouchEventUpdateRequest.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TouchEventUpdateRequest.kt
index 176fb6d..0cbee46 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TouchEventUpdateRequest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TouchEventUpdateRequest.kt
@@ -16,15 +16,21 @@
package androidx.appactions.interaction.capabilities.core.task.impl
+import androidx.annotation.RestrictTo
import androidx.appactions.interaction.proto.ParamValue
-/** Represents a fulfillment request coming from user tap. */
-internal data class TouchEventUpdateRequest(val paramValuesMap: Map<String, List<ParamValue>>) {
+/**
+ * Represents a fulfillment request coming from user tap.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+data class TouchEventUpdateRequest(val paramValuesMap: Map<String, List<ParamValue>>) {
companion object {
/**
- * merge two TouchEventUpdateRequest instances. Map entries in newRequest will take priority in
- * case of conflict.
+ * merge two TouchEventUpdateRequest instances. Map entries in newRequest will take priority
+ * in case of conflict.
*/
@JvmStatic
fun merge(
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.java
deleted file mode 100644
index b9bf730..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task.impl;
-
-import com.google.auto.value.AutoOneOf;
-
-/** Contains either an AssistantUpdateRequest or a TouchEventUpdateRequest */
-@AutoOneOf(UpdateRequest.Kind.class)
-abstract class UpdateRequest {
- static UpdateRequest of(AssistantUpdateRequest request) {
- return AutoOneOf_UpdateRequest.assistant(request);
- }
-
- static UpdateRequest of(TouchEventUpdateRequest request) {
- return AutoOneOf_UpdateRequest.touchEvent(request);
- }
-
- abstract Kind getKind();
-
- abstract AssistantUpdateRequest assistant();
-
- abstract TouchEventUpdateRequest touchEvent();
-
- enum Kind {
- ASSISTANT,
- TOUCH_EVENT
- }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.kt
new file mode 100644
index 0000000..60c3816
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/UpdateRequest.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.appactions.interaction.capabilities.core.task.impl
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Contains either an AssistantUpdateRequest or a TouchEventUpdateRequest
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class UpdateRequest
+private constructor(
+ val assistantRequest: AssistantUpdateRequest?,
+ val touchEventRequest: TouchEventUpdateRequest?,
+) {
+ constructor(assistantRequest: AssistantUpdateRequest) : this(assistantRequest, null)
+
+ constructor(touchEventRequest: TouchEventUpdateRequest) : this(null, touchEventRequest)
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
index 7d9f775..a5fcd1f 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
@@ -19,8 +19,11 @@
import android.util.SizeF
import androidx.appactions.interaction.capabilities.core.ActionCapability
import androidx.appactions.interaction.capabilities.core.ActionExecutor
+import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
+import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync.Companion.toActionExecutorAsync
import androidx.appactions.interaction.capabilities.core.ExecutionResult
import androidx.appactions.interaction.capabilities.core.HostProperties
+import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -50,6 +53,13 @@
@Ignore // b/271033076
@Test
fun oneShotCapability_successWithOutput() {
+ val actionExecutor = object : ActionExecutor<Argument, Output> {
+ override suspend fun execute(argument: Argument): ExecutionResult<Output> =
+ ExecutionResult.Builder<Output>().setOutput(
+ Output.builder().setOptionalStringField("stringOutput")
+ .build(),
+ ).build()
+ }
val capability: ActionCapability =
SingleTurnCapabilityImpl<Property, Argument, Output>(
"capabilityId",
@@ -59,13 +69,7 @@
).setOptionalStringField(
StringProperty.Builder().setProhibited(true).build(),
).build(),
- object : ActionExecutor<Argument, Output> {
- override suspend fun execute(argument: Argument): ExecutionResult<Output> =
- ExecutionResult.Builder<Output>().setOutput(
- Output.builder().setOptionalStringField("stringOutput")
- .build(),
- ).build()
- },
+ actionExecutor.toActionExecutorAsync(),
)
val expectedFulfillmentResponse: FulfillmentResponse =
FulfillmentResponse.newBuilder().setExecutionOutput(
@@ -99,12 +103,10 @@
}
@Test
- fun oneShotSession_uiHandle() {
- val actionExecutor: ActionExecutor<Argument, Output> =
- object : ActionExecutor<Argument, Output> {
- override suspend fun execute(
- argument: Argument,
- ): ExecutionResult<Output> = ExecutionResult.getDefaultInstance<Output>()
+ fun oneShotSession_uiHandle_withActionExecutor() {
+ val actionExecutor =
+ ActionExecutor<Argument, Output> {
+ ExecutionResult.getDefaultInstance()
}
val capability: ActionCapability =
SingleTurnCapabilityImpl<Property, Argument, Output>(
@@ -113,12 +115,30 @@
Property.newBuilder().setRequiredEntityField(
EntityProperty.Builder().build(),
).build(),
- actionExecutor,
+ actionExecutor.toActionExecutorAsync(),
)
val session = capability.createSession(hostProperties)
assertThat(session.uiHandle).isSameInstanceAs(actionExecutor)
}
+ @Test
+ fun oneShotSession_uiHandle_withActionExecutorAsync() {
+ val actionExecutorAsync = ActionExecutorAsync<Argument, Output> {
+ Futures.immediateFuture(ExecutionResult.getDefaultInstance())
+ }
+ val capability: ActionCapability =
+ SingleTurnCapabilityImpl<Property, Argument, Output>(
+ "capabilityId",
+ ACTION_SPEC,
+ Property.newBuilder().setRequiredEntityField(
+ EntityProperty.Builder().build(),
+ ).build(),
+ actionExecutorAsync,
+ )
+ val session = capability.createSession(hostProperties)
+ assertThat(session.uiHandle).isSameInstanceAs(actionExecutorAsync)
+ }
+
companion object {
val ACTION_SPEC: ActionSpec<Property, Argument, Output> =
ActionSpecBuilder.ofCapabilityNamed(
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelperTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelperTest.kt
new file mode 100644
index 0000000..c5af959
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelperTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.appactions.interaction.capabilities.core.impl.concurrent
+
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.coroutines.cancellation.CancellationException
+@RunWith(JUnit4::class)
+class ListenableFutureHelperTest {
+ val tag = "tag"
+ val SHORT_TIMEOUT_MILLIS = 50L
+
+ @Test
+ fun suspendToListenableFuture_smokeTest() {
+ val stringFuture = ListenableFutureHelper.convertToListenableFuture<String>(tag) { "hello" }
+ assertThat(stringFuture.get()).isEqualTo("hello")
+ }
+
+ @Test
+ fun suspendToListenableFuture_pollingTest() {
+ val stringChannel = Channel<String>(1)
+ val stringFuture = ListenableFutureHelper.convertToListenableFuture<String>(tag) {
+ getNextValueFromChannel(stringChannel)
+ }
+ assertThat(stringFuture.isDone()).isFalse()
+
+ runBlocking {
+ withTimeout(SHORT_TIMEOUT_MILLIS) {
+ stringChannel.send("hello")
+ }
+ }
+ assertThat(stringFuture.get()).isEqualTo("hello")
+ }
+
+ @Test
+ fun suspendToListenableFuture_cancellationTest() {
+ val stringChannel = Channel<String>(1)
+ val cancellationChannel = Channel<Boolean>(1)
+ val stringFuture = ListenableFutureHelper.convertToListenableFuture<String>(tag) {
+ getNextValueFromChannel(stringChannel, cancellationChannel)
+ }
+ stringFuture.cancel(true)
+ assertThat(stringFuture.isCancelled()).isTrue()
+ runBlocking {
+ withTimeout(SHORT_TIMEOUT_MILLIS) {
+ assertThat(cancellationChannel.receive()).isTrue()
+ }
+ }
+ }
+
+ private suspend fun <T> getNextValueFromChannel(
+ valueChannel: ReceiveChannel<T>,
+ cancellationChannel: SendChannel<Boolean>? = null,
+ ): T {
+ try {
+ return valueChannel.receive()
+ } catch (e: CancellationException) {
+ cancellationChannel?.send(true)
+ throw e
+ }
+ }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
index 9ea61c1..f012e395 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
@@ -285,6 +285,33 @@
.containsExactlyElementsIn(expectedExecutionOutput.getOutputValuesList());
}
+ @Test
+ @SuppressWarnings("JdkImmutableCollections")
+ public void convertOutputToProto_emptyOutput() {
+ Output output = Output.builder().setRepeatedStringField(List.of("test3", "test4")).build();
+ // No optionalStringOutput since it is not in the output above.
+ StructuredOutput expectedExecutionOutput =
+ StructuredOutput.newBuilder()
+ .addOutputValues(
+ OutputValue.newBuilder()
+ .setName("repeatedStringOutput")
+ .addValues(
+ ParamValue.newBuilder()
+ .setStringValue("test3")
+ .build())
+ .addValues(
+ ParamValue.newBuilder()
+ .setStringValue("test4")
+ .build())
+ .build())
+ .build();
+
+ StructuredOutput executionOutput = ACTION_SPEC.convertOutputToProto(output);
+
+ assertThat(executionOutput.getOutputValuesList())
+ .containsExactlyElementsIn(expectedExecutionOutput.getOutputValuesList());
+ }
+
enum TestEnum {
VALUE_1,
VALUE_2,
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
index a709e71..043ad48 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
@@ -37,8 +37,7 @@
import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver
import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult
import androidx.appactions.interaction.capabilities.core.task.ValidationResult
-import androidx.appactions.interaction.capabilities.core.task.ValueListener.Companion.toValueListener
-import androidx.appactions.interaction.capabilities.core.task.ValueListenerAsync
+import androidx.appactions.interaction.capabilities.core.task.ValueListener
import androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils.buildRequestArgs
import androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils.buildSearchActionParamValue
import androidx.appactions.interaction.capabilities.core.testing.TestingUtils.CB_TIMEOUT
@@ -187,7 +186,7 @@
}
}
- private class DeferredValueListener<T> : ValueListenerAsync<T> {
+ private class DeferredValueListener<T> : ValueListener<T> {
val mCompleterRef: AtomicReference<Completer<ValidationResult>> =
AtomicReference<Completer<ValidationResult>>()
@@ -197,7 +196,7 @@
completer.set(t)
}
- override fun onReceived(value: T): ListenableFuture<ValidationResult> {
+ override fun onReceivedAsync(value: T): ListenableFuture<ValidationResult> {
return CallbackToFutureAdapter.getFuture { newCompleter ->
val oldCompleter: Completer<ValidationResult>? = mCompleterRef.getAndSet(
newCompleter,
@@ -268,11 +267,11 @@
val sessionBridge = SessionBridge<CapabilityTwoEntityValues.Session, Void> {
TaskHandler.Builder<Void>().registerValueTaskParam(
"slotA",
- AUTO_ACCEPT_ENTITY_VALUE.toValueListener(),
+ AUTO_ACCEPT_ENTITY_VALUE,
TypeConverters::toEntityValue,
).registerValueTaskParam(
"slotB",
- AUTO_ACCEPT_ENTITY_VALUE.toValueListener(),
+ AUTO_ACCEPT_ENTITY_VALUE,
TypeConverters::toEntityValue,
).build()
}
@@ -348,11 +347,11 @@
val sessionBridge = SessionBridge<CapabilityTwoEntityValues.Session, Void> {
TaskHandler.Builder<Void>().registerValueTaskParam(
"slotA",
- AUTO_ACCEPT_ENTITY_VALUE.toValueListener(),
+ AUTO_ACCEPT_ENTITY_VALUE,
TypeConverters::toEntityValue,
).registerValueTaskParam(
"slotB",
- AUTO_REJECT_ENTITY_VALUE.toValueListener(),
+ AUTO_REJECT_ENTITY_VALUE,
TypeConverters::toEntityValue,
).build()
}
@@ -491,7 +490,7 @@
)
}
- override fun onReceived(
+ override fun onReceivedAsync(
value: EntityValue,
): ListenableFuture<ValidationResult> {
return Futures.immediateFuture(ValidationResult.newAccepted())
@@ -654,7 +653,7 @@
}
override fun getListItemListener() = object : AppEntityResolver<ListItem> {
- override fun onReceived(
+ override fun onReceivedAsync(
value: ListItem,
): ListenableFuture<ValidationResult> {
onReceivedCb.set(value)
@@ -907,7 +906,9 @@
)
}
- override fun onReceived(value: EntityValue): ListenableFuture<ValidationResult> {
+ override fun onReceivedAsync(
+ value: EntityValue,
+ ): ListenableFuture<ValidationResult> {
return Futures.immediateFuture(ValidationResult.newAccepted())
}
}
@@ -923,7 +924,9 @@
)
}
- override fun onReceived(value: EntityValue): ListenableFuture<ValidationResult> {
+ override fun onReceivedAsync(
+ value: EntityValue,
+ ): ListenableFuture<ValidationResult> {
return Futures.immediateFuture(ValidationResult.newRejected())
}
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
index 5dfd041..4a000eb 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
@@ -27,6 +27,7 @@
import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
+import androidx.appactions.interaction.capabilities.core.task.ValueListener;
import androidx.appactions.interaction.capabilities.core.testing.spec.SettableFutureWrapper;
import androidx.appactions.interaction.capabilities.core.values.SearchAction;
import androidx.appactions.interaction.proto.CurrentValue;
@@ -61,7 +62,7 @@
new InventoryResolver<T>() {
@NonNull
@Override
- public ListenableFuture<ValidationResult> onReceived(T value) {
+ public ListenableFuture<ValidationResult> onReceivedAsync(T value) {
valueConsumer.accept(value);
return Futures.immediateFuture(validationResult);
}
@@ -78,23 +79,30 @@
private <T> GenericResolverInternal<T> createValueResolver(
ValidationResult validationResult, Consumer<T> valueConsumer) {
return GenericResolverInternal.fromValueListener(
- value -> {
- valueConsumer.accept(value);
- return Futures.immediateFuture(validationResult);
+ new ValueListener<T>() {
+ @NonNull
+ @Override
+ public ListenableFuture<ValidationResult> onReceivedAsync(T value) {
+ valueConsumer.accept(value);
+ return Futures.immediateFuture(validationResult);
+ }
});
}
private <T> GenericResolverInternal<T> createValueResolver(ValidationResult validationResult) {
- return createValueResolver(validationResult, (unused) -> {
- });
+ return createValueResolver(validationResult, (unused) -> {});
}
private <T> GenericResolverInternal<T> createValueListResolver(
ValidationResult validationResult, Consumer<List<T>> valueConsumer) {
return GenericResolverInternal.fromValueListListener(
- value -> {
- valueConsumer.accept(value);
- return Futures.immediateFuture(validationResult);
+ new ValueListener<List<T>>() {
+ @NonNull
+ @Override
+ public ListenableFuture<ValidationResult> onReceivedAsync(List<T> value) {
+ valueConsumer.accept(value);
+ return Futures.immediateFuture(validationResult);
+ }
});
}
@@ -107,7 +115,7 @@
new AppEntityResolver<T>() {
@NonNull
@Override
- public ListenableFuture<ValidationResult> onReceived(T value) {
+ public ListenableFuture<ValidationResult> onReceivedAsync(T value) {
valueConsumer.accept(value);
return Futures.immediateFuture(validationResult);
}
@@ -124,13 +132,14 @@
@Test
public void processSlot_singleValue_accepted() throws Exception {
- TaskParamBinding<String> binding = new TaskParamBinding<>(
- "singularValue",
- (paramValue) -> false,
- createValueResolver(ValidationResult.newAccepted()),
- TypeConverters::toStringValue,
- null,
- null);
+ TaskParamBinding<String> binding =
+ new TaskParamBinding<>(
+ "singularValue",
+ (paramValue) -> false,
+ createValueResolver(ValidationResult.newAccepted()),
+ TypeConverters::toStringValue,
+ null,
+ null);
Map<String, TaskParamBinding<?>> taskParamMap = new HashMap<>();
taskParamMap.put("singularValue", binding);
List<ParamValue> args =
@@ -146,21 +155,24 @@
.get();
assertThat(result.isSuccessful()).isTrue();
- assertThat(result.processedValues())
+ assertThat(result.getProcessedValues())
.containsExactly(
- CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
- Status.ACCEPTED).build());
+ CurrentValue.newBuilder()
+ .setValue(args.get(0))
+ .setStatus(Status.ACCEPTED)
+ .build());
}
@Test
public void processSlot_singleValue_rejected() throws Exception {
- TaskParamBinding<String> binding = new TaskParamBinding<>(
- "singularValue",
- (paramValue) -> false,
- createValueResolver(ValidationResult.newRejected()),
- TypeConverters::toStringValue,
- null,
- null);
+ TaskParamBinding<String> binding =
+ new TaskParamBinding<>(
+ "singularValue",
+ (paramValue) -> false,
+ createValueResolver(ValidationResult.newRejected()),
+ TypeConverters::toStringValue,
+ null,
+ null);
Map<String, TaskParamBinding<?>> taskParamMap = new HashMap<>();
taskParamMap.put("singularValue", binding);
List<ParamValue> args =
@@ -176,23 +188,26 @@
.get();
assertThat(result.isSuccessful()).isFalse();
- assertThat(result.processedValues())
+ assertThat(result.getProcessedValues())
.containsExactly(
- CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
- Status.REJECTED).build());
+ CurrentValue.newBuilder()
+ .setValue(args.get(0))
+ .setStatus(Status.REJECTED)
+ .build());
}
@Test
public void processSlot_repeatedValue_accepted() throws Exception {
SettableFutureWrapper<List<String>> lastReceivedArgs = new SettableFutureWrapper<>();
- TaskParamBinding<String> binding = new TaskParamBinding<>(
- "repeatedValue",
- (paramValue) -> false,
- createValueListResolver(
- ValidationResult.newAccepted(), lastReceivedArgs::set),
- TypeConverters::toStringValue,
- null,
- null);
+ TaskParamBinding<String> binding =
+ new TaskParamBinding<>(
+ "repeatedValue",
+ (paramValue) -> false,
+ createValueListResolver(
+ ValidationResult.newAccepted(), lastReceivedArgs::set),
+ TypeConverters::toStringValue,
+ null,
+ null);
Map<String, TaskParamBinding<?>> taskParamMap = new HashMap<>();
taskParamMap.put("repeatedValue", binding);
List<ParamValue> args =
@@ -209,26 +224,31 @@
.get();
assertThat(result.isSuccessful()).isTrue();
- assertThat(result.processedValues())
+ assertThat(result.getProcessedValues())
.containsExactly(
- CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
- Status.ACCEPTED).build(),
- CurrentValue.newBuilder().setValue(args.get(1)).setStatus(
- Status.ACCEPTED).build());
+ CurrentValue.newBuilder()
+ .setValue(args.get(0))
+ .setStatus(Status.ACCEPTED)
+ .build(),
+ CurrentValue.newBuilder()
+ .setValue(args.get(1))
+ .setStatus(Status.ACCEPTED)
+ .build());
assertThat(lastReceivedArgs.getFuture().get()).containsExactly("testValue1", "testValue2");
}
@Test
public void processSlot_repeatedValue_rejected() throws Exception {
SettableFutureWrapper<List<String>> lastReceivedArgs = new SettableFutureWrapper<>();
- TaskParamBinding<String> binding = new TaskParamBinding<>(
- "repeatedValue",
- (paramValue) -> false,
- createValueListResolver(
- ValidationResult.newRejected(), lastReceivedArgs::set),
- TypeConverters::toStringValue,
- null,
- null);
+ TaskParamBinding<String> binding =
+ new TaskParamBinding<>(
+ "repeatedValue",
+ (paramValue) -> false,
+ createValueListResolver(
+ ValidationResult.newRejected(), lastReceivedArgs::set),
+ TypeConverters::toStringValue,
+ null,
+ null);
Map<String, TaskParamBinding<?>> taskParamMap = new HashMap<>();
taskParamMap.put("repeatedValue", binding);
List<ParamValue> args =
@@ -245,12 +265,16 @@
.get();
assertThat(result.isSuccessful()).isFalse();
- assertThat(result.processedValues())
+ assertThat(result.getProcessedValues())
.containsExactly(
- CurrentValue.newBuilder().setValue(args.get(0)).setStatus(
- Status.REJECTED).build(),
- CurrentValue.newBuilder().setValue(args.get(1)).setStatus(
- Status.REJECTED).build());
+ CurrentValue.newBuilder()
+ .setValue(args.get(0))
+ .setStatus(Status.REJECTED)
+ .build(),
+ CurrentValue.newBuilder()
+ .setValue(args.get(1))
+ .setStatus(Status.REJECTED)
+ .build());
assertThat(lastReceivedArgs.getFuture().get()).containsExactly("testValue1", "testValue2");
}
@@ -259,15 +283,15 @@
throws Exception {
SettableFutureWrapper<String> onReceivedCb = new SettableFutureWrapper<>();
SettableFutureWrapper<List<String>> renderCb = new SettableFutureWrapper<>();
- TaskParamBinding<String> binding = new TaskParamBinding<>(
- "assistantDrivenSlot",
- (paramValue) -> !paramValue.hasIdentifier(),
- createAssistantDisambigResolver(
- ValidationResult.newAccepted(), onReceivedCb::set,
- renderCb::set),
- TypeConverters::toStringValue,
- null,
- null);
+ TaskParamBinding<String> binding =
+ new TaskParamBinding<>(
+ "assistantDrivenSlot",
+ (paramValue) -> !paramValue.hasIdentifier(),
+ createAssistantDisambigResolver(
+ ValidationResult.newAccepted(), onReceivedCb::set, renderCb::set),
+ TypeConverters::toStringValue,
+ null,
+ null);
Map<String, TaskParamBinding<?>> taskParamMap = new HashMap<>();
taskParamMap.put("assistantDrivenSlot", binding);
CurrentValue previouslyAccepted =
@@ -278,9 +302,11 @@
.setIdentifier("id")
.setStructValue(
Struct.newBuilder()
- .putFields("id",
- Value.newBuilder().setStringValue(
- "1234").build())))
+ .putFields(
+ "id",
+ Value.newBuilder()
+ .setStringValue("1234")
+ .build())))
.build();
List<CurrentValue> values =
Arrays.asList(
@@ -289,31 +315,35 @@
.setStatus(Status.PENDING)
.setDisambiguationData(
DisambiguationData.newBuilder()
- .addEntities(Entity.newBuilder().setIdentifier(
- "entity-1"))
- .addEntities(Entity.newBuilder().setIdentifier(
- "entity-2")))
+ .addEntities(
+ Entity.newBuilder()
+ .setIdentifier("entity-1"))
+ .addEntities(
+ Entity.newBuilder()
+ .setIdentifier("entity-2")))
.build());
SlotProcessingResult result =
- TaskSlotProcessor.processSlot("assistantDrivenSlot", values, taskParamMap,
- Runnable::run)
+ TaskSlotProcessor.processSlot(
+ "assistantDrivenSlot", values, taskParamMap, Runnable::run)
.get();
assertThat(result.isSuccessful()).isFalse();
assertThat(onReceivedCb.getFuture().get()).isEqualTo("id");
assertThat(renderCb.getFuture().get()).containsExactly("entity-1", "entity-2");
- assertThat(result.processedValues())
+ assertThat(result.getProcessedValues())
.containsExactly(
previouslyAccepted,
CurrentValue.newBuilder()
.setStatus(Status.DISAMBIG)
.setDisambiguationData(
DisambiguationData.newBuilder()
- .addEntities(Entity.newBuilder().setIdentifier(
- "entity-1"))
- .addEntities(Entity.newBuilder().setIdentifier(
- "entity-2")))
+ .addEntities(
+ Entity.newBuilder()
+ .setIdentifier("entity-1"))
+ .addEntities(
+ Entity.newBuilder()
+ .setIdentifier("entity-2")))
.build());
}
@@ -328,17 +358,18 @@
onReceivedCb::set,
entitySearchResult, // app-grounding returns REJECTED in all cases
appSearchCb::set);
- TaskParamBinding<String> binding = new TaskParamBinding<>(
- "appDrivenSlot",
- (paramValue) -> true, // always invoke app-grounding in all cases
- resolver,
- TypeConverters::toStringValue, // Not invoked
- (unused) -> Entity.getDefaultInstance(),
- (unused) ->
- SearchAction.<String>newBuilder()
- .setQuery("A")
- .setObject("nested")
- .build());
+ TaskParamBinding<String> binding =
+ new TaskParamBinding<>(
+ "appDrivenSlot",
+ (paramValue) -> true, // always invoke app-grounding in all cases
+ resolver,
+ TypeConverters::toStringValue, // Not invoked
+ (unused) -> Entity.getDefaultInstance(),
+ (unused) ->
+ SearchAction.<String>newBuilder()
+ .setQuery("A")
+ .setObject("nested")
+ .build());
Map<String, TaskParamBinding<?>> taskParamMap = new HashMap<>();
taskParamMap.put("appDrivenSlot", binding);
List<CurrentValue> values =
@@ -349,16 +380,19 @@
.build());
SlotProcessingResult result =
- TaskSlotProcessor.processSlot("appDrivenSlot", values, taskParamMap,
- Runnable::run).get();
+ TaskSlotProcessor.processSlot("appDrivenSlot", values, taskParamMap, Runnable::run)
+ .get();
assertThat(result.isSuccessful()).isFalse();
assertThat(onReceivedCb.getFuture().isDone()).isFalse();
assertThat(appSearchCb.getFuture().isDone()).isTrue();
assertThat(appSearchCb.getFuture().get())
- .isEqualTo(SearchAction.<String>newBuilder().setQuery("A").setObject(
- "nested").build());
- assertThat(result.processedValues())
+ .isEqualTo(
+ SearchAction.<String>newBuilder()
+ .setQuery("A")
+ .setObject("nested")
+ .build());
+ assertThat(result.getProcessedValues())
.containsExactly(
CurrentValue.newBuilder()
.setStatus(Status.REJECTED)
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
index a6e64ba..7afa492 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
@@ -17,7 +17,7 @@
package androidx.appactions.interaction.capabilities.core.testing;
import androidx.annotation.NonNull;
-import androidx.appactions.interaction.capabilities.core.ActionExecutor;
+import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync;
import androidx.appactions.interaction.capabilities.core.ExecutionResult;
import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
@@ -29,7 +29,6 @@
import com.google.auto.value.AutoOneOf;
import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.atomic.AtomicReference;
@@ -165,12 +164,7 @@
}
public static <ArgumentT, OutputT>
- ActionExecutor<ArgumentT, OutputT> createFakeActionExecutor() {
- return new ActionExecutor<ArgumentT, OutputT>() {
- @Override
- public ListenableFuture<ExecutionResult<OutputT>> executeAsync(ArgumentT args) {
- return Futures.immediateFuture(ExecutionResult.<OutputT>getDefaultInstance());
- }
- };
+ ActionExecutorAsync<ArgumentT, OutputT> createFakeActionExecutor() {
+ return args -> Futures.immediateFuture(ExecutionResult.<OutputT>getDefaultInstance());
}
}
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTImer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
similarity index 100%
rename from appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTImer.kt
rename to appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
diff --git a/appactions/interaction/interaction-service-proto/build.gradle b/appactions/interaction/interaction-service-proto/build.gradle
index 5fcc43c..38a00154 100644
--- a/appactions/interaction/interaction-service-proto/build.gradle
+++ b/appactions/interaction/interaction-service-proto/build.gradle
@@ -24,8 +24,7 @@
}
dependencies {
- // TODO(b/268709908): Bump this to version 1.52.0 and make available from libs.grpcProtobufLite
- implementation("io.grpc:grpc-protobuf-lite:1.45.1") {
+ implementation(libs.grpcProtobufLite) {
// Ensure we only bundle grpc-protobuf-lite. Any of its dependencies should be added
// as `compileOnly` dependencies below.
exclude group: 'com.google.protobuf'
@@ -44,8 +43,8 @@
// any library that bundles interaction-service-proto.
compileOnly(libs.protobufLite)
compileOnly(libs.grpcStub)
+ compileOnly(libs.jsr250)
compileOnly("androidx.annotation:annotation:1.1.0")
- compileOnly("javax.annotation:javax.annotation-api:1.3.2")
}
protobuf {
@@ -55,7 +54,7 @@
// Configure the codegen plugins for GRPC.
plugins {
grpc {
- artifact = 'io.grpc:protoc-gen-grpc-java:1.52.0'
+ artifact = libs.grpcProtobufCompiler.get()
}
}
diff --git a/appactions/interaction/interaction-service/build.gradle b/appactions/interaction/interaction-service/build.gradle
index acac500..faf752e 100644
--- a/appactions/interaction/interaction-service/build.gradle
+++ b/appactions/interaction/interaction-service/build.gradle
@@ -38,14 +38,14 @@
bundleInside(project(":appactions:interaction:interaction-service-proto"))
implementation(project(":appactions:interaction:interaction-capabilities-core"))
+ implementation("androidx.annotation:annotation:1.1.0")
+ implementation("androidx.concurrent:concurrent-futures:1.1.0")
+ implementation("androidx.wear.tiles:tiles:1.1.0")
implementation(libs.grpcAndroid)
implementation(libs.grpcBinder)
implementation(libs.grpcStub)
implementation(libs.kotlinStdlib)
- implementation("androidx.annotation:annotation:1.1.0")
- implementation("androidx.concurrent:concurrent-futures:1.1.0")
- implementation("androidx.wear.tiles:tiles:1.1.0")
- implementation("javax.annotation:javax.annotation-api:1.3.2")
+ implementation(libs.jsr250)
testImplementation(project(":appactions:interaction:interaction-service-proto"))
testImplementation(libs.kotlinStdlib)
diff --git a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionService.java b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionService.java
deleted file mode 100644
index fd3f218..0000000
--- a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionService.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.service;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appactions.interaction.capabilities.core.ActionCapability;
-import androidx.appactions.interaction.service.proto.AppInteractionServiceGrpc;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import io.grpc.Server;
-import io.grpc.binder.AndroidComponentAddress;
-import io.grpc.binder.BinderServerBuilder;
-import io.grpc.binder.IBinderReceiver;
-import io.grpc.binder.SecurityPolicies;
-import io.grpc.binder.SecurityPolicy;
-import io.grpc.binder.ServerSecurityPolicy;
-
-/**
- * Base service class for the AppInteractionService SDK. This sets up the GRPC on-device server for
- * communication with Assistant.
- */
-// TODO(b/267772921): Rewrite public facing class to Kotlin
-public abstract class AppInteractionService extends Service {
-
- private static final String TAG = "AppInteractionService";
- private ServerLifecycle mBinderSupplier;
-
- public AppInteractionService() {}
-
- /**
- * Called by the system once after the Assistant binds to the service.
- *
- * @return the list of capabilities that this service supports.
- */
- @NonNull
- protected abstract List<ActionCapability> registerCapabilities();
-
- /**
- * Sets a custom {@link SecurityPolicy} for the gRPC service. This gives control over which
- * clients are allowed to bind to your service.
- *
- * <p>Overriding this method is <b>not</b> the preferred method for security enforcement. We
- * recommend developers override {@link #getAllowedApps()} for security needs. Implementing your
- * own security policy requires significant care, and an understanding of the details and
- * pitfalls of Android security. If you choose to do so, we <b>strongly</b> recommend you get
- * such a change reviewed by Android security experts.
- */
- @NonNull
- protected SecurityPolicy getSecurityPolicy() {
- return SecurityPolicies.anyOf(
- getSecurityPolicyFromAllowedList(getAllowedApps()).toArray(new SecurityPolicy[0]));
- }
-
- /**
- * Returns a list of the apps {@link AppVerificationInfo} that are allowed to interact with the
- * app's bound service. This gives control over which clients are allowed to communicate with
- * the service.
- *
- * <p>This is the default method for enforcing security and must be overridden. Developers
- * should return an empty list should they choose to define their own security by way of
- * overriding {@link #getSecurityPolicyFromAllowedList}.
- */
- @NonNull
- protected abstract List<AppVerificationInfo> getAllowedApps();
-
- /**
- * Sets a custom {@link SecurityPolicy} for the gRPC service given the client's allowed pairs of
- * package names with corresponding Sha256 signatures. This gives control over which clients are
- * allowed to bind to your service.
- *
- * <p>A SecurityPolicy is returned per supported Assistant. Such as "Google Assistant", "Bixby",
- * etc.
- */
- @NonNull
- private List<SecurityPolicy> getSecurityPolicyFromAllowedList(
- List<AppVerificationInfo> verificationInfoList) {
-
- List<SecurityPolicy> policies = new ArrayList<>();
- if (verificationInfoList == null || verificationInfoList.isEmpty()) {
- policies.add(SecurityPolicies.internalOnly());
- return policies;
- }
- for (AppVerificationInfo verificationInfo : verificationInfoList) {
- policies.add(
- SecurityPolicies.oneOfSignatureSha256Hash(
- this.getPackageManager(),
- verificationInfo.getPackageName(),
- verificationInfo.getSignatures()));
- }
- return policies;
- }
-
- @Override
- @CallSuper
- public void onCreate() {
- super.onCreate();
- IBinderReceiver binderReceiver = new IBinderReceiver();
- ServerSecurityPolicy serverSecurityPolicy =
- ServerSecurityPolicy.newBuilder()
- .servicePolicy(AppInteractionServiceGrpc.SERVICE_NAME, getSecurityPolicy())
- .build();
- Server server =
- BinderServerBuilder.forAddress(
- AndroidComponentAddress.forContext(this), binderReceiver)
- .securityPolicy(serverSecurityPolicy)
- .intercept(new RemoteViewsOverMetadataInterceptor())
- .addService(AppInteractionServiceFactory.create(this))
- .build();
-
- mBinderSupplier = new ServerLifecycle(server, binderReceiver);
- }
-
- @Override
- @NonNull
- public IBinder onBind(@Nullable Intent intent) {
- return mBinderSupplier.get();
- }
-
- @Override
- @CallSuper
- public void onDestroy() {
- if (mBinderSupplier != null) {
- mBinderSupplier.shutdown();
- }
- super.onDestroy();
- }
-
- static final class ServerLifecycle {
- private final Server mServer;
- private final IBinderReceiver mReceiver;
- private boolean mStarted;
-
- ServerLifecycle(Server server, IBinderReceiver receiver) {
- this.mServer = server;
- this.mReceiver = receiver;
- }
-
- public IBinder get() {
- synchronized (this) {
- if (!mStarted) {
- try {
- mStarted = true;
- mServer.start();
- } catch (IOException ioe) {
- Log.e(TAG, "Unable to start server " + mServer, ioe);
- }
- }
- return mReceiver.get();
- }
- }
-
- public void shutdown() {
- if (mStarted) {
- mServer.shutdownNow();
- mStarted = false;
- }
- }
- }
-}
diff --git a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionService.kt b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionService.kt
new file mode 100644
index 0000000..264ff9e
--- /dev/null
+++ b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionService.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.appactions.interaction.service
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import androidx.annotation.CallSuper
+import androidx.appactions.interaction.capabilities.core.ActionCapability
+import androidx.appactions.interaction.service.proto.AppInteractionServiceGrpc
+import io.grpc.Server
+import io.grpc.binder.AndroidComponentAddress
+import io.grpc.binder.BinderServerBuilder
+import io.grpc.binder.IBinderReceiver
+import io.grpc.binder.SecurityPolicies
+import io.grpc.binder.SecurityPolicy
+import io.grpc.binder.ServerSecurityPolicy
+import java.io.IOException
+
+/**
+ * Base service class for the AppInteractionService SDK. This sets up the GRPC on-device server for
+ * communication with Assistant.
+ */
+abstract class AppInteractionService : Service() {
+ private var binderSupplier: ServerLifecycle? = null
+
+ /**
+ * Called by the system once after the Assistant binds to the service.
+ *
+ * @return the list of capabilities that this service supports.
+ */
+ abstract val registeredCapabilities: List<ActionCapability>
+
+ /**
+ * A list of [AppVerificationInfo] which define who is allowed to interact with the app's bound
+ * service. This gives control over which clients are allowed to communicate with the service.
+ *
+ * This is the default method for enforcing security and must be overridden. Developers should
+ * return an empty list should they choose to define their own security by way of overriding
+ * [.getSecurityPolicyFromAllowedList].
+ */
+ protected abstract val allowedApps: List<AppVerificationInfo>
+
+ /**
+ * Sets a custom [SecurityPolicy] for the gRPC service. This gives control over which clients
+ * are allowed to bind to your service.
+ *
+ * Overriding this property is **not** the preferred method for security enforcement. We
+ * recommend developers override [allowedApps] for security needs. Implementing your own
+ * security policy requires significant care, and an understanding of the details and pitfalls
+ * of Android security. If you choose to do so, we **strongly** recommend you get such a change
+ * reviewed by Android security experts.
+ */
+ protected open val getSecurityPolicy: SecurityPolicy
+ get() =
+ SecurityPolicies.anyOf(*getSecurityPolicyFromAllowedList(allowedApps).toTypedArray())
+
+ /**
+ * Sets a custom [SecurityPolicy] for the gRPC service given the client's allowed pairs of
+ * package names with corresponding Sha256 signatures. This gives control over which clients are
+ * allowed to bind to your service.
+ *
+ * A SecurityPolicy is returned per supported Assistant. Such as "Google Assistant", "Bixby",
+ * etc.
+ */
+ private fun getSecurityPolicyFromAllowedList(
+ verificationInfoList: List<AppVerificationInfo>
+ ): List<SecurityPolicy> {
+ val policies: MutableList<SecurityPolicy> = ArrayList()
+ if (verificationInfoList.isEmpty()) {
+ policies.add(SecurityPolicies.internalOnly())
+ return policies
+ }
+ for (verificationInfo in verificationInfoList) {
+ policies.add(
+ SecurityPolicies.oneOfSignatureSha256Hash(
+ this.packageManager,
+ verificationInfo.packageName,
+ verificationInfo.signatures
+ )
+ )
+ }
+ return policies
+ }
+
+ @CallSuper
+ override fun onCreate() {
+ super.onCreate()
+ val binderReceiver = IBinderReceiver()
+ val serverSecurityPolicy =
+ ServerSecurityPolicy.newBuilder()
+ .servicePolicy(AppInteractionServiceGrpc.SERVICE_NAME, getSecurityPolicy)
+ .build()
+ val server =
+ BinderServerBuilder.forAddress(AndroidComponentAddress.forContext(this), binderReceiver)
+ .securityPolicy(serverSecurityPolicy)
+ .intercept(RemoteViewsOverMetadataInterceptor())
+ .addService(AppInteractionServiceFactory.create(this))
+ .build()
+ binderSupplier = ServerLifecycle(server, binderReceiver)
+ }
+
+ override fun onBind(intent: Intent?): IBinder {
+ return binderSupplier!!.get()!!
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ if (binderSupplier != null) {
+ binderSupplier!!.shutdown()
+ }
+ super.onDestroy()
+ }
+
+ internal class ServerLifecycle(
+ private val server: Server,
+ private val receiver: IBinderReceiver
+ ) {
+ private var isServerStarted = false
+
+ fun get(): IBinder? {
+ synchronized(this) {
+ if (!isServerStarted) {
+ try {
+ isServerStarted = true
+ server.start()
+ } catch (ioe: IOException) {
+ Log.e(TAG, "Unable to start server $server", ioe)
+ }
+ }
+ return receiver.get()
+ }
+ }
+
+ fun shutdown() {
+ synchronized(this) {
+ if (isServerStarted) {
+ server.shutdownNow()
+ isServerStarted = false
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "AppInteractionService"
+ }
+}
diff --git a/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/AppVerificationInfoTest.kt b/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/AppVerificationInfoTest.kt
index 6e63e1c3..20c792f 100644
--- a/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/AppVerificationInfoTest.kt
+++ b/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/AppVerificationInfoTest.kt
@@ -26,7 +26,7 @@
@Test
fun builderPattern() {
- var verificationInfo: AppVerificationInfo =
+ val verificationInfo: AppVerificationInfo =
AppVerificationInfo.Builder()
.setPackageName("packageName")
.addSignature(listOf(ByteArray(5)))
diff --git a/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/UiSessionsTest.kt b/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/UiSessionsTest.kt
index ccd680b..ce816a0 100644
--- a/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/UiSessionsTest.kt
+++ b/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/UiSessionsTest.kt
@@ -21,6 +21,7 @@
import android.widget.RemoteViews
import androidx.appactions.interaction.capabilities.core.ActionExecutor
import androidx.appactions.interaction.capabilities.core.BaseSession
+import androidx.appactions.interaction.capabilities.core.ExecutionResult
import androidx.appactions.interaction.service.test.R
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -132,7 +133,9 @@
@Test
fun actionExecutor_hasUpdateUiExtension() {
- val actionExecutor = object : ActionExecutor<String, String> {}
+ val actionExecutor = ActionExecutor<String, String> {
+ ExecutionResult.getDefaultInstance()
+ }
actionExecutor.updateUi(remoteViewsUiResponse)
diff --git a/appsearch/appsearch-local-storage/api/current.txt b/appsearch/appsearch-local-storage/api/current.txt
index 7289580..7e20080 100644
--- a/appsearch/appsearch-local-storage/api/current.txt
+++ b/appsearch/appsearch-local-storage/api/current.txt
@@ -2,9 +2,20 @@
package androidx.appsearch.localstorage {
public class LocalStorage {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext);
method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
}
+ public static final class LocalStorage.GlobalSearchContext {
+ method public java.util.concurrent.Executor getWorkerExecutor();
+ }
+
+ public static final class LocalStorage.GlobalSearchContext.Builder {
+ ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
+ method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
+ method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+ }
+
public static final class LocalStorage.SearchContext {
method public String getDatabaseName();
method public java.util.concurrent.Executor getWorkerExecutor();
diff --git a/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt b/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
index 7289580..7e20080 100644
--- a/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
@@ -2,9 +2,20 @@
package androidx.appsearch.localstorage {
public class LocalStorage {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext);
method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
}
+ public static final class LocalStorage.GlobalSearchContext {
+ method public java.util.concurrent.Executor getWorkerExecutor();
+ }
+
+ public static final class LocalStorage.GlobalSearchContext.Builder {
+ ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
+ method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
+ method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+ }
+
public static final class LocalStorage.SearchContext {
method public String getDatabaseName();
method public java.util.concurrent.Executor getWorkerExecutor();
diff --git a/appsearch/appsearch-local-storage/api/restricted_current.txt b/appsearch/appsearch-local-storage/api/restricted_current.txt
index 7289580..7e20080 100644
--- a/appsearch/appsearch-local-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-local-storage/api/restricted_current.txt
@@ -2,9 +2,20 @@
package androidx.appsearch.localstorage {
public class LocalStorage {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext);
method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
}
+ public static final class LocalStorage.GlobalSearchContext {
+ method public java.util.concurrent.Executor getWorkerExecutor();
+ }
+
+ public static final class LocalStorage.GlobalSearchContext.Builder {
+ ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
+ method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
+ method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+ }
+
public static final class LocalStorage.SearchContext {
method public String getDatabaseName();
method public java.util.concurrent.Executor getWorkerExecutor();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 94e56cc..b862ce3 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -167,12 +167,7 @@
}
}
- /**
- * Contains information relevant to creating a global search session.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ /** Contains information relevant to creating a global search session. */
public static final class GlobalSearchContext {
final Context mContext;
final Executor mExecutor;
@@ -283,12 +278,13 @@
/**
* Opens a new {@link GlobalSearchSession} on this storage.
*
+ * <p>The {@link GlobalSearchSession} opened from this {@link LocalStorage} allows the user to
+ * search across all local databases within the {@link LocalStorage} of this app, however
+ * cross-app search is not possible with {@link LocalStorage}.
+ *
* <p>This process requires a native search library. If it's not created, the initialization
* process will create one.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
@NonNull GlobalSearchContext context) {
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
index 2684330..a4b94c1 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
@@ -16,14 +16,18 @@
package androidx.baselineprofile.gradle.consumer
+import javax.inject.Inject
import org.gradle.api.Action
-import org.gradle.api.Incubating
+import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
+import org.gradle.api.model.ObjectFactory
/**
* Allows specifying settings for the Baseline Profile Consumer Plugin.
*/
-open class BaselineProfileConsumerExtension {
+abstract class BaselineProfileConsumerExtension @Inject constructor(
+ objectFactory: ObjectFactory
+) : BaselineProfileVariantConfiguration {
companion object {
private const val EXTENSION_NAME = "baselineProfile"
@@ -38,12 +42,146 @@
}
}
+ val variants: NamedDomainObjectContainer<BaselineProfileVariantConfigurationImpl> =
+ objectFactory.domainObjectContainer(BaselineProfileVariantConfigurationImpl::class.java)
+
+ // Shortcut to access the "main" variant.
+ private val main: BaselineProfileVariantConfiguration = variants.create("main") {
+
+ // These are the default global settings.
+ it.mergeIntoMain = null
+ it.baselineProfileOutputDir = "generated/baselineProfiles"
+ it.enableR8BaselineProfileRewrite = false
+ it.saveInSrc = true
+ it.automaticGenerationDuringBuild = false
+ }
+
+ /**
+ * Controls the global [BaselineProfileVariantConfiguration.enableR8BaselineProfileRewrite].
+ * Note that this value is overridden by per variant configurations.
+ */
+ override var enableR8BaselineProfileRewrite: Boolean?
+ get() = main.enableR8BaselineProfileRewrite
+ set(value) {
+ main.enableR8BaselineProfileRewrite = value
+ }
+
+ /**
+ * Controls the global [BaselineProfileVariantConfiguration.saveInSrc].
+ * Note that this value is overridden by per variant configurations.
+ */
+ override var saveInSrc: Boolean?
+ get() = main.saveInSrc
+ set(value) {
+ main.saveInSrc = value
+ }
+
+ /**
+ * Controls the global [BaselineProfileVariantConfiguration.automaticGenerationDuringBuild].
+ * Note that this value is overridden by per variant configurations.
+ */
+ override var automaticGenerationDuringBuild: Boolean?
+ get() = main.automaticGenerationDuringBuild
+ set(value) {
+ main.automaticGenerationDuringBuild = value
+ }
+
+ /**
+ * Controls the global [BaselineProfileVariantConfiguration.baselineProfileOutputDir].
+ * Note that this value is overridden by per variant configurations.
+ */
+ override var baselineProfileOutputDir: String?
+ get() = main.baselineProfileOutputDir
+ set(value) {
+ main.baselineProfileOutputDir = value
+ }
+
+ /**
+ * Controls the global [BaselineProfileVariantConfiguration.mergeIntoMain].
+ * Note that this value is overridden by per variant configurations.
+ */
+ override var mergeIntoMain: Boolean?
+ get() = main.mergeIntoMain
+ set(value) {
+ main.mergeIntoMain = value
+ }
+
+ /**
+ * Applies the global [BaselineProfileVariantConfiguration.filter].
+ * This function is just a shortcut for `baselineProfiles.variants.main.filters { }`
+ */
+ override fun filter(action: FilterRules.() -> (Unit)) = main.filter(action)
+
+ /**
+ * Applies the global [BaselineProfileVariantConfiguration.filter].
+ * This function is just a shortcut for `baselineProfiles.variants.main.filters { }`
+ */
+ override fun filter(action: Action<FilterRules>) = main.filter(action)
+
+ /**
+ * Applies global dependencies for baseline profiles. This has the same effect of defining
+ * a baseline profile dependency in the dependency block. For example:
+ * ```
+ * dependencies {
+ * baselineProfile(project(":baseline-profile"))
+ * }
+ * ```
+ */
+ override fun from(project: Project, variantName: String?) = main.from(project, variantName)
+
+ fun variants(
+ action: Action<NamedDomainObjectContainer<BaselineProfileVariantConfigurationImpl>>
+ ) {
+ action.execute(variants)
+ }
+
+ fun variants(
+ action: NamedDomainObjectContainer<out BaselineProfileVariantConfigurationImpl>.() -> Unit
+ ) {
+ action.invoke(variants)
+ }
+}
+
+abstract class BaselineProfileVariantConfigurationImpl(val name: String) :
+ BaselineProfileVariantConfiguration {
+
+ internal val filters = FilterRules()
+ internal val dependencies = mutableListOf<Pair<Project, String?>>()
+
+ /**
+ * @inheritDoc
+ */
+ override fun filter(action: FilterRules.() -> (Unit)) = action.invoke(filters)
+
+ /**
+ * @inheritDoc
+ */
+ override fun filter(action: Action<FilterRules>) = action.execute(filters)
+
+ /**
+ * @inheritDoc
+ */
+ override fun from(project: Project, variantName: String?) {
+ dependencies.add(Pair(project, variantName))
+ }
+}
+
+interface BaselineProfileVariantConfiguration {
+
+ /**
+ * Enables R8 to rewrite the incoming human readable baseline profile rules to account for
+ * synthetics, so they are preserved after optimizations by R8.
+ * TODO: This feature is experimental and currently not working properly.
+ * https://issuetracker.google.com/issue?id=271172067.
+ */
+ var enableR8BaselineProfileRewrite: Boolean?
+
/**
* Specifies whether generated baseline profiles should be stored in the src folder.
* When this flag is set to true, the generated baseline profiles are stored in
* `src/<variant>/generated/baselineProfiles`.
*/
- var saveInSrc = true
+ var saveInSrc: Boolean?
/**
* Specifies whether baseline profiles should be regenerated when building, for example, during
@@ -51,13 +189,14 @@
* of building the release build. This including rebuilding the non minified release, running
* the baseline profile tests and ultimately building the release build.
*/
- var automaticGenerationDuringBuild = false
+ var automaticGenerationDuringBuild: Boolean?
/**
- * Specifies the output directory for generated baseline profiles when [saveInSrc] is
- * `true`. Note that the dir specified here is created in the `src/<variant>/` folder.
+ * Specifies the output directory for generated baseline profiles when
+ * [BaselineProfileVariantConfiguration.saveInSrc] is `true`.
+ * Note that the dir specified here is created in the `src/<variant>/` folder.
*/
- var baselineProfileOutputDir = "generated/baselineProfiles"
+ var baselineProfileOutputDir: String?
/**
* Specifies if baseline profile files should be merged into a single one when generating for
@@ -72,16 +211,7 @@
* this setting still determines whether the profile included in the built apk or
* aar includes all the variant profiles.
*/
- var mergeIntoMain: Boolean? = null
-
- /**
- * Enables R8 to rewrite the incoming human readable baseline profile rules to account for
- * synthetics, so they are preserved after optimizations by R8.
- * TODO: This feature is experimental and currently not working properly.
- * https://issuetracker.google.com/issue?id=271172067.
- */
- @Incubating
- var enableR8BaselineProfileRewrite = false
+ var mergeIntoMain: Boolean?
/**
* Specifies a filtering rule to decide which profiles rules should be included in this
@@ -115,24 +245,8 @@
* exclude "com.somelibrary.widget.grid.debug.**"
* }
* ```
- *
- * Filters also support variants and they can be expressed as follows:
- * ```
- * filter { include "com.somelibrary.*" }
- * filter("free") { include "com.somelibrary.*" }
- * filter("paid") { include "com.somelibrary.*" }
- * filter("release") { include "com.somelibrary.*" }
- * filter("freeRelease") { include "com.somelibrary.*" }
- * ```
- * Filter block without specifying a variant applies to `main`, i.e. all the variants.
- * Note that when a variant matches multiple filter blocks, all the filters will be merged.
- * For example with `filter { ... }`, `filter("free") { ... }` and `filter("release") { ... }`
- * all the blocks will be evaluated for variant `freeRelease` but only `main` and `release` for
- * variant `paidRelease`.
*/
- @JvmOverloads
- fun filter(variant: String = "main", action: FilterRules.() -> (Unit)) = action
- .invoke(filterRules.computeIfAbsent(variant) { FilterRules() })
+ fun filter(action: FilterRules.() -> (Unit))
/**
* Specifies a filtering rule to decide which profiles rules should be included in this
@@ -166,30 +280,46 @@
* exclude "com.somelibrary.widget.text.debug.**"
* }
* ```
- *
- * Filters also support variants and they can be expressed as follows:
- * ```
- * filter { include "com.somelibrary.*" }
- * filter("free") { include "com.somelibrary.*" }
- * filter("paid") { include "com.somelibrary.*" }
- * filter("release") { include "com.somelibrary.*" }
- * filter("freeRelease") { include "com.somelibrary.*" }
- * ```
- * Filter block without specifying a variant applies to `main`, i.e. all the variants.
- * Note that when a variant matches multiple filter blocks, all the filters will be merged.
- * For example with `filter { ... }`, `filter("free") { ... }` and `filter("release") { ... }`
- * all the blocks will be evaluated for variant `freeRelease` but only `main` and `release` for
- * variant `paidRelease`.
*/
- @JvmOverloads
- fun filter(variant: String = "main", action: Action<FilterRules>) = action
- .execute(filterRules.computeIfAbsent(variant) { FilterRules() })
+ fun filter(action: Action<FilterRules>)
- internal val filterRules = mutableMapOf<String, FilterRules>()
+ /**
+ * Allows to specify a target `com.android.test` module that has the `androidx.baselineprofile`
+ * plugin, and that can provide a baseline profile for this module. For example
+ * ```
+ * baselineProfile {
+ * variants {
+ * freeRelease {
+ * from(project(":baseline-profile"))
+ * }
+ * }
+ * }
+ * ```
+ */
+ fun from(project: Project) = from(project, null)
+
+ /**
+ * Allows to specify a target `com.android.test` module that has the `androidx.baselineprofile`
+ * plugin, and that can provide a baseline profile for this module. The [variantName] can
+ * directly map to a test variant, to fetch a baseline profile for a different variant.
+ * For example it's possible to use a `paidRelease` baseline profile for `freeRelease` variant.
+ * ```
+ * baselineProfile {
+ * variants {
+ * freeRelease {
+ * from(project(":baseline-profile"), "paidRelease")
+ * }
+ * }
+ * }
+ * ```
+ */
+ fun from(project: Project, variantName: String?)
}
class FilterRules {
+
internal val rules = mutableListOf<Pair<RuleType, String>>()
+
fun include(pkg: String) = rules.add(Pair(RuleType.INCLUDE, pkg))
fun exclude(pkg: String) = rules.add(Pair(RuleType.EXCLUDE, pkg))
}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
index bebdaa7..3968173d 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
@@ -124,7 +124,8 @@
variantName = "",
flavorName = "",
buildTypeName = "",
- mainConfiguration = null
+ mainConfiguration = null,
+ hasDirectConfiguration = false
)
// Here we select the build types we want to process, i.e. non debuggable build types that
@@ -180,8 +181,15 @@
if (variant.buildType !in nonDebuggableBuildTypes) return@onVariants
+ // For test only: this registers a print task with the configuration of the
+ // variant.
+ baselineProfileExtension
+ .registerPrintConfigurationTaskForVariant(project, variant)
+
// Sets the r8 rewrite baseline profile for the non debuggable variant.
- if (baselineProfileExtension.enableR8BaselineProfileRewrite &&
+ val enableR8Rewrite = baselineProfileExtension
+ .getValueForVariant(variant) { enableR8BaselineProfileRewrite }
+ if (enableR8Rewrite &&
project.agpVersion() >= AndroidPluginVersion(8, 0, 0).beta(2)
) {
// TODO: Note that currently there needs to be at least a baseline profile,
@@ -191,10 +199,14 @@
@Suppress("UnstableApiUsage")
variant.experimentalProperties.put(
PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES,
- baselineProfileExtension.enableR8BaselineProfileRewrite
+ enableR8Rewrite
)
}
+ // Check if this variant has any direct dependency
+ val variantDependencies = baselineProfileExtension
+ .getMergedListValuesForVariant(variant) { dependencies }
+
// Creates the configuration to carry the specific variant artifact
val baselineProfileConfiguration =
createBaselineProfileConfigurationForVariant(
@@ -203,9 +215,32 @@
productFlavors = variant.productFlavors,
flavorName = variant.flavorName ?: "",
buildTypeName = variant.buildType ?: "",
- mainConfiguration = mainBaselineProfileConfiguration
+ mainConfiguration = mainBaselineProfileConfiguration,
+ hasDirectConfiguration = variantDependencies.any { it.second != null }
)
+ // Adds the custom dependencies for baseline profiles. Note that dependencies
+ // for global, build type, flavor and variant specific are all merged.
+ variantDependencies.forEach {
+ val targetProject = it.first
+ val variantName = it.second
+ val targetProjectDependency = if (variantName != null) {
+ val configurationName = camelCase(
+ variantName,
+ CONFIGURATION_NAME_BASELINE_PROFILES
+ )
+ project.dependencies.project(
+ mutableMapOf(
+ "path" to targetProject.path,
+ "configuration" to configurationName
+ )
+ )
+ } else {
+ project.dependencyFactory.create(targetProject)
+ }
+ baselineProfileConfiguration.dependencies.add(targetProjectDependency)
+ }
+
// There are 2 different ways in which the output task can merge the baseline
// profile rules, according to [BaselineProfileConsumerExtension#mergeIntoMain].
// When mergeIntoMain is `true` the first variant will create a task shared across
@@ -215,7 +250,8 @@
// artifact per task, specific for that variant.
// When mergeIntoMain is not specified, it's by default true for libraries and false
// for apps.
- val mergeIntoMain = baselineProfileExtension.mergeIntoMain ?: !isApplication
+ val mergeIntoMain = baselineProfileExtension
+ .getValueForVariant(variant, default = !isApplication) { mergeIntoMain }
// TODO: When `mergeIntoMain` is true it lazily triggers the generation of all
// the variants for all the build types. Due to b/265438201, that fails when
@@ -263,27 +299,22 @@
// is always stored in the intermediates
task.baselineProfileDir.set(mergedTaskOutputDir)
- // Sets the package filter rules. If this is the first task
+ // Sets the package filter rules. Note that variant rules are merged
+ // with global rules here.
task.filterRules.addAll(
- baselineProfileExtension.filterRules
- .filter {
- it.key in listOfNotNull(
- "main",
- variant.flavorName,
- variant.buildType,
- variant.name
- )
- }
- .flatMap { it.value.rules }
+ baselineProfileExtension
+ .getMergedListValuesForVariant(variant) { filters.rules }
)
}
// If `saveInSrc` is true, we create an additional task to copy the output
// of the merge task in the src folder.
- val lastTaskProvider = if (baselineProfileExtension.saveInSrc) {
+ val saveInSrc = baselineProfileExtension
+ .getValueForVariant(variant) { saveInSrc }
+ val lastTaskProvider = if (saveInSrc) {
- val baselineProfileOutputDir =
- baselineProfileExtension.baselineProfileOutputDir
+ val baselineProfileOutputDir = baselineProfileExtension
+ .getValueForVariant(variant) { baselineProfileOutputDir }
val srcOutputDir = project
.layout
.projectDirectory
@@ -313,31 +344,39 @@
.baselineProfiles?.addStaticSourceDirectory(absolutePath)
}
+ // If this is an application, we need to ensure that:
+ // If `automaticGenerationDuringBuild` is true, building a release build
+ // should trigger the generation of the profile. This is done through a
+ // dependsOn rule.
+ // If `automaticGenerationDuringBuild` is false and the user calls both
+ // tasks to generate and assemble, assembling the release should wait of the
+ // generation to be completed. This is done through a `mustRunAfter` rule.
// Depending on whether the flag `automaticGenerationDuringBuild` is enabled
- // we can set either a dependsOn or a mustRunAfter dependency between the
- // task that packages the profile and the copy. Note that we cannot use
- // the variant src set api `addGeneratedSourceDirectory` since that
- // overwrites the outputDir, that would be re-set in the build dir.
- afterVariantBlocks.add {
-
- // Determines which AGP task to depend on based on whether this is an
- // app or a library.
- if (isApplication) {
- project.tasks.named(
- camelCase("merge", variant.name, "artProfile")
- )
- } else {
- project.tasks.named(
- camelCase("prepare", variant.name, "artProfile")
- )
- }.configure {
-
- // Sets the task dependency according to the configuration flag.
- if (baselineProfileExtension.automaticGenerationDuringBuild) {
- it.dependsOn(copyTaskProvider)
- } else {
- it.mustRunAfter(copyTaskProvider)
- }
+ // Note that we cannot use the variant src set api
+ // `addGeneratedSourceDirectory` since that overwrites the outputDir,
+ // that would be re-set in the build dir.
+ // Also this is specific for applications: doing this for a library would
+ // trigger a circular task dependency since the library would require
+ // the profile in order to build the aar for the sample app and generate
+ // the profile.
+ if (isApplication) {
+ afterVariantBlocks.add {
+ project
+ .tasks
+ .named(camelCase("merge", variant.name, "artProfile"))
+ .configure {
+ // Sets the task dependency according to the configuration
+ // flag.
+ val automaticGeneration = baselineProfileExtension
+ .getValueForVariant(variant) {
+ automaticGenerationDuringBuild
+ }
+ if (automaticGeneration) {
+ it.dependsOn(copyTaskProvider)
+ } else {
+ it.mustRunAfter(copyTaskProvider)
+ }
+ }
}
}
@@ -345,8 +384,9 @@
copyTaskProvider
} else {
- if (baselineProfileExtension.automaticGenerationDuringBuild) {
-
+ val automaticGeneration = baselineProfileExtension
+ .getValueForVariant(variant) { automaticGenerationDuringBuild }
+ if (automaticGeneration) {
// If the flag `automaticGenerationDuringBuild` is true, we can set the
// merge task to provide generated sources for the variant, using the
// src set variant api. This means that we don't need to manually depend
@@ -440,7 +480,8 @@
productFlavors: List<Pair<String, String>>,
flavorName: String,
buildTypeName: String,
- mainConfiguration: Configuration?
+ mainConfiguration: Configuration?,
+ hasDirectConfiguration: Boolean
): Configuration {
val buildTypeConfiguration =
@@ -484,6 +525,10 @@
isCanBeResolved = true
isCanBeConsumed = false
+ // Skip the attributes configuration if there is a direct named configuration
+ // matching this one.
+ if (hasDirectConfiguration) return@apply
+
attributes {
// Main specialized attribute
@@ -540,6 +585,115 @@
}
}
}
+
+ private fun <T> BaselineProfileConsumerExtension.getMergedListValuesForVariant(
+ variant: Variant,
+ getter: BaselineProfileVariantConfigurationImpl.() -> (List<T>)
+ ): List<T> =
+ listOfNotNull("main", variant.flavorName, variant.buildType, variant.name)
+ .mapNotNull { variants.findByName(it) }
+ .map { getter.invoke(it) }
+ .flatten()
+
+ private fun <T> BaselineProfileConsumerExtension.getValueForVariant(
+ variant: Variant,
+ default: T? = null,
+ getter: BaselineProfileVariantConfigurationImpl.() -> (T?)
+ ): T {
+
+ // Here we select a setting for the given variant. [BaselineProfileVariantConfiguration]
+ // are evaluated in the following order: variant, flavor, build type, `main`.
+ // If a property is found it will return it. Note that `main` should have all the defaults
+ // set so this method never returns a nullable value and should always return.
+
+ val definedProperties = listOfNotNull(
+ variant.name,
+ variant.flavorName,
+ variant.buildType,
+ "main"
+ ).mapNotNull {
+ val variantConfig = variants.findByName(it) ?: return@mapNotNull null
+ return@mapNotNull Pair(it, getter.invoke(variantConfig))
+ }.filter { it.second != null }
+
+ // This is a case where the property is defined in both build type and flavor.
+ // In this case it should fail because the result is ambiguous.
+ val propMap = definedProperties.toMap()
+ if (variant.flavorName in propMap &&
+ variant.buildType in propMap &&
+ propMap[variant.flavorName] != propMap[variant.buildType]
+ ) {
+ throw GradleException(
+ """
+ The per-variant configuration for baseline profiles is ambiguous. This happens when
+ that the same property has been defined in both a build type and a flavor.
+
+ For example:
+
+ baselineProfiles {
+ variants {
+ free {
+ saveInSrc = true
+ }
+ release {
+ saveInSrc = false
+ }
+ }
+ }
+
+ In this case for `freeRelease` it's not possible to determine the exact value of the
+ property. Please specify either the build type or the flavor.
+ """.trimIndent()
+ )
+ }
+
+ val value = definedProperties.firstOrNull()?.second
+ if (value != null) {
+ return value
+ }
+ if (default != null) {
+ return default
+ }
+
+ // This should never happen. It means the extension is missing a default property and no
+ // default was specified when accessing this value. This cannot happen because of the user
+ // configuration.
+ throw GradleException("The required property does not have a default.")
+ }
+
+ fun BaselineProfileConsumerExtension.registerPrintConfigurationTaskForVariant(
+ project: Project,
+ variant: Variant
+ ) {
+ project
+ .tasks
+ .maybeRegister<PrintConfigurationForVariant>(
+ "printBaselineProfileExtensionForVariant",
+ variant.name
+ ) {
+ it.text.set(
+ """
+ mergeIntoMain=`${getValueForVariant(variant, default = "null") { mergeIntoMain }}`
+ baselineProfileOutputDir=`${getValueForVariant(variant) { baselineProfileOutputDir }}`
+ enableR8BaselineProfileRewrite=`${getValueForVariant(variant) { enableR8BaselineProfileRewrite }}`
+ saveInSrc=`${getValueForVariant(variant) { saveInSrc }}`
+ automaticGenerationDuringBuild=`${getValueForVariant(variant) { automaticGenerationDuringBuild }}`
+ """.trimIndent()
+ )
+ }
+ }
+}
+
+@DisableCachingByDefault(because = "Not worth caching. Used only for tests.")
+abstract class PrintConfigurationForVariant : DefaultTask() {
+
+ @get: Input
+ abstract val text: Property<String>
+
+ @TaskAction
+ fun exec() {
+ logger.warn(text.get())
+ }
}
@DisableCachingByDefault(because = "Not worth caching.")
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index 6342c02..d7388b8 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -181,9 +181,9 @@
val originalBuildTypeName = extendedTypeToOriginalTypeMapping[it.buildType] ?: ""
val configurationName = createBaselineProfileConfigurationForVariant(
project = project,
- variantName = it.name,
productFlavors = it.productFlavors,
- originalBuildTypeName = originalBuildTypeName
+ originalBuildTypeName = originalBuildTypeName,
+ flavorName = it.flavorName
)
// Prepares a block to execute later that creates the tasks for this variant
@@ -292,12 +292,13 @@
private fun createBaselineProfileConfigurationForVariant(
project: Project,
- variantName: String,
+ flavorName: String?,
productFlavors: List<Pair<String, String>>,
originalBuildTypeName: String,
): String {
+
val configurationName =
- camelCase(variantName, CONFIGURATION_NAME_BASELINE_PROFILES)
+ camelCase(flavorName ?: "", originalBuildTypeName, CONFIGURATION_NAME_BASELINE_PROFILES)
project.configurations
.maybeCreate(configurationName)
.apply {
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
index 5b925ae..d91619c 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
@@ -531,6 +531,241 @@
}
}
+ @Test
+ fun testVariantConfigurationOverrideForFlavors() {
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+ setupConsumerProject(
+ androidPlugin = "com.android.library",
+ flavors = true,
+ baselineProfileBlock = """
+
+ // Global configuration
+ enableR8BaselineProfileRewrite = false
+ saveInSrc = true
+ automaticGenerationDuringBuild = false
+ baselineProfileOutputDir = "generated/baselineProfiles"
+ mergeIntoMain = true
+
+ // Per variant configuration overrides global configuration.
+ variants {
+ free {
+ enableR8BaselineProfileRewrite = true
+ saveInSrc = false
+ automaticGenerationDuringBuild = true
+ baselineProfileOutputDir = "somefolder"
+ mergeIntoMain = false
+ }
+ paidRelease {
+ enableR8BaselineProfileRewrite = true
+ saveInSrc = false
+ automaticGenerationDuringBuild = true
+ baselineProfileOutputDir = "someOtherfolder"
+ mergeIntoMain = false
+ }
+ }
+
+ """.trimIndent()
+ )
+
+ gradleRunner.buildAndAssertThatOutput(
+ "printBaselineProfileExtensionForVariantFreeRelease"
+ ) {
+ contains("enableR8BaselineProfileRewrite=`true`")
+ contains("saveInSrc=`false`")
+ contains("automaticGenerationDuringBuild=`true`")
+ contains("baselineProfileOutputDir=`somefolder`")
+ contains("mergeIntoMain=`false`")
+ }
+
+ gradleRunner.buildAndAssertThatOutput(
+ "printBaselineProfileExtensionForVariantPaidRelease"
+ ) {
+ contains("enableR8BaselineProfileRewrite=`true`")
+ contains("saveInSrc=`false`")
+ contains("automaticGenerationDuringBuild=`true`")
+ contains("baselineProfileOutputDir=`someOtherfolder`")
+ contains("mergeIntoMain=`false`")
+ }
+ }
+
+ @Test
+ fun testVariantConfigurationOverrideForBuildTypes() {
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+ setupConsumerProject(
+ androidPlugin = "com.android.library",
+ flavors = true,
+ baselineProfileBlock = """
+
+ // Global configuration
+ enableR8BaselineProfileRewrite = false
+ saveInSrc = true
+ automaticGenerationDuringBuild = false
+ baselineProfileOutputDir = "generated/baselineProfiles"
+ mergeIntoMain = true
+
+ // Per variant configuration overrides global configuration.
+ variants {
+ release {
+ enableR8BaselineProfileRewrite = true
+ saveInSrc = false
+ automaticGenerationDuringBuild = true
+ baselineProfileOutputDir = "myReleaseFolder"
+ mergeIntoMain = false
+ }
+ paidRelease {
+ enableR8BaselineProfileRewrite = true
+ saveInSrc = false
+ automaticGenerationDuringBuild = true
+ baselineProfileOutputDir = "someOtherfolder"
+ mergeIntoMain = false
+ }
+ }
+
+ """.trimIndent()
+ )
+
+ gradleRunner.buildAndAssertThatOutput(
+ "printBaselineProfileExtensionForVariantFreeRelease"
+ ) {
+ contains("enableR8BaselineProfileRewrite=`true`")
+ contains("saveInSrc=`false`")
+ contains("automaticGenerationDuringBuild=`true`")
+ contains("baselineProfileOutputDir=`myReleaseFolder`")
+ contains("mergeIntoMain=`false`")
+ }
+
+ gradleRunner.buildAndAssertThatOutput(
+ "printBaselineProfileExtensionForVariantPaidRelease"
+ ) {
+ contains("enableR8BaselineProfileRewrite=`true`")
+ contains("saveInSrc=`false`")
+ contains("automaticGenerationDuringBuild=`true`")
+ contains("baselineProfileOutputDir=`someOtherfolder`")
+ contains("mergeIntoMain=`false`")
+ }
+ }
+
+ @Test
+ fun testVariantConfigurationOverrideForFlavorsAndBuildType() {
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+ setupConsumerProject(
+ androidPlugin = "com.android.library",
+ flavors = true,
+ baselineProfileBlock = """
+ variants {
+ free {
+ saveInSrc = true
+ }
+ release {
+ saveInSrc = false
+ }
+ }
+
+ """.trimIndent()
+ )
+ gradleRunner
+ .withArguments("printBaselineProfileExtensionForVariantFreeRelease", "--stacktrace")
+ .buildAndFail()
+ .output
+ .let {
+ assertThat(it)
+ .contains("The per-variant configuration for baseline profiles is ambiguous")
+ }
+ }
+
+ @Test
+ fun testVariantDependenciesWithFlavors() {
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+
+ // In this setup no dependency is being added through the dependency block.
+ // Instead dependencies are being added through per-variant configuration block.
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ dependencyOnProducerProject = false,
+ baselineProfileBlock = """
+ variants {
+ free {
+ from(project(":$producerModuleName"))
+ }
+ paid {
+ from(project(":$producerModuleName"))
+ }
+ }
+
+ """.trimIndent()
+ )
+ gradleRunner
+ .withArguments("generateReleaseBaselineProfile", "--stacktrace")
+ .build()
+
+ assertThat(readBaselineProfileFileContent("freeRelease"))
+ .containsExactly(
+ Fixtures.CLASS_1,
+ Fixtures.CLASS_1_METHOD_1,
+ )
+ assertThat(readBaselineProfileFileContent("paidRelease"))
+ .containsExactly(
+ Fixtures.CLASS_2,
+ Fixtures.CLASS_2_METHOD_1,
+ )
+ }
+
+ @Test
+ fun testVariantDependenciesWithVariantsAndDirectConfiguration() {
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+
+ // In this setup no dependency is being added through the dependency block.
+ // Instead dependencies are being added through per-variant configuration block.
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ dependencyOnProducerProject = false,
+ baselineProfileBlock = """
+ variants {
+ freeRelease {
+ from(project(":$producerModuleName"))
+ }
+ paidRelease {
+ from(project(":$producerModuleName"), "freeRelease")
+ }
+ }
+
+ """.trimIndent()
+ )
+ gradleRunner
+ .withArguments("generateReleaseBaselineProfile", "--stacktrace")
+ .build()
+
+ assertThat(readBaselineProfileFileContent("freeRelease"))
+ .containsExactly(
+ Fixtures.CLASS_1,
+ Fixtures.CLASS_1_METHOD_1,
+ )
+
+ // This output should be the same of free release
+ assertThat(readBaselineProfileFileContent("paidRelease"))
+ .containsExactly(
+ Fixtures.CLASS_1,
+ Fixtures.CLASS_1_METHOD_1,
+ )
+ }
+
private fun setupConsumerProject(
androidPlugin: String,
flavors: Boolean = false,
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index d8d5116..61577bd 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -155,3 +155,42 @@
}
+package androidx.benchmark.perfetto {
+
+ @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
+ }
+
+ @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
+ ctor public PerfettoTraceProcessor();
+ method public <T> T! loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
+ method public static <T> T! runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
+ field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
+ }
+
+ public static final class PerfettoTraceProcessor.Companion {
+ method public <T> T! runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
+ }
+
+ public static final class PerfettoTraceProcessor.Session {
+ method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
+ method public byte[] queryBytes(@org.intellij.lang.annotations.Language("sql") String query);
+ }
+
+ @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object> {
+ ctor public Row(java.util.Map<java.lang.String,?> map);
+ method public byte[] bytes(String columnName);
+ method public double double(String columnName);
+ method public long long(String columnName);
+ method public byte[]? nullableBytes(String columnName);
+ method public Double? nullableDouble(String columnName);
+ method public Long? nullableLong(String columnName);
+ method public String? nullableString(String columnName);
+ method public String string(String columnName);
+ }
+
+ public final class RowKt {
+ method @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,?>... pairs);
+ }
+
+}
+
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 9edcc9e..d167114 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -86,10 +86,13 @@
description = "Android Benchmark - Macrobenchmark"
}
-// Allow usage of Kotlin's @OptIn.
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
- freeCompilerArgs += ['-opt-in=kotlin.RequiresOptIn']
+ // Enable using experimental APIs from within same version group
+ freeCompilerArgs += [
+ "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
+ "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi"
+ ]
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
index 4b2a453..6c10384 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
@@ -17,10 +17,10 @@
package androidx.benchmark.macro
import androidx.benchmark.junit4.PerfettoTraceRule
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.tracing.trace
@@ -55,9 +55,12 @@
base.evaluate()
if (PerfettoHelper.isAbiSupported()) {
assertNotNull(trace)
- val sliceNameInstances = PerfettoTraceProcessor.runServer(trace!!.path) {
+ val sliceNameInstances = PerfettoTraceProcessor.runSingleSessionServer(
+ trace!!.path
+ ) {
querySlices(UNIQUE_SLICE_NAME)
- }.map { slice -> slice.name }
+ .map { slice -> slice.name }
+ }
assertEquals(listOf(UNIQUE_SLICE_NAME), sliceNameInstances)
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
index 329b736..5ab38e0 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
@@ -18,8 +18,8 @@
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Test
@@ -42,7 +42,7 @@
val categories = PowerCategory.values()
.associateWith { PowerCategoryDisplayLevel.BREAKDOWN }
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Energy(categories)).getMetrics(captureInfo, this)
}
@@ -80,7 +80,7 @@
val categories = PowerCategory.values()
.associateWith { PowerCategoryDisplayLevel.TOTAL }
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Power(categories)).getMetrics(captureInfo, this)
}
@@ -115,7 +115,7 @@
PowerCategory.UNCATEGORIZED to PowerCategoryDisplayLevel.BREAKDOWN
)
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Power(categories)).getMetrics(captureInfo, this)
}
@@ -144,7 +144,7 @@
val categories = PowerCategory.values()
.associateWith { PowerCategoryDisplayLevel.BREAKDOWN }
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Energy(categories)).getMetrics(captureInfo, this)
}
@@ -163,7 +163,7 @@
val traceFile = createTempFileFromAsset("api31_battery_discharge", ".perfetto-trace")
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerMetric(PowerMetric.Battery()).getMetrics(captureInfo, this)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index a57afdd..9f72b02 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -22,9 +22,9 @@
import androidx.annotation.RequiresApi
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Outputs
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -204,7 +204,7 @@
val packageName = "androidx.benchmark.integration.macrobenchmark.target"
metric.configure(packageName)
- return PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ return PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
metric.getMetrics(
captureInfo = Metric.CaptureInfo(
targetPackageName = "androidx.benchmark.integration.macrobenchmark.target",
@@ -212,7 +212,7 @@
startupMode = StartupMode.WARM,
apiLevel = 32
),
- perfettoTraceProcessor = this
+ session = this
)
}
}
@@ -228,7 +228,7 @@
val metric = StartupTimingMetric()
metric.configure(Packages.TEST)
- val metrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val metrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
metric.getMetrics(
captureInfo = Metric.CaptureInfo(
targetPackageName = Packages.TEST,
@@ -236,7 +236,7 @@
startupMode = StartupMode.WARM,
apiLevel = 24
),
- perfettoTraceProcessor = this
+ session = this
)
}
@@ -300,7 +300,7 @@
block = measureBlock
)!!
- return PerfettoTraceProcessor.runServer(tracePath) {
+ return PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
metric.getMetrics(
captureInfo = Metric.CaptureInfo(
targetPackageName = packageName,
@@ -308,7 +308,7 @@
startupMode = startupMode,
apiLevel = Build.VERSION.SDK_INT
),
- perfettoTraceProcessor = this
+ session = this
)
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
index 98120bd..698ef07 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/TraceSectionMetricTest.kt
@@ -16,8 +16,8 @@
package androidx.benchmark.macro
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.filters.MediumTest
import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
@@ -106,10 +106,10 @@
val expectedKey = sectionName + "Ms"
metric.configure(packageName = packageName)
- val iterationResult = PerfettoTraceProcessor.runServer(tracePath) {
+ val iterationResult = PerfettoTraceProcessor.runSingleSessionServer(tracePath) {
metric.getMetrics(
captureInfo = captureInfo,
- perfettoTraceProcessor = this
+ session = this
)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
index 6bfe76a..657a201 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
@@ -21,11 +21,15 @@
import androidx.benchmark.perfetto.PerfettoCapture
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.toSlices
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.tracing.Trace
import androidx.tracing.trace
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
@@ -33,8 +37,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
/**
* Tests for androidx.tracing.Trace, which validate actual trace content
@@ -94,8 +96,8 @@
perfettoCapture.stop(traceFilePath)
- val queryResult = PerfettoTraceProcessor.runServer(traceFilePath) {
- rawQuery(query = QUERY)
+ val queryResult = PerfettoTraceProcessor.runSingleSessionServer(traceFilePath) {
+ query(query = QUERY)
}
val matchingSlices = queryResult.toSlices()
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
index 5798301..2588611 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AudioUnderrunQueryTest.kt
@@ -18,13 +18,14 @@
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertEquals
@SdkSuppress(minSdkVersion = 23)
@RunWith(AndroidJUnit4::class)
@@ -37,7 +38,7 @@
// the trace was generated during 2 seconds AudioUnderrunBenchmark scenario run
val traceFile = createTempFileFromAsset("api23_audio_underrun", ".perfetto-trace")
- val subMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val subMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
AudioUnderrunQuery.getSubMetrics(this)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
index eb91d3c..59411dc 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
@@ -22,6 +22,7 @@
import androidx.benchmark.macro.PowerMetric
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -40,7 +41,7 @@
val traceFile = createTempFileFromAsset("api31_battery_discharge", ".perfetto-trace")
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
val slice = querySlices(PowerMetric.MEASURE_BLOCK_SECTION_NAME).first()
BatteryDischargeQuery.getBatteryDischargeMetrics(this, slice)
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
index 35e287c..66ed720 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
@@ -18,15 +18,16 @@
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationCpuNs
-import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameOverrunNs
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationUiNs
+import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameOverrunNs
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
-import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
class FrameTimingQueryTest {
@@ -36,9 +37,11 @@
assumeTrue(isAbiSupported())
val traceFile = createTempFileFromAsset("api28_scroll", ".perfetto-trace")
- val frameSubMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val frameSubMetrics = PerfettoTraceProcessor.runSingleSessionServer(
+ traceFile.absolutePath
+ ) {
FrameTimingQuery.getFrameSubMetrics(
- perfettoTraceProcessor = this,
+ session = this,
captureApiLevel = 28,
packageName = "androidx.benchmark.integration.macrobenchmark.target"
)
@@ -66,9 +69,11 @@
assumeTrue(isAbiSupported())
val traceFile = createTempFileFromAsset("api31_scroll", ".perfetto-trace")
- val frameSubMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val frameSubMetrics = PerfettoTraceProcessor.runSingleSessionServer(
+ traceFile.absolutePath
+ ) {
FrameTimingQuery.getFrameSubMetrics(
- perfettoTraceProcessor = this,
+ session = this,
captureApiLevel = 31,
packageName = "androidx.benchmark.integration.macrobenchmark.target"
)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index f8eb993..8797a72 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -23,28 +23,29 @@
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOWEST_BUNDLED_VERSION_SUPPORTED
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.testutils.verifyWithPolling
import androidx.tracing.Trace
import androidx.tracing.trace
+import kotlin.test.assertEquals
+import kotlin.test.fail
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-import kotlin.test.assertEquals
-import kotlin.test.fail
-import org.junit.Ignore
/**
* Trace validation tests for PerfettoCapture
*
* Note: this test is defined in benchmark-macro instead of benchmark-common so that it can
- * validate trace contents with PerfettoTraceProcessor
+ * validate trace contents with TraceProcessor
*/
@SdkSuppress(minSdkVersion = 23)
@LargeTest
@@ -119,7 +120,7 @@
perfettoCapture.stop(traceFilePath)
- val matchingSlices = PerfettoTraceProcessor.runServer(traceFilePath) {
+ val matchingSlices = PerfettoTraceProcessor.runSingleSessionServer(traceFilePath) {
querySlices("PerfettoCaptureTest_%")
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
index 02ffea2..70bdeb59 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
@@ -20,6 +20,7 @@
import androidx.benchmark.junit4.PerfettoTraceRule
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.tracing.trace
@@ -56,7 +57,7 @@
if (enableAppTagTracing) yield(StringSource.appTagTraceStrings)
if (enableUserspaceTracing) yield(StringSource.userspaceTraceStrings)
}.flatMap { it }.toList()
- val actualSlices = PerfettoTraceProcessor.runServer(trace.path) {
+ val actualSlices = PerfettoTraceProcessor.runSingleSessionServer(trace.path) {
StringSource.allTraceStrings.flatMap { querySlices(it).map { s -> s.name } }
}
assertThat(actualSlices).containsExactlyElementsIn(expectedSlices)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt
deleted file mode 100644
index d074851..0000000
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.macro.perfetto
-
-import androidx.benchmark.Shell
-import androidx.benchmark.macro.createTempFileFromAsset
-import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import java.net.ConnectException
-import java.net.HttpURLConnection
-import java.net.URL
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import org.junit.Assert.assertTrue
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class PerfettoTraceProcessorTest {
-
- @Test
- fun shellPath() {
- assumeTrue(isAbiSupported())
- val shellPath = PerfettoTraceProcessor.shellPath
- val out = Shell.executeScriptCaptureStdout("$shellPath --version")
- assertTrue(
- "expect to get Perfetto version string, saw: $out",
- out.contains("Perfetto v")
- )
- }
-
- @Test
- fun getJsonMetrics_tracePathWithSpaces() {
- assumeTrue(isAbiSupported())
- assertFailsWith<IllegalArgumentException> {
- PerfettoTraceProcessor.runServer("/a b") { }
- }
- }
-
- @Test
- fun getJsonMetrics_metricWithSpaces() {
- assumeTrue(isAbiSupported())
- assertFailsWith<IllegalArgumentException> {
- PerfettoTraceProcessor.runServer(
- createTempFileFromAsset(
- "api31_startup_cold",
- ".perfetto-trace"
- ).absolutePath
- ) {
- getTraceMetrics("a b")
- }
- }
- }
-
- @Test
- fun validateAbiNotSupportedBehavior() {
- assumeFalse(isAbiSupported())
- assertFailsWith<IllegalStateException> {
- PerfettoTraceProcessor.shellPath
- }
-
- assertFailsWith<IllegalStateException> {
- PerfettoTraceProcessor.runServer(
- createTempFileFromAsset(
- "api31_startup_cold",
- ".perfetto-trace"
- ).absolutePath
- ) {
- getTraceMetrics("ignored_metric")
- }
- }
- }
-
- @Test
- fun querySlices() {
- // check known slice content is queryable
- assumeTrue(isAbiSupported())
- val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
- PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
-
- assertEquals(
- expected = listOf(
- Slice(
- name = "activityStart",
- ts = 186975009436431,
- dur = 29580628
- )
- ),
- actual = querySlices("activityStart")
- )
- assertEquals(
- expected = listOf(
- Slice(
- name = "activityStart",
- ts = 186975009436431,
- dur = 29580628
- ),
- Slice(
- name = "activityResume",
- ts = 186975039764298,
- dur = 6570418
- )
- ),
- actual = querySlices("activityStart", "activityResume")
- .sortedBy { it.ts }
- )
- }
- }
-
- @Test
- fun validateTraceProcessorBinariesExist() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val suffixes = listOf("aarch64")
- val entries = suffixes.map { "trace_processor_shell_$it" }.toSet()
- val assets = context.assets.list("") ?: emptyArray()
- assertTrue(
- "Expected to find $entries",
- assets.toSet().containsAll(entries)
- )
- }
-
- @Test
- fun runServerShouldHandleStartAndStopServer() {
- assumeTrue(isAbiSupported())
-
- // This method will return true if the server status endpoint returns 200 (that is also
- // the only status code being returned).
- fun isRunning(): Boolean = try {
- val url = URL("http://localhost:${PerfettoTraceProcessor.PORT}/")
- with(url.openConnection() as HttpURLConnection) {
- return@with responseCode == 200
- }
- } catch (e: ConnectException) {
- false
- }
-
- // Check server is not running
- assertTrue(!isRunning())
-
- PerfettoTraceProcessor.runServer {
-
- // Check server is running
- assertTrue(isRunning())
- }
-
- // Check server is not running
- assertTrue(!isRunning())
- }
-}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
index a86691c..c3e4847 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
@@ -21,6 +21,7 @@
import androidx.benchmark.macro.PowerMetric.Companion.MEASURE_BLOCK_SECTION_NAME
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -39,7 +40,7 @@
assumeTrue(isAbiSupported())
val traceFile = createTempFileFromAsset("api32_odpm_rails", ".perfetto-trace")
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerQuery.getPowerMetrics(
this,
querySlices(MEASURE_BLOCK_SECTION_NAME).first()
@@ -180,7 +181,7 @@
val traceFile = createTempFileFromAsset("api31_odpm_rails_empty", ".perfetto-trace")
- val actualMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val actualMetrics = PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
PowerQuery.getPowerMetrics(this, querySlices(MEASURE_BLOCK_SECTION_NAME).first())
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
index 7583d02..8a32b4d 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
@@ -19,6 +19,7 @@
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.createTempFileFromAsset
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import java.util.Locale
@@ -39,9 +40,11 @@
assumeTrue(isAbiSupported())
val traceFile = createTempFileFromAsset(prefix = tracePrefix, suffix = ".perfetto-trace")
- val startupSubMetrics = PerfettoTraceProcessor.runServer(traceFile.absolutePath) {
+ val startupSubMetrics = PerfettoTraceProcessor.runSingleSessionServer(
+ traceFile.absolutePath
+ ) {
StartupTimingQuery.getFrameSubMetrics(
- perfettoTraceProcessor = this,
+ session = this,
captureApiLevel = api,
targetPackageName = "androidx.benchmark.integration.macrobenchmark.target",
startupMode = startupMode
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
new file mode 100644
index 0000000..2a7f619
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.benchmark.perfetto
+
+import androidx.benchmark.Shell
+import androidx.benchmark.macro.createTempFileFromAsset
+import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import java.net.ConnectException
+import java.net.HttpURLConnection
+import java.net.URL
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PerfettoTraceProcessorTest {
+ @Test
+ fun shellPath() {
+ assumeTrue(isAbiSupported())
+ val shellPath = PerfettoTraceProcessor.shellPath
+ val out = Shell.executeScriptCaptureStdout("$shellPath --version")
+ assertTrue(
+ "expect to get Perfetto version string, saw: $out",
+ out.contains("Perfetto v")
+ )
+ }
+
+ @Test
+ fun getJsonMetrics_tracePathWithSpaces() {
+ assumeTrue(isAbiSupported())
+ assertFailsWith<IllegalArgumentException> {
+ PerfettoTraceProcessor.runSingleSessionServer("/a b") { }
+ }
+ }
+
+ @Test
+ fun getJsonMetrics_metricWithSpaces() {
+ assumeTrue(isAbiSupported())
+ assertFailsWith<IllegalArgumentException> {
+ PerfettoTraceProcessor.runSingleSessionServer(
+ createTempFileFromAsset(
+ "api31_startup_cold",
+ ".perfetto-trace"
+ ).absolutePath
+ ) {
+ getTraceMetrics("a b")
+ }
+ }
+ }
+
+ @Test
+ fun validateAbiNotSupportedBehavior() {
+ assumeFalse(isAbiSupported())
+ assertFailsWith<IllegalStateException> {
+ PerfettoTraceProcessor.shellPath
+ }
+
+ assertFailsWith<IllegalStateException> {
+ PerfettoTraceProcessor.runSingleSessionServer(
+ createTempFileFromAsset(
+ "api31_startup_cold",
+ ".perfetto-trace"
+ ).absolutePath
+ ) {
+ getTraceMetrics("ignored_metric")
+ }
+ }
+ }
+
+ @Test
+ fun querySlices() {
+ // check known slice content is queryable
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ assertEquals(
+ expected = listOf(
+ Slice(
+ name = "activityStart",
+ ts = 186975009436431,
+ dur = 29580628
+ )
+ ),
+ actual = querySlices("activityStart")
+ )
+ assertEquals(
+ expected = listOf(
+ Slice(
+ name = "activityStart",
+ ts = 186975009436431,
+ dur = 29580628
+ ),
+ Slice(
+ name = "activityResume",
+ ts = 186975039764298,
+ dur = 6570418
+ )
+ ),
+ actual = querySlices("activityStart", "activityResume")
+ .sortedBy { it.ts }
+ )
+ }
+ }
+
+ @Test
+ fun query() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ // raw list of maps
+ assertEquals(
+ expected = listOf(
+ rowOf(
+ "name" to "activityStart",
+ "ts" to 186975009436431L,
+ "dur" to 29580628L)
+ ),
+ actual = query(
+ "SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\""
+ ).toList(),
+ )
+ query("""
+ |SELECT
+ | slice.name,slice.ts,slice.dur
+ |FROM slice
+ | INNER JOIN thread_track on slice.track_id = thread_track.id
+ | INNER JOIN thread USING(utid)
+ | INNER JOIN process USING(upid)
+ |WHERE
+ | slice.name LIKE \"activityStart\"
+ """.trimMargin()
+ ).forEach { println(it) }
+
+ // list of lists
+ assertEquals(
+ expected = listOf(
+ listOf("activityStart", 186975009436431L, 29580628L)
+ ),
+ actual = query(
+ "SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\""
+ ).map {
+ listOf(it.string("name"), it.long("ts"), it.long("dur"))
+ }.toList(),
+ )
+
+ // multiple result query
+ assertEquals(
+ expected = listOf(
+ listOf("activityStart", 186975009436431L, 29580628L),
+ listOf("activityResume", 186975039764298L, 6570418L)
+ ),
+ actual = query(
+ "SELECT name,ts,dur FROM slice WHERE" +
+ " name LIKE \"activityStart\" OR" +
+ " name LIKE \"activityResume\""
+ ).map {
+ listOf(it.string("name"), it.long("ts"), it.long("dur"))
+ }.toList(),
+ )
+ }
+ }
+
+ /**
+ * Validate parsing of bytes is possible
+ */
+ @Test
+ fun queryBytes() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val bytes = queryBytes(
+ "SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\""
+ )
+ assertEquals(
+ expected = listOf(
+ rowOf(
+ "name" to "activityStart",
+ "ts" to 186975009436431L,
+ "dur" to 29580628L)
+ ),
+ actual = QueryResultIterator(perfetto.protos.QueryResult.ADAPTER.decode(bytes))
+ .asSequence()
+ .toList(),
+ )
+ }
+ }
+
+ @Test
+ fun validatePerfettoTraceProcessorBinariesExist() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val suffixes = listOf("aarch64")
+ val entries = suffixes.map { "trace_processor_shell_$it" }.toSet()
+ val assets = context.assets.list("") ?: emptyArray()
+ assertTrue(
+ "Expected to find $entries",
+ assets.toSet().containsAll(entries)
+ )
+ }
+
+ @Test
+ fun runServerShouldHandleStartAndStopServer() {
+ assumeTrue(isAbiSupported())
+
+ // This method will return true if the server status endpoint returns 200 (that is also
+ // the only status code being returned).
+ fun isRunning(): Boolean = try {
+ val url = URL("http://localhost:${PerfettoTraceProcessor.PORT}/")
+ with(url.openConnection() as HttpURLConnection) {
+ return@with responseCode == 200
+ }
+ } catch (e: ConnectException) {
+ false
+ }
+
+ // Check server is not running
+ assertTrue(!isRunning())
+
+ PerfettoTraceProcessor.runServer {
+ // Check server is running
+ assertTrue(isRunning())
+ }
+
+ // Check server is not running
+ assertTrue(!isRunning())
+ }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/RowTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/RowTest.kt
new file mode 100644
index 0000000..c5007eb
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/RowTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.benchmark.perfetto
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RowTest {
+ @Test
+ fun basic() {
+ val map = mapOf<String, Any?>("name" to "Name", "ts" to 0L, "dur" to 1L)
+ val row = rowOf("name" to "Name", "ts" to 0L, "dur" to 1L)
+
+ assertEquals(map, row)
+ assertEquals(map.hashCode(), row.hashCode())
+ assertEquals(map.toString(), row.toString())
+ }
+
+ @Test
+ fun gettersSetters() {
+ val row = rowOf(
+ "string" to "foo",
+ "double" to 0.0,
+ "long" to 1L,
+ "bytes" to byteArrayOf(0x00, 0x01),
+ "null" to null
+ )
+
+ assertEquals("foo", row.string("string"))
+ assertEquals("foo", row.nullableString("string"))
+ assertContentEquals(byteArrayOf(0x00, 0x01), row.bytes("bytes"))
+ assertContentEquals(byteArrayOf(0x00, 0x01), row.nullableBytes("bytes"))
+ assertEquals(0.0, row.double("double"))
+ assertEquals(0.0, row.nullableDouble("double"))
+ assertEquals(1L, row.long("long"))
+ assertEquals(1L, row.nullableLong("long"))
+
+ assertNull(row.nullableString("null"))
+ assertNull(row.nullableBytes("null"))
+ assertNull(row.nullableDouble("null"))
+ assertNull(row.nullableLong("null"))
+ }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 6d3aaed..357acee 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -32,8 +32,9 @@
import androidx.benchmark.UserspaceTracing
import androidx.benchmark.checkAndGetSuppressionState
import androidx.benchmark.conditionalError
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.UiState
import androidx.benchmark.perfetto.appendUiState
import androidx.benchmark.userspaceTrace
@@ -251,9 +252,7 @@
tracePaths.add(tracePath)
- // Loads a new trace in the perfetto trace processor shell
- loadTrace(tracePath)
- val iterationResult =
+ val iterationResult = loadTrace(PerfettoTrace(tracePath)) {
// Extracts the metrics using the perfetto trace processor
userspaceTrace("extract metrics") {
metrics
@@ -272,6 +271,7 @@
// merge into one map
.reduce { sum, element -> sum + element }
}
+ }
// append UI state to trace, so tools opening trace will highlight relevant part in UI
val uiState = UiState(
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 9bf649f..b3293ce 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -28,11 +28,11 @@
import androidx.benchmark.macro.perfetto.BatteryDischargeQuery
import androidx.benchmark.macro.perfetto.FrameTimingQuery
import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
import androidx.benchmark.macro.perfetto.PowerQuery
-import androidx.benchmark.macro.perfetto.Slice
import androidx.benchmark.macro.perfetto.StartupTimingQuery
import androidx.benchmark.macro.perfetto.camelCase
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.Slice
/**
* Metric interface.
@@ -53,7 +53,7 @@
*/
internal abstract fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult
internal data class CaptureInfo(
@@ -98,9 +98,9 @@
internal override fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult {
- val subMetrics = AudioUnderrunQuery.getSubMetrics(perfettoTraceProcessor)
+ val subMetrics = AudioUnderrunQuery.getSubMetrics(session)
return IterationResult(
singleMetrics = mapOf(
@@ -135,10 +135,10 @@
@SuppressLint("SyntheticAccessor")
internal override fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult {
val subMetricsMsMap = FrameTimingQuery.getFrameSubMetrics(
- perfettoTraceProcessor = perfettoTraceProcessor,
+ session = session,
captureApiLevel = Build.VERSION.SDK_INT,
packageName = captureInfo.targetPackageName
)
@@ -188,10 +188,10 @@
@SuppressLint("SyntheticAccessor")
internal override fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult {
return StartupTimingQuery.getFrameSubMetrics(
- perfettoTraceProcessor = perfettoTraceProcessor,
+ session = session,
captureApiLevel = captureInfo.apiLevel,
targetPackageName = captureInfo.targetPackageName,
@@ -230,11 +230,11 @@
internal override fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult {
// Acquires perfetto metrics
- val traceMetrics = perfettoTraceProcessor.getTraceMetrics("android_startup")
+ val traceMetrics = session.getTraceMetrics("android_startup")
val androidStartup = traceMetrics.android_startup
?: throw IllegalStateException("No android_startup metric found.")
val appStartup =
@@ -316,9 +316,9 @@
@SuppressLint("SyntheticAccessor")
internal override fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult {
- val slices = perfettoTraceProcessor.querySlices(sectionName)
+ val slices = session.querySlices(sectionName)
return when (mode) {
Mode.First -> {
@@ -474,26 +474,26 @@
internal override fun getMetrics(
captureInfo: CaptureInfo,
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): IterationResult {
// collect metrics between trace point flags
- val slice = perfettoTraceProcessor.querySlices(MEASURE_BLOCK_SECTION_NAME)
+ val slice = session.querySlices(MEASURE_BLOCK_SECTION_NAME)
.firstOrNull()
?: return IterationResult.EMPTY
if (type is Type.Battery) {
- return getBatteryDischargeMetrics(perfettoTraceProcessor, slice)
+ return getBatteryDischargeMetrics(session, slice)
}
- return getPowerMetrics(perfettoTraceProcessor, slice)
+ return getPowerMetrics(session, slice)
}
private fun getBatteryDischargeMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
slice: Slice
): IterationResult {
val metrics = BatteryDischargeQuery.getBatteryDischargeMetrics(
- perfettoTraceProcessor,
+ session,
slice
)
@@ -508,10 +508,10 @@
}
private fun getPowerMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
slice: Slice
): IterationResult {
- val metrics = PowerQuery.getPowerMetrics(perfettoTraceProcessor, slice)
+ val metrics = PowerQuery.getPowerMetrics(session, slice)
val metricMap: Map<String, Double> = getSpecifiedMetrics(metrics)
if (metricMap.isEmpty()) {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
index f1ae5c4..cdb0f15 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
@@ -16,7 +16,11 @@
package androidx.benchmark.macro.perfetto
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import org.intellij.lang.annotations.Language
+
internal object AudioUnderrunQuery {
+ @Language("sql")
private fun getFullQuery() = """
SELECT track.name, counter.value, counter.ts
FROM track
@@ -30,9 +34,9 @@
)
fun getSubMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor
+ session: PerfettoTraceProcessor.Session
): SubMetrics {
- val queryResult = perfettoTraceProcessor.rawQuery(getFullQuery())
+ val queryResult = session.query(getFullQuery())
var trackName: String? = null
var lastTs: Long? = null
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt
index d170ac2..00973a0 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/BatteryDischargeQuery.kt
@@ -16,7 +16,12 @@
package androidx.benchmark.macro.perfetto
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.Slice
+import org.intellij.lang.annotations.Language
+
internal object BatteryDischargeQuery {
+ @Language("sql")
private fun getFullQuery(slice: Slice) = """
SELECT
max(c.value)/1000 AS startMah,
@@ -34,22 +39,22 @@
)
fun getBatteryDischargeMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
slice: Slice
): List<BatteryDischargeMeasurement> {
- val queryResult = perfettoTraceProcessor.rawQuery(
+ val queryResult = session.query(
query = getFullQuery(slice)
- )
+ ).toList()
if (queryResult.isEmpty()) {
return emptyList()
}
- if (queryResult.size() != 1) {
+ if (queryResult.size != 1) {
throw IllegalStateException("Unexpected query result size for battery discharge.")
}
- val row = queryResult.next()
+ val row = queryResult.single()
return listOf(
BatteryDischargeMeasurement(
name = "Start",
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
index e885746..ac49d29 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
@@ -16,7 +16,14 @@
package androidx.benchmark.macro.perfetto
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.Slice
+import androidx.benchmark.perfetto.processNameLikePkg
+import androidx.benchmark.perfetto.toSlices
+import org.intellij.lang.annotations.Language
+
internal object FrameTimingQuery {
+ @Language("sql")
private fun getFullQuery(packageName: String) = """
------ Select all frame-relevant slices from slice table
SELECT
@@ -135,11 +142,11 @@
}
fun getFrameSubMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
captureApiLevel: Int,
packageName: String,
): Map<SubMetric, List<Long>> {
- val queryResultIterator = perfettoTraceProcessor.rawQuery(
+ val queryResultIterator = session.query(
query = getFullQuery(packageName)
)
val slices = queryResultIterator.toSlices().let { list ->
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
deleted file mode 100644
index 830a342..0000000
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.macro.perfetto
-
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
-import androidx.benchmark.macro.perfetto.server.PerfettoHttpServer
-import androidx.benchmark.macro.perfetto.server.QueryResultIterator
-import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.benchmark.userspaceTrace
-import java.io.File
-import org.jetbrains.annotations.TestOnly
-import perfetto.protos.TraceMetrics
-
-/**
- * Enables parsing perfetto traces on-device
- */
-@RestrictTo(LIBRARY_GROUP) // for internal benchmarking only
-class PerfettoTraceProcessor {
-
- companion object {
- const val PORT = 9001
-
- /**
- * The actual [File] path to the `trace_processor_shell`.
- *
- * Lazily copies the `trace_processor_shell` and enables parsing of the perfetto trace files.
- */
- @get:TestOnly
- val shellPath: String by lazy {
- // Checks for ABI support
- PerfettoHelper.createExecutable("trace_processor_shell")
- }
-
- /**
- * Starts a perfetto trace processor shell server in http mode, loads a trace and executes
- * the given block. It stops the server after the block is complete
- */
- fun <T> runServer(
- absoluteTracePath: String? = null,
- block: PerfettoTraceProcessor.() -> T
- ): T = userspaceTrace("PerfettoTraceProcessor#runServer") {
- var perfettoTraceProcessor: PerfettoTraceProcessor? = null
- try {
-
- // Initializes the server process
- perfettoTraceProcessor = PerfettoTraceProcessor().startServer()
-
- // Loads a trace if required
- if (absoluteTracePath != null) {
- perfettoTraceProcessor.loadTrace(absoluteTracePath)
- }
-
- // Executes the query block
- return@userspaceTrace userspaceTrace("PerfettoTraceProcessor#runServer#block") {
- block(perfettoTraceProcessor)
- }
- } finally {
- perfettoTraceProcessor?.stopServer()
- }
- }
- }
-
- private val perfettoHttpServer: PerfettoHttpServer = PerfettoHttpServer()
- private var traceLoaded = false
-
- private fun startServer(): PerfettoTraceProcessor =
- userspaceTrace("PerfettoTraceProcessor#startServer") {
- perfettoHttpServer.startServer()
- return@userspaceTrace this
- }
-
- private fun stopServer() = userspaceTrace("PerfettoTraceProcessor#stopServer") {
- perfettoHttpServer.stopServer()
- }
-
- /**
- * Loads a trace in the current instance of the trace processor, clearing any previous loaded
- * trace if existing.
- */
- fun loadTrace(absoluteTracePath: String) = userspaceTrace("PerfettoTraceProcessor#loadTrace") {
- require(!absoluteTracePath.contains(" ")) {
- "Trace path must not contain spaces: $absoluteTracePath"
- }
-
- val traceFile = File(absoluteTracePath)
- require(traceFile.exists() && traceFile.isFile) {
- "Trace path must exist and not be a directory: $absoluteTracePath"
- }
-
- // In case a previous trace was loaded, ensures to clear
- if (traceLoaded) {
- clearTrace()
- }
- traceLoaded = false
-
- val parseResult = perfettoHttpServer.parse(traceFile.readBytes())
- if (parseResult.error != null) {
- throw IllegalStateException(parseResult.error)
- }
-
- // Notifies the server that it won't receive any more trace parts
- perfettoHttpServer.notifyEof()
-
- traceLoaded = true
- }
-
- /**
- * Clears the current loaded trace.
- */
- private fun clearTrace() = userspaceTrace("PerfettoTraceProcessor#clearTrace") {
- perfettoHttpServer.restoreInitialTables()
- }
-
- /**
- * Computes the given metric on the previously loaded trace.
- */
- fun getTraceMetrics(metric: String): TraceMetrics =
- userspaceTrace("PerfettoTraceProcessor#getTraceMetrics $metric") {
- require(!metric.contains(" ")) {
- "Metric must not contain spaces: $metric"
- }
- require(perfettoHttpServer.isRunning()) {
- "Perfetto trace_shell_process is not running."
- }
-
- // Compute metrics
- val computeResult = perfettoHttpServer.computeMetric(listOf(metric))
- if (computeResult.error != null) {
- throw IllegalStateException(computeResult.error)
- }
-
- // Decode and return trace metrics
- return@userspaceTrace TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
- }
-
- /**
- * Computes the given query on the previously loaded trace.
- */
- fun rawQuery(query: String): QueryResultIterator =
- userspaceTrace("PerfettoTraceProcessor#rawQuery $query".take(127)) {
- require(perfettoHttpServer.isRunning()) {
- "Perfetto trace_shell_process is not running."
- }
- return@userspaceTrace perfettoHttpServer.query(query)
- }
-
- /**
- * Query a trace for a list of slices - name, timestamp, and duration.
- *
- * Note that sliceNames may include wildcard matches, such as `foo%`
- */
- fun querySlices(
- vararg sliceNames: String
- ): List<Slice> {
- require(perfettoHttpServer.isRunning()) { "Perfetto trace_shell_process is not running." }
-
- val whereClause = sliceNames
- .joinToString(separator = " OR ") {
- "slice.name LIKE \"$it\""
- }
-
- val queryResultIterator = rawQuery(
- query = """
- SELECT slice.name,ts,dur
- FROM slice
- WHERE $whereClause
- """.trimMargin()
- )
-
- return queryResultIterator.toSlices()
- }
-}
-
-/**
- * Helper for fuzzy matching process name to package
- */
-internal fun processNameLikePkg(pkg: String): String {
- return """(process.name LIKE "$pkg" OR process.name LIKE "$pkg:%")"""
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
index 0aca0d8..b1f47b9 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
@@ -19,11 +19,15 @@
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.PowerCategory
import androidx.benchmark.macro.PowerMetric
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.Slice
+import org.intellij.lang.annotations.Language
// We want to use android_powrails.sql, but cannot as they do not split into sections with slice
@OptIn(ExperimentalMetricApi::class)
internal object PowerQuery {
+ @Language("sql")
private fun getFullQuery(slice: Slice) = """
SELECT
t.name,
@@ -94,11 +98,11 @@
}
fun getPowerMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
slice: Slice
): Map<PowerCategory, CategoryMeasurement> {
// gather all recorded rails
- val railMetrics: List<ComponentMeasurement> = getRailMetrics(perfettoTraceProcessor, slice)
+ val railMetrics: List<ComponentMeasurement> = getRailMetrics(session, slice)
railMetrics.ifEmpty { return emptyMap() }
// sort ComponentMeasurements into CategoryMeasurements
@@ -135,20 +139,17 @@
}
private fun getRailMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
slice: Slice
): List<ComponentMeasurement> {
-
val query = getFullQuery(slice)
- val queryResult = perfettoTraceProcessor.rawQuery(query)
-
- return queryResult.toList {
+ return session.query(query).map {
ComponentMeasurement(
name = (it["name"] as String).camelCase(),
energyUws = it["energyUws"] as Double,
powerUw = it["powerUs"] as Double,
)
- }
+ }.toList()
}
/**
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
index 6cb5a71..4ac199d 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StartupTimingQuery.kt
@@ -18,9 +18,14 @@
import android.util.Log
import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.Slice
+import androidx.benchmark.perfetto.processNameLikePkg
+import androidx.benchmark.perfetto.toSlices
+import org.intellij.lang.annotations.Language
internal object StartupTimingQuery {
-
+ @Language("sql")
private fun getFullQuery(targetPackageName: String) = """
------ Select all startup-relevant slices from slice table
SELECT
@@ -102,12 +107,12 @@
}
fun getFrameSubMetrics(
- perfettoTraceProcessor: PerfettoTraceProcessor,
+ session: PerfettoTraceProcessor.Session,
captureApiLevel: Int,
targetPackageName: String,
startupMode: StartupMode
): SubMetrics? {
- val queryResultIterator = perfettoTraceProcessor.rawQuery(
+ val queryResultIterator = session.query(
query = getFullQuery(
targetPackageName = targetPackageName
)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StringHelper.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StringHelper.kt
index 56dbf21..cb268cc 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StringHelper.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/StringHelper.kt
@@ -23,7 +23,7 @@
return this.substring(1, length - 1)
}
internal fun String.camelCase(): String {
- var words = this
+ val words = this
.lowercase()
.replace('_', '.')
.replace("power", "")
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
index e268c30..27b143c 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
@@ -23,7 +23,8 @@
import androidx.annotation.RequiresApi
import androidx.benchmark.Shell
import androidx.benchmark.ShellScript
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.QueryResultIterator
import androidx.benchmark.userspaceTrace
import java.io.IOException
import java.io.InputStream
@@ -185,20 +186,25 @@
}
/**
- * Executes the given [sqlQuery] on a previously parsed trace and returns the result as a
- * query result iterator.
+ * Executes the given [sqlQuery] on a previously parsed trace with custom decoding.
*/
- fun query(sqlQuery: String): QueryResultIterator =
- QueryResultIterator(
- httpRequest(
- method = METHOD_POST,
- url = PATH_QUERY,
- encodeBlock = { QueryArgs.ADAPTER.encode(it, QueryArgs(sqlQuery)) },
- decodeBlock = { QueryResult.ADAPTER.decode(it) }
- )
+ fun <T> query(sqlQuery: String, decodeBlock: (InputStream) -> T): T =
+ httpRequest(
+ method = METHOD_POST,
+ url = PATH_QUERY,
+ encodeBlock = { QueryArgs.ADAPTER.encode(it, QueryArgs(sqlQuery)) },
+ decodeBlock = decodeBlock
)
/**
+ * Executes the given [sqlQuery] on a previously parsed trace and returns the result as a
+ * [QueryResultIterator]
+ */
+ fun query(sqlQuery: String): QueryResultIterator = query(sqlQuery) {
+ QueryResultIterator(QueryResult.ADAPTER.decode(it))
+ }
+
+ /**
* Computes the given metrics on a previously parsed trace.
*/
fun computeMetric(metrics: List<String>): ComputeMetricResult =
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoTraceProcessorApi.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoTraceProcessorApi.kt
new file mode 100644
index 0000000..8e5fd8a
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoTraceProcessorApi.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.perfetto
+
+/**
+ * Annotation indicating experimental API for querying data from Perfetto traces with
+ * Perfetto Trace Processor.
+ */
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalPerfettoTraceProcessorApi
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
new file mode 100644
index 0000000..5254f1c
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
@@ -0,0 +1,300 @@
+/*
+ * 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.benchmark.perfetto
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.benchmark.macro.perfetto.server.PerfettoHttpServer
+import androidx.benchmark.userspaceTrace
+import java.io.File
+import org.intellij.lang.annotations.Language
+import perfetto.protos.QueryResult
+import perfetto.protos.TraceMetrics
+
+/**
+ * Kotlin API for [Perfetto Trace Processor](https://perfetto.dev/docs/analysis/trace-processor),
+ * which enables SQL querying against the data stored in a Perfetto trace.
+ *
+ * This includes synchronous and async trace sections, kernel-level scheduling timing,
+ * binder events... If it's displayed in Android Studio system trace or
+ * [ui.perfetto.dev](https://ui.perfetto.dev), it can be queried from this API.
+ *
+ * ```
+ * // Collect the duration of all slices named "activityStart" in the trace
+ * val activityStartDurationNs = PerfettoTraceProcessor.runServer {
+ * loadTrace(trace) {
+ * query("SELECT dur FROM slice WHERE name LIKE \"activityStart\"").toList {
+ * it.long("dur")
+ * }
+ * }
+ * }
+ * ```
+ *
+ * Note that traces generally hold events from multiple apps, services and processes, so it's
+ * recommended to filter potentially common trace events to the process you're interested in. See
+ * the following example which queries `Choreographer#doFrame` slices (labelled spans of time) only
+ * for a given package name:
+ *
+ * ```
+ * query("""
+ * |SELECT
+ * | slice.name,slice.ts,slice.dur
+ * |FROM slice
+ * | INNER JOIN thread_track on slice.track_id = thread_track.id
+ * | INNER JOIN thread USING(utid)
+ * | INNER JOIN process USING(upid)
+ * |WHERE
+ * | slice.name LIKE "Choreographer#doFrame%" AND
+ * | process.name LIKE "$packageName"
+ * """.trimMargin()
+ * )
+ * ```
+ * See also Perfetto project documentation:
+ * * [Trace Processor overview](https://perfetto.dev/docs/analysis/trace-processor)
+ * * [Common queries](https://perfetto.dev/docs/analysis/common-queries)
+ *
+ * @see PerfettoTrace
+ */
+@ExperimentalPerfettoTraceProcessorApi
+class PerfettoTraceProcessor {
+ companion object {
+ internal const val PORT = 9001
+
+ /**
+ * The actual [File] path to the `trace_processor_shell`.
+ *
+ * Lazily copies the `trace_processor_shell` and enables parsing of the Perfetto trace files.
+ */
+ internal val shellPath: String by lazy {
+ // Checks for ABI support
+ PerfettoHelper.createExecutable("trace_processor_shell")
+ }
+
+ /**
+ * Starts a Perfetto trace processor shell server in http mode, loads a trace and executes
+ * the given block. It stops the server after the block is complete
+ */
+ @JvmStatic
+ fun <T> runServer(
+ block: PerfettoTraceProcessor.() -> T
+ ): T = userspaceTrace("PerfettoTraceProcessor#runServer") {
+ var perfettoTraceProcessor: PerfettoTraceProcessor? = null
+ try {
+
+ // Initializes the server process
+ perfettoTraceProcessor = PerfettoTraceProcessor().startServer()
+
+ // Executes the query block
+ return@userspaceTrace userspaceTrace("PerfettoTraceProcessor#runServer#block") {
+ block(perfettoTraceProcessor)
+ }
+ } finally {
+ perfettoTraceProcessor?.stopServer()
+ }
+ }
+
+ @RestrictTo(LIBRARY_GROUP)
+ fun <T> runSingleSessionServer(
+ absoluteTracePath: String,
+ block: Session.() -> T
+ ) = runServer {
+ loadTrace(PerfettoTrace(absoluteTracePath)) {
+ block(this)
+ }
+ }
+ }
+
+ /**
+ * Loads a PerfettoTrace into the trace processor server to query data out of the trace.
+ */
+ fun <T> loadTrace(
+ trace: PerfettoTrace,
+ block: Session.() -> T
+ ): T {
+ loadTraceImpl(trace.path)
+ // TODO: unload trace after block
+ return block.invoke(Session(this))
+ }
+
+ class Session internal constructor(
+ private val traceProcessor: PerfettoTraceProcessor
+ ) {
+ /**
+ * Computes the given metric on the previously loaded trace.
+ */
+ @RestrictTo(LIBRARY_GROUP) // avoids exposing Proto API
+ fun getTraceMetrics(metric: String): TraceMetrics {
+ userspaceTrace("PerfettoTraceProcessor#getTraceMetrics $metric") {
+ require(!metric.contains(" ")) {
+ "Metric must not contain spaces: $metric"
+ }
+ require(traceProcessor.perfettoHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+
+ // Compute metrics
+ val computeResult = traceProcessor.perfettoHttpServer.computeMetric(listOf(metric))
+ if (computeResult.error != null) {
+ throw IllegalStateException(computeResult.error)
+ }
+
+ // Decode and return trace metrics
+ return TraceMetrics.ADAPTER.decode(computeResult.metrics!!)
+ }
+ }
+
+ /**
+ * Computes the given query on the currently loaded trace.
+ *
+ * Each row returned by a query is returned by the `Sequence` as a [Row]. To extract data
+ * from a `Row`, query by column name. The following example does this for name, timestamp,
+ * and duration of slices:
+ * ```
+ * // Runs the provided callback on each activityStart instance in the trace,
+ * // providing name, start timestamp (in ns) and duration (in ns)
+ * fun PerfettoTraceProcessor.Session.forEachActivityStart(callback: (String, Long, Long) -> Unit) {
+ * query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
+ * callback(it.string("name"), it.long("ts"), it.long("dur")
+ * // or, used as a map:
+ * //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
+ * }
+ * }
+ * ```
+ */
+ fun query(@Language("sql") query: String): Sequence<Row> {
+ userspaceTrace("PerfettoTraceProcessor#query $query".take(127)) {
+ require(traceProcessor.perfettoHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+ val queryResult = traceProcessor.perfettoHttpServer.query(query) {
+ QueryResult.ADAPTER.decode(it)
+ }
+ return Sequence { QueryResultIterator(queryResult) }
+ }
+ }
+
+ /**
+ * Computes the given query on the currently loaded trace, returning the resulting protobuf
+ * bytes as a [ByteArray].
+ *
+ * Use [Session.query] if you do not wish to parse the Proto result yourself.
+ *
+ * The `QueryResult` protobuf definition can be found
+ * [in the Perfetto project](https://github.com/google/perfetto/blob/master/protos/perfetto/trace_processor/trace_processor.proto),
+ * which can be used to decode the result returned here with a protobuf parsing library.
+ *
+ * @see Session.query
+ */
+ fun queryBytes(@Language("sql") query: String): ByteArray {
+ userspaceTrace("PerfettoTraceProcessor#query $query".take(127)) {
+ require(traceProcessor.perfettoHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+ return traceProcessor.perfettoHttpServer.query(query) { it.readBytes() }
+ }
+ }
+
+ /**
+ * Query a trace for a list of slices - name, timestamp, and duration.
+ *
+ * Note that sliceNames may include wildcard matches, such as `foo%`
+ */
+ @RestrictTo(LIBRARY_GROUP) // Slice API not currently exposed, since it doesn't track table
+ fun querySlices(vararg sliceNames: String): List<Slice> {
+ require(traceProcessor.perfettoHttpServer.isRunning()) {
+ "Perfetto trace_shell_process is not running."
+ }
+
+ val whereClause = sliceNames
+ .joinToString(separator = " OR ") {
+ "slice.name LIKE \"$it\""
+ }
+
+ val queryResultIterator = query(
+ query = """
+ SELECT slice.name,ts,dur
+ FROM slice
+ WHERE $whereClause
+ """.trimMargin()
+ )
+
+ return queryResultIterator.toSlices()
+ }
+ }
+
+ private val perfettoHttpServer: PerfettoHttpServer = PerfettoHttpServer()
+ private var traceLoaded = false
+
+ private fun startServer(): PerfettoTraceProcessor =
+ userspaceTrace("PerfettoTraceProcessor#startServer") {
+ println("startserver")
+ perfettoHttpServer.startServer()
+ return@userspaceTrace this
+ }
+
+ private fun stopServer() = userspaceTrace("PerfettoTraceProcessor#stopServer") {
+ println("stopserver")
+ perfettoHttpServer.stopServer()
+ }
+
+ /**
+ * Loads a trace in the current instance of the trace processor, clearing any previous loaded
+ * trace if existing.
+ */
+ private fun loadTraceImpl(absoluteTracePath: String) {
+ userspaceTrace("PerfettoTraceProcessor#loadTraceImpl") {
+ require(!absoluteTracePath.contains(" ")) {
+ "Trace path must not contain spaces: $absoluteTracePath"
+ }
+
+ val traceFile = File(absoluteTracePath)
+ require(traceFile.exists() && traceFile.isFile) {
+ "Trace path must exist and not be a directory: $absoluteTracePath"
+ }
+
+ // In case a previous trace was loaded, ensures to clear
+ if (traceLoaded) {
+ clearTrace()
+ }
+
+ val parseResult = perfettoHttpServer.parse(traceFile.readBytes())
+ if (parseResult.error != null) {
+ throw IllegalStateException(parseResult.error)
+ }
+
+ // Notifies the server that it won't receive any more trace parts
+ perfettoHttpServer.notifyEof()
+
+ traceLoaded = true
+ }
+ }
+
+ /**
+ * Clears the current loaded trace.
+ */
+ private fun clearTrace() = userspaceTrace("PerfettoTraceProcessor#clearTrace") {
+ perfettoHttpServer.restoreInitialTables()
+ traceLoaded = false
+ }
+}
+
+/**
+ * Helper for fuzzy matching process name to package
+ */
+internal fun processNameLikePkg(pkg: String): String {
+ return """(process.name LIKE "$pkg" OR process.name LIKE "$pkg:%")"""
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/QueryResultIterator.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/QueryResultIterator.kt
similarity index 78%
rename from benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/QueryResultIterator.kt
rename to benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/QueryResultIterator.kt
index 164b524..caabb5e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/QueryResultIterator.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/QueryResultIterator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -14,26 +14,19 @@
* limitations under the License.
*/
-package androidx.benchmark.macro.perfetto.server
+package androidx.benchmark.perfetto
-import androidx.annotation.RestrictTo
-import okio.ByteString
import perfetto.protos.QueryResult
/**
- * Wrapper class around [QueryResult] returned after executing a query on perfetto. The parsing
- * logic is copied from the python project:
- * https://github.com/google/perfetto/blob/master/python/perfetto/trace_processor/api.py#L89
+ * Iterator for results from a [PerfettoTraceProcessor] query.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // for internal benchmarking only
-class QueryResultIterator internal constructor(queryResult: QueryResult) :
- Iterator<Map<String, Any?>> {
-
+internal class QueryResultIterator internal constructor(queryResult: QueryResult) : Iterator<Row> {
private val dataLists = object {
val stringBatches = mutableListOf<String>()
val varIntBatches = mutableListOf<Long>()
val float64Batches = mutableListOf<Double>()
- val blobBatches = mutableListOf<ByteString>()
+ val blobBatches = mutableListOf<ByteArray>()
var stringIndex = 0
var varIntIndex = 0
@@ -49,14 +42,13 @@
private var currentIndex = 0
init {
-
// Parsing every batch
for (batch in queryResult.batch) {
val stringsBatch = batch.string_cells!!.split(0x00.toChar()).dropLast(1)
dataLists.stringBatches.addAll(stringsBatch)
dataLists.varIntBatches.addAll(batch.varint_cells)
dataLists.float64Batches.addAll(batch.float64_cells)
- dataLists.blobBatches.addAll(batch.blob_cells)
+ dataLists.blobBatches.addAll(batch.blob_cells.map { it.toByteArray() })
cells.addAll(batch.cells)
}
@@ -78,6 +70,9 @@
return count == 0
}
+ /**
+ * Returns true if there are more rows not yet parsed from the query result.
+ */
override fun hasNext(): Boolean {
return currentIndex < count
}
@@ -88,7 +83,10 @@
* @throws IllegalArgumentException if the query returns an invalid cell type
* @throws NoSuchElementException if the query has no next row.
*/
- override fun next(): Map<String, Any?> {
+ override fun next(): Row {
+ // Parsing logic is copied from the python project:
+ // https://github.com/google/perfetto/blob/master/python/perfetto/trace_processor/api.py#L89
+
if (!hasNext()) throw NoSuchElementException()
val row = mutableMapOf<String, Any?>()
@@ -126,14 +124,6 @@
}
currentIndex += 1
- return row
- }
-
- /**
- * Converts this iterator to a list of [T] using the given mapping function.
- * Note that this method is provided for convenience and exhausts the iterator.
- */
- fun <T> toList(mapFunc: (Map<String, Any?>) -> (T)): List<T> {
- return this.asSequence().map(mapFunc).toList()
+ return Row(row)
}
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Row.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Row.kt
new file mode 100644
index 0000000..4e87536
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Row.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.benchmark.perfetto
+
+/**
+ * Convenience for constructing a RowResult for given column values.
+ *
+ * Useful when asserting expected query results in tests.
+ */
+@ExperimentalPerfettoTraceProcessorApi
+fun rowOf(vararg pairs: Pair<String, Any?>): Row {
+ return Row(pairs.toMap())
+}
+
+/**
+ * A Map<String, Any?> that maps column name to value in a row result from a [QueryResultIterator].
+ *
+ * Provides convenience methods for converting to internal base types - `String`, `Long`, `Double`,
+ * and `ByteArray`.
+ *
+ * ```
+ * session.query("SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\"").forEach {
+ * callback(it.string("name"), it.long("ts"), it.long("dur")
+ * // or, used as a map:
+ * //callback(it["name"] as String, it["ts"] as Long, it["dur"] as Long)
+ * }
+ * ```
+ * Nullable variants of each convenience method are also provided.
+ *
+ */
+@ExperimentalPerfettoTraceProcessorApi
+class Row(private val map: Map<String, Any?>) : Map<String, Any?> by map {
+ fun string(columnName: String): String = map[columnName] as String
+ fun long(columnName: String): Long = map[columnName] as Long
+ fun double(columnName: String): Double = map[columnName] as Double
+ fun bytes(columnName: String): ByteArray = map[columnName] as ByteArray
+ fun nullableString(columnName: String): String? = map[columnName] as String?
+ @Suppress("AutoBoxing") // primitives are already internally boxed
+ fun nullableLong(columnName: String): Long? = map[columnName] as Long?
+ @Suppress("AutoBoxing") // primitives are already internally boxed
+ fun nullableDouble(columnName: String): Double? = map[columnName] as Double?
+ fun nullableBytes(columnName: String): ByteArray? = map[columnName] as ByteArray?
+
+ override fun hashCode(): Int = map.hashCode()
+ override fun toString(): String = map.toString()
+ override fun equals(other: Any?): Boolean = map == other
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Slice.kt
similarity index 71%
rename from benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
rename to benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Slice.kt
index 7ee5044..1a45d7d 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/Slice.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package androidx.benchmark.macro.perfetto
+package androidx.benchmark.perfetto
import androidx.annotation.RestrictTo
-import androidx.benchmark.macro.perfetto.server.QueryResultIterator
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class Slice(
@@ -35,9 +34,9 @@
/**
* Convenient function to immediately retrieve a list of slices.
- * Note that this method is provided for convenience and exhausts the iterator.
+ * Note that this method is provided for convenience.
*/
-internal fun QueryResultIterator.toSlices(): List<Slice> =
- toList {
- Slice(name = it["name"] as String, ts = it["ts"] as Long, dur = it["dur"] as Long)
- }
\ No newline at end of file
+internal fun Sequence<Row>.toSlices(): List<Slice> =
+ map {
+ Slice(name = it.string("name"), ts = it.long("ts"), dur = it.long("dur"))
+ }.toList()
\ No newline at end of file
diff --git a/benchmark/benchmark/build.gradle b/benchmark/benchmark/build.gradle
index 983aaf0..87c1d07 100644
--- a/benchmark/benchmark/build.gradle
+++ b/benchmark/benchmark/build.gradle
@@ -15,7 +15,6 @@
*/
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
diff --git a/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle b/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle
index 97cffb1..d8f0efa 100644
--- a/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle
@@ -48,19 +48,25 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.constraintLayout)
-
- baselineProfile(project(":benchmark:integration-tests:baselineprofile-flavors-producer"))
}
baselineProfile {
filter {
include "androidx.benchmark.integration.baselineprofile.flavors.consumer.*"
}
- filter("free") {
- include "androidx.benchmark.integration.baselineprofile.flavors.consumer.free.*"
- }
- filter("paid") {
- include "androidx.benchmark.integration.baselineprofile.flavors.consumer.paid.*"
+ variants {
+ freeRelease {
+ filter {
+ include "androidx.benchmark.integration.baselineprofile.flavors.consumer.free.*"
+ from(project(":benchmark:integration-tests:baselineprofile-flavors-producer"))
+ }
+ }
+ paidRelease {
+ filter {
+ include "androidx.benchmark.integration.baselineprofile.flavors.consumer.paid.*"
+ from(project(":benchmark:integration-tests:baselineprofile-flavors-producer"))
+ }
+ }
}
// Note that these are the default settings, just reported here to make it explicit.
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
index 325ddc9..e67d285 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
@@ -20,8 +20,11 @@
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi
import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -34,7 +37,11 @@
@SmallTest
@SdkSuppress(minSdkVersion = 29)
-@OptIn(ExperimentalMetricApi::class)
+@OptIn(
+ ExperimentalMetricApi::class,
+ ExperimentalPerfettoTraceProcessorApi::class,
+ ExperimentalPerfettoCaptureApi::class
+)
class PerfettoTraceProcessorBenchmark {
@get:Rule
@@ -46,6 +53,44 @@
fun setUp() = Assume.assumeTrue(PerfettoHelper.isAbiSupported())
@Test
+ fun loadServer() = benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(TraceSectionMetric("PerfettoTraceProcessorBenchmark")),
+ iterations = 5,
+ ) {
+ trace("PerfettoTraceProcessorBenchmark") {
+ PerfettoTraceProcessor.runServer {}
+ }
+ }
+
+ @Test
+ fun singleTrace() = benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(TraceSectionMetric("PerfettoTraceProcessorBenchmark")),
+ iterations = 5,
+ ) {
+ trace("PerfettoTraceProcessorBenchmark") {
+ PerfettoTraceProcessor.runServer {
+ loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
+ }
+ }
+ }
+
+ @Test
+ fun doubleTrace() = benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(TraceSectionMetric("PerfettoTraceProcessorBenchmark")),
+ iterations = 5,
+ ) {
+ trace("PerfettoTraceProcessorBenchmark") {
+ PerfettoTraceProcessor.runServer {
+ loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
+ loadTrace(PerfettoTrace(traceFile.absolutePath)) {}
+ }
+ }
+ }
+
+ @Test
fun computeSingleMetric() = benchmarkWithTrace {
runComputeStartupMetric()
}
@@ -70,30 +115,29 @@
runProcessQuery()
}
- private fun benchmarkWithTrace(block: PerfettoTraceProcessor.() -> Unit) =
+ private fun benchmarkWithTrace(block: PerfettoTraceProcessor.Session.() -> Unit) =
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = listOf(TraceSectionMetric("PerfettoTraceProcessorBenchmark")),
iterations = 5,
) {
trace("PerfettoTraceProcessorBenchmark") {
-
// This will run perfetto trace processor http server on the specified port 10555.
// Note that this is an arbitrary number and the default cannot be used because
// the macrobenchmark instance of the server is running at the same time.
- PerfettoTraceProcessor.runServer(
+ PerfettoTraceProcessor.runSingleSessionServer(
absoluteTracePath = traceFile.absolutePath,
block = block
)
}
}
- private fun PerfettoTraceProcessor.runComputeStartupMetric() {
+ private fun PerfettoTraceProcessor.Session.runComputeStartupMetric() {
getTraceMetrics("android_startup")
}
- private fun PerfettoTraceProcessor.runSlicesQuery() {
- rawQuery(
+ private fun PerfettoTraceProcessor.Session.runSlicesQuery() {
+ query(
"""
SELECT slice.name, slice.ts, slice.dur, thread_track.id, thread_track.name
FROM slice
@@ -104,8 +148,8 @@
)
}
- private fun PerfettoTraceProcessor.runCounterQuery() {
- rawQuery(
+ private fun PerfettoTraceProcessor.Session.runCounterQuery() {
+ query(
"""
SELECT track.name, counter.value, counter.ts
FROM track
@@ -114,8 +158,8 @@
)
}
- private fun PerfettoTraceProcessor.runProcessQuery() {
- rawQuery(
+ private fun PerfettoTraceProcessor.Session.runProcessQuery() {
+ query(
"""
SELECT upid
FROM counter
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index 5f63c3b..81ab23a 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -18,13 +18,26 @@
// This project is stored outside of buildSrc/ so that waiting for these tests to complete doesn't delay the rest of the build
import androidx.build.BuildServerConfigurationKt
+import androidx.build.LibraryType
import androidx.build.SdkResourceGenerator
plugins {
id("AndroidXPlugin")
id("kotlin")
+ id("java-gradle-plugin")
}
+
+// We need 'java-gradle-plugin' on classpath for Gradle test kit projects. If it's not, we get the following error:
+// org.gradle.testkit.runner.InvalidPluginMetadataException: Test runtime classpath does not contain
+// plugin metadata file 'plugin-under-test-metadata.properties'but if we generate a .jar
+//
+// However, if we actually run the :jar task for buildSrc-tests we get the following warning log:
+//
+// :buildSrc-tests:jar: No valid plugin descriptors were found in META-INF/gradle-plugins
+// so we disable it.
+jar.enabled = false
+
apply from: "../buildSrc/kotlin-dsl-dependency.gradle"
def buildSrcJar(jarName) {
@@ -37,7 +50,10 @@
}
dependencies {
+
+ implementation(findGradleKotlinDsl())
implementation(gradleApi())
+ implementation(libs.androidGradlePluginz)
implementation(buildSrcJar("private"))
implementation(buildSrcJar("public"))
implementation(buildSrcJar("jetpad-integration"))
@@ -49,6 +65,7 @@
}
// Required for dom4j to parse comments correctly.
implementation(libs.xerces)
+ implementation(libs.kotlinGradlePluginz)
testImplementation(libs.junit)
testImplementation(libs.truth)
@@ -56,9 +73,7 @@
testImplementation(project(":internal-testutils-truth"))
testImplementation(gradleTestKit())
testImplementation(libs.checkmark)
- testImplementation(libs.kotlinGradlePluginz)
testImplementation(libs.toml)
- testImplementation(findGradleKotlinDsl())
}
SdkResourceGenerator.generateForHostTest(project)
@@ -68,6 +83,10 @@
it.jvmArgs = ["--add-opens=java.base/java.lang=ALL-UNNAMED"]
}
+androidx {
+ type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
+}
+
// Also do style checking of the buildSrc project from within this project
// We run that from this project so that it doesn't block other projects while it runs
def ktlintDir = file("../buildSrc")
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
index 4035dac..8166dff 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
@@ -16,13 +16,36 @@
package androidx.build.buildInfo
+import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.asBuildInfoDependencies
+import androidx.build.jetpad.LibraryBuildInfoFile
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.gson.Gson
+import java.io.File
+import net.saff.checkmark.Checkmark.Companion.check
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Before
+import org.junit.Rule
import org.junit.Test
-import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.asBuildInfoDependencies
-import net.saff.checkmark.Checkmark.Companion.check
+import org.junit.rules.TemporaryFolder
class CreateLibraryBuildInfoFileTaskTest {
+ @get:Rule
+ val distDir = TemporaryFolder()
+
+ @get:Rule
+ val projectSetup = ProjectSetupRule()
+ private lateinit var gradleRunner: GradleRunner
+
+ @Before
+ fun setUp() {
+ gradleRunner = GradleRunner.create()
+ .withProjectDir(projectSetup.rootDir)
+ .withPluginClasspath()
+ .withEnvironment(mapOf("DIST_DIR" to distDir.root.absolutePath))
+ }
+
@Test
fun buildInfoDependencies() {
val deps: List<ModuleDependency> =
@@ -38,4 +61,80 @@
computeTaskSuffix("cubane-jvm").check { it == "Jvm" }
computeTaskSuffix("cubane-jvm-linux-x64").check { it == "JvmLinuxX64" }
}
-}
\ No newline at end of file
+
+ @Test
+ fun buildInfoTaskCreatesSimpleFile() {
+ setupBuildInfoProject()
+ gradleRunner.withArguments("createLibraryBuildInfoFiles").build()
+
+ val buildInfoFile = distDir.root.resolve(
+ "build-info/androidx.build_info_test_test_build_info.txt"
+ )
+ buildInfoFile.check { it.exists() }
+ val buildInfo = parseBuildInfo(buildInfoFile)
+ buildInfo.check {
+ it.groupId == "androidx.build_info_test"
+ it.artifactId == "test"
+ it.groupIdRequiresSameVersion == false
+ it.version == "0.0.1"
+ it.dependencies == listOf(
+ LibraryBuildInfoFile.Dependency().apply {
+ groupId = "androidx.core"
+ artifactId = "core"
+ version = "1.1.0"
+ isTipOfTree = false
+ }
+ )
+ it.kotlinVersion == projectSetup.props.kotlinVersion
+ }
+ }
+
+ fun setupBuildInfoProject() {
+ projectSetup.writeDefaultBuildGradle(
+ prefix = """
+ import androidx.build.buildInfo.CreateLibraryBuildInfoFileTaskKt
+ plugins {
+ id("com.android.library")
+ id("maven-publish")
+ id("kotlin-android")
+ }
+ ext {
+ supportRootFolder = new File("${projectSetup.rootDir}")
+ }
+ """.trimIndent(),
+ suffix = """
+ dependencies {
+ implementation("androidx.core:core:1.1.0")
+ }
+ android {
+ namespace 'androidx.build_info'
+ }
+ group = "androidx.build_info_test"
+ publishing {
+ publications {
+ maven(MavenPublication) {
+ groupId = 'androidx.build_info_test'
+ artifactId = 'test'
+ version = '0.0.1'
+ }
+ }
+ publications.withType(MavenPublication) {
+ CreateLibraryBuildInfoFileTaskKt.createBuildInfoTask(
+ project,
+ it,
+ null,
+ it.artifactId,
+ project.provider { "fakeSha" }
+ )
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ fun parseBuildInfo(buildInfoFile: File): LibraryBuildInfoFile {
+ val gson = Gson()
+ val contents = buildInfoFile.readText(Charsets.UTF_8)
+ return gson.fromJson(contents, LibraryBuildInfoFile::class.java)
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 05930af..0737dd3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -53,7 +53,6 @@
import com.android.build.gradle.TestExtension
import com.android.build.gradle.TestPlugin
import com.android.build.gradle.TestedExtension
-import com.android.build.gradle.internal.tasks.AnalyticsRecordingTask
import com.android.build.gradle.internal.tasks.CheckAarMetadataTask
import com.android.build.gradle.internal.tasks.ListingFileRedirectTask
import java.io.File
@@ -711,12 +710,6 @@
// lives alongside the project's buildDir.
externalNativeBuild.cmake.buildStagingDirectory =
File(project.buildDir, "../nativeBuildStaging")
-
- // disable analytics recording
- // It's always out-of-date, and we don't release any apps in this repo
- project.tasks.withType(AnalyticsRecordingTask::class.java).configureEach { task ->
- task.enabled = false
- }
}
/**
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
index 78e9bf4..b805725 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
@@ -27,6 +27,7 @@
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
@@ -64,6 +65,9 @@
@get:Internal
abstract val appLoader: Property<BuiltArtifactsLoader>
+ @get:Input
+ abstract val apkPackageName: Property<String>
+
@get:Optional
@get:Input
@get:Option(option = "className", description = "Fully qualified class name of a class to run")
@@ -74,6 +78,11 @@
@get:Option(option = "packageName", description = "Package name test classes to run")
abstract val packageName: Property<String>
+ @get:Optional
+ @get:Input
+ @get:Option(option = "pullScreenshots", description = "true if screenshots should be pulled")
+ abstract val pullScreenshots: Property<String>
+
@get:Input
abstract val device: Property<String>
@@ -104,6 +113,9 @@
if (className.isPresent) "class ${className.get()}" else null,
if (packageName.isPresent) "package ${packageName.get()}" else null,
).joinToString(separator = ",")
+
+ val shouldPull = pullScreenshots.isPresent && pullScreenshots.get() == "true"
+
execOperations.exec {
it.commandLine(
listOfNotNull(
@@ -126,6 +138,10 @@
testApkPath,
if (hasFilters) "--test-targets" else null,
if (hasFilters) filters else null,
+ if (shouldPull) "--directories-to-pull" else null,
+ if (shouldPull) {
+ "/sdcard/Android/data/${apkPackageName.get()}/cache/androidx_screenshots"
+ } else null,
)
)
}
@@ -145,23 +161,27 @@
onVariants { variant ->
var name: String? = null
var artifacts: Artifacts? = null
+ var apkPackageName: Provider<String>? = null
when {
variant is HasAndroidTest -> {
name = variant.androidTest?.name
artifacts = variant.androidTest?.artifacts
+ apkPackageName = variant.androidTest?.namespace
}
project.plugins.hasPlugin("com.android.test") -> {
name = variant.name
artifacts = variant.artifacts
+ apkPackageName = variant.namespace
}
}
- if (name == null || artifacts == null) {
+ if (name == null || artifacts == null || apkPackageName == null) {
return@onVariants
}
devicesToRunOn.forEach { (taskPrefix, model) ->
tasks.register("$taskPrefix$name", FtlRunner::class.java) { task ->
task.device.set(model)
+ task.apkPackageName.set(apkPackageName)
task.testFolder.set(artifacts.get(SingleArtifact.APK))
task.testLoader.set(artifacts.getBuiltArtifactsLoader())
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
index e38d217..03f53c7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
@@ -205,7 +205,7 @@
)
)
task.commit.set(shaProvider)
- task.groupIdRequiresSameVersion.set(mavenGroup?.requireSameVersion)
+ task.groupIdRequiresSameVersion.set(mavenGroup?.requireSameVersion ?: false)
task.groupZipPath.set(project.getGroupZipPath())
task.projectZipPath.set(project.getProjectZipPath())
@@ -297,8 +297,26 @@
libraryGroup: LibraryGroup?,
artifactId: String
) {
- val task: TaskProvider<CreateLibraryBuildInfoFileTask> =
- CreateLibraryBuildInfoFileTask.setup(
+ val task = createBuildInfoTask(
+ pub,
+ libraryGroup,
+ artifactId,
+ project.provider {
+ project.getFrameworksSupportCommitShaAtHead()
+ }
+ )
+ rootProject.tasks.named(CreateLibraryBuildInfoFileTask.TASK_NAME)
+ .configure { it.dependsOn(task) }
+ addTaskToAggregateBuildInfoFileTask(task)
+}
+
+private fun Project.createBuildInfoTask(
+ pub: ProjectComponentPublication,
+ libraryGroup: LibraryGroup?,
+ artifactId: String,
+ shaProvider: Provider<String>
+): TaskProvider<CreateLibraryBuildInfoFileTask> {
+ return CreateLibraryBuildInfoFileTask.setup(
project = project,
mavenGroup = libraryGroup,
variant = VariantPublishPlan(
@@ -316,14 +334,8 @@
component.usages.orEmpty().flatMap { it.dependencyConstraints }
}.orEmpty()
}),
- shaProvider = project.provider {
- project.getFrameworksSupportCommitShaAtHead()
- }
+ shaProvider = shaProvider
)
-
- rootProject.tasks.named(CreateLibraryBuildInfoFileTask.TASK_NAME)
- .configure { it.dependsOn(task) }
- addTaskToAggregateBuildInfoFileTask(task)
}
private fun dependenciesOnKmpVariants(component: SoftwareComponentInternal) =
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
index 243d9e2..95d9e98 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
@@ -389,6 +389,7 @@
)
}
+ @SdkSuppress(minSdkVersion = 22) // b/266740827
@Test
fun setZoomRatio_operationCanceledExceptionIfNoUseCase() {
assertFutureFailedWithOperationCancellation(cameraControl.setZoomRatio(1.5f))
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
index 5d6c9c4..e527c4b 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
@@ -39,7 +39,9 @@
@RunWith(Parameterized::class)
@SmallTest
@SdkSuppress(minSdkVersion = 21)
-class EncoderProfilesProviderAdapterDeviceTest(private val quality: Int) {
+class EncoderProfilesProviderAdapterDeviceTest(
+ private val quality: Int,
+) {
companion object {
@JvmStatic
@@ -74,7 +76,10 @@
cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
intCameraId = cameraId.toInt()
+ setUptEncoderProfileProvider()
+ }
+ private fun setUptEncoderProfileProvider() {
encoderProfilesProvider = EncoderProfilesProviderAdapter(cameraId)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 94c9c19..2fb684c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -19,16 +19,17 @@
package androidx.camera.camera2.pipe.integration.adapter
import android.annotation.SuppressLint
-import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraMetadata
-import android.os.Build
import android.util.Range
import android.util.Size
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
@@ -44,7 +45,6 @@
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.EncoderProfilesProvider
-import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.core.impl.Quirks
import androidx.camera.core.impl.Timebase
import androidx.camera.core.impl.utils.CameraOrientationUtil
@@ -52,8 +52,6 @@
import java.util.concurrent.Executor
import javax.inject.Inject
-internal val defaultQuirks = Quirks(emptyList())
-
/**
* Adapt the [CameraInfoInternal] interface to [CameraPipe].
*/
@@ -67,9 +65,11 @@
private val cameraStateAdapter: CameraStateAdapter,
private val cameraControlStateAdapter: CameraControlStateAdapter,
private val cameraCallbackMap: CameraCallbackMap,
- private val focusMeteringControl: FocusMeteringControl
+ private val focusMeteringControl: FocusMeteringControl,
+ private val cameraQuirks: CameraQuirks,
+ private val encoderProfilesProviderAdapter: EncoderProfilesProviderAdapter,
+ private val streamConfigurationMapCompat: StreamConfigurationMapCompat,
) : CameraInfoInternal {
- private lateinit var encoderProfilesProviderAdapter: EncoderProfilesProviderAdapter
@OptIn(ExperimentalCamera2Interop::class)
internal val camera2CameraInfo: Camera2CameraInfo by lazy {
Camera2CameraInfo.create(cameraProperties)
@@ -92,8 +92,7 @@
}
override fun getSensorRotationDegrees(): Int = getSensorRotationDegrees(Surface.ROTATION_0)
- override fun hasFlashUnit(): Boolean =
- cameraProperties.metadata[CameraCharacteristics.FLASH_INFO_AVAILABLE]!!
+ override fun hasFlashUnit(): Boolean = cameraProperties.isFlashAvailable()
override fun getSensorRotationDegrees(relativeRotation: Int): Int {
val sensorOrientation: Int =
@@ -129,9 +128,6 @@
override fun getImplementationType(): String = "CameraPipe"
override fun getEncoderProfilesProvider(): EncoderProfilesProvider {
- if (!::encoderProfilesProviderAdapter.isInitialized) {
- encoderProfilesProviderAdapter = EncoderProfilesProviderAdapter(cameraId)
- }
return encoderProfilesProviderAdapter
}
@@ -148,34 +144,19 @@
@SuppressLint("ClassVerificationFailure")
override fun getSupportedResolutions(format: Int): List<Size> {
- val streamConfigurationMap =
- cameraProperties.metadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
- return if (Build.VERSION.SDK_INT < 23 &&
- format == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
- ) {
- streamConfigurationMap.getOutputSizes(SurfaceTexture::class.java)
- } else {
- streamConfigurationMap.getOutputSizes(format)
- }?.toList() ?: emptyList()
+ return streamConfigurationMapCompat.getOutputSizes(format)?.toList() ?: emptyList()
}
@SuppressLint("ClassVerificationFailure")
override fun getSupportedHighResolutions(format: Int): List<Size> {
- val streamConfigurationMap =
- cameraProperties.metadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
- if (Build.VERSION.SDK_INT >= 23) {
- streamConfigurationMap.getHighResolutionOutputSizes(format)?.let {
- return it.toList()
- }
- }
- return emptyList()
+ return streamConfigurationMapCompat.getHighResolutionOutputSizes(format)?.toList()
+ ?: emptyList()
}
override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
override fun getCameraQuirks(): Quirks {
- Log.warn { "TODO: Quirks are not yet supported." }
- return defaultQuirks
+ return cameraQuirks.quirks
}
override fun isFocusMeteringSupported(action: FocusMeteringAction) =
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 3a88c28..91b1c4f 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
@@ -19,9 +19,12 @@
import android.content.Context
import android.hardware.camera2.CameraCaptureSession.CaptureCallback
import android.hardware.camera2.CameraDevice
+import android.util.Size
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.core.Log.info
+import androidx.camera.camera2.pipe.integration.compat.workaround.setupHDRnet
+import androidx.camera.camera2.pipe.integration.compat.workaround.toggleHDRPlus
import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
import androidx.camera.camera2.pipe.integration.impl.SESSION_PHYSICAL_CAMERA_ID_OPTION
@@ -30,12 +33,15 @@
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.PreviewConfig
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
/**
* This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
@@ -65,7 +71,7 @@
* configuration cannot be produced.
*/
override fun getConfig(
- captureType: UseCaseConfigFactory.CaptureType,
+ captureType: CaptureType,
captureMode: Int
): Config? {
debug { "Creating config for $captureType" }
@@ -76,13 +82,13 @@
val mutableConfig = MutableOptionsBundle.create()
val sessionBuilder = SessionConfig.Builder()
when (captureType) {
- UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
- UseCaseConfigFactory.CaptureType.PREVIEW,
- UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS -> sessionBuilder.setTemplateType(
+ CaptureType.IMAGE_CAPTURE,
+ CaptureType.PREVIEW,
+ CaptureType.IMAGE_ANALYSIS -> sessionBuilder.setTemplateType(
CameraDevice.TEMPLATE_PREVIEW
)
- UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE -> sessionBuilder.setTemplateType(
+ CaptureType.VIDEO_CAPTURE -> sessionBuilder.setTemplateType(
CameraDevice.TEMPLATE_RECORD
)
}
@@ -92,12 +98,12 @@
)
val captureBuilder = CaptureConfig.Builder()
when (captureType) {
- UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE ->
+ CaptureType.IMAGE_CAPTURE ->
captureBuilder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
- UseCaseConfigFactory.CaptureType.PREVIEW,
- UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS,
- UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE ->
+ CaptureType.PREVIEW,
+ CaptureType.IMAGE_ANALYSIS,
+ CaptureType.VIDEO_CAPTURE ->
captureBuilder.templateType = CameraDevice.TEMPLATE_RECORD
}
mutableConfig.insertOption(
@@ -105,21 +111,22 @@
captureBuilder.build()
)
- // TODO: the ImageCaptureOptionUnpacker not porting yet. Will need porting the
- // ImageCaptureOptionUnpacker.
-
// Only CAPTURE_TYPE_IMAGE_CAPTURE has its own ImageCaptureOptionUnpacker. Other
- // capture types all use the standard Camera2CaptureOptionUnpacker.
+ // capture types all use the standard DefaultCaptureOptionsUnpacker.
mutableConfig.insertOption(
UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER,
- DefaultCaptureOptionsUnpacker
+ if (captureType == CaptureType.IMAGE_CAPTURE) {
+ ImageCaptureOptionUnpacker.INSTANCE
+ } else {
+ DefaultCaptureOptionsUnpacker.INSTANCE
+ }
)
mutableConfig.insertOption(
UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER,
DefaultSessionOptionsUnpacker
)
- if (captureType == UseCaseConfigFactory.CaptureType.PREVIEW) {
+ if (captureType == CaptureType.PREVIEW) {
mutableConfig.insertOption(
ImageOutputConfig.OPTION_MAX_RESOLUTION,
displayInfoManager.previewSize
@@ -133,7 +140,7 @@
return OptionsBundle.from(mutableConfig)
}
- object DefaultCaptureOptionsUnpacker : CaptureConfig.OptionUnpacker {
+ open class DefaultCaptureOptionsUnpacker : CaptureConfig.OptionUnpacker {
@OptIn(ExperimentalCamera2Interop::class)
override fun unpack(config: UseCaseConfig<*>, builder: CaptureConfig.Builder) {
val defaultCaptureConfig = config.getDefaultCaptureConfig(null)
@@ -170,11 +177,34 @@
// Copy extension keys
builder.addImplementationOptions(camera2Config.captureRequestOptions)
}
+
+ companion object {
+ val INSTANCE = DefaultCaptureOptionsUnpacker()
+ }
+ }
+
+ class ImageCaptureOptionUnpacker : DefaultCaptureOptionsUnpacker() {
+
+ override fun unpack(config: UseCaseConfig<*>, builder: CaptureConfig.Builder) {
+ super.unpack(config, builder)
+ require(config is ImageCaptureConfig) { "config is not ImageCaptureConfig" }
+ builder.addImplementationOptions(
+ Camera2ImplConfig.Builder().apply { toggleHDRPlus(config) }.build()
+ )
+ }
+
+ companion object {
+ val INSTANCE = ImageCaptureOptionUnpacker()
+ }
}
object DefaultSessionOptionsUnpacker : SessionConfig.OptionUnpacker {
@OptIn(ExperimentalCamera2Interop::class)
- override fun unpack(config: UseCaseConfig<*>, builder: SessionConfig.Builder) {
+ override fun unpack(
+ resolution: Size,
+ config: UseCaseConfig<*>,
+ builder: SessionConfig.Builder
+ ) {
val defaultSessionConfig = config.getDefaultSessionConfig( /*valueIfMissing=*/null)
var implOptions: Config = OptionsBundle.emptyBundle()
@@ -194,6 +224,11 @@
// Set any additional implementation options
builder.setImplementationOptions(implOptions)
+ if (config is PreviewConfig) {
+ // Set the WYSIWYG preview for CAPTURE_TYPE_PREVIEW
+ builder.setupHDRnet(resolution)
+ }
+
// Get Camera2 extended options
val camera2Config = Camera2ImplConfig(config)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
index 73addda..6d83a30 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
@@ -24,16 +24,22 @@
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
import androidx.camera.camera2.pipe.integration.compat.quirk.InvalidVideoProfilesQuirk
+import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.core.Logger
import androidx.camera.core.impl.EncoderProfilesProvider
import androidx.camera.core.impl.EncoderProfilesProxy
import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat
+import javax.inject.Inject
+import javax.inject.Named
/**
* Adapt the [EncoderProfilesProvider] interface to [CameraPipe].
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-class EncoderProfilesProviderAdapter(private val cameraIdString: String) : EncoderProfilesProvider {
+@CameraScope
+class EncoderProfilesProviderAdapter @Inject constructor(
+ @Named("CameraId") private val cameraIdString: String,
+) : EncoderProfilesProvider {
private val hasValidCameraId: Boolean
private val cameraId: Int
@@ -51,15 +57,17 @@
}
this.hasValidCameraId = hasValidCameraId
cameraId = intCameraId
-
- // TODO(b/241296464): CamcorderProfileResolutionQuirk
}
override fun hasProfile(quality: Int): Boolean {
if (!hasValidCameraId) {
return false
}
- return CamcorderProfile.hasProfile(cameraId, quality)
+
+ if (!CamcorderProfile.hasProfile(cameraId, quality)) {
+ return false
+ }
+ return true
}
override fun getAll(quality: Int): EncoderProfilesProxy? {
@@ -67,7 +75,7 @@
return null
}
if (!CamcorderProfile.hasProfile(cameraId, quality)) {
- return null
+ return null
}
return getProfilesInternal(quality)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 6733cc7..c8a98dc0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -25,11 +25,12 @@
import android.hardware.display.DisplayManager
import android.media.CamcorderProfile
import android.media.MediaRecorder
-import android.os.Build
import android.util.Size
import android.view.Display
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.EncoderProfilesProxy
import androidx.camera.core.impl.ImageFormatConstants
@@ -74,6 +75,7 @@
internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
private val displayManager: DisplayManager =
(context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
+ private val streamConfigurationMapCompat = getStreamConfigurationMapCompat()
init {
checkCapabilities()
@@ -318,7 +320,7 @@
} catch (e: NumberFormatException) {
// The camera Id is not an integer because the camera may be a removable device. Use
// StreamConfigurationMap to determine the RECORD size.
- return getRecordSizeFromStreamConfigurationMap()
+ return getRecordSizeFromStreamConfigurationMapCompat()
}
var profiles: EncoderProfilesProxy? = null
if (encoderProfilesProviderAdapter.hasProfile(cameraId)) {
@@ -332,9 +334,10 @@
/**
* Obtains the stream configuration map from camera meta data.
*/
- private fun getStreamConfigurationMap(): StreamConfigurationMap {
- return cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
+ private fun getStreamConfigurationMapCompat(): StreamConfigurationMapCompat {
+ val map = cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
?: throw IllegalArgumentException("Cannot retrieve SCALER_STREAM_CONFIGURATION_MAP")
+ return StreamConfigurationMapCompat(map, OutputSizesCorrector(cameraMetadata))
}
/**
@@ -343,9 +346,8 @@
*
* @return Maximum supported video size.
*/
- private fun getRecordSizeFromStreamConfigurationMap(): Size {
- val map: StreamConfigurationMap = getStreamConfigurationMap()
- val videoSizeArr = map.getOutputSizes(
+ private fun getRecordSizeFromStreamConfigurationMapCompat(): Size {
+ val videoSizeArr = streamConfigurationMapCompat.getOutputSizes(
MediaRecorder::class.java
) ?: return RESOLUTION_480P
Arrays.sort(videoSizeArr, CompareSizesByArea(true))
@@ -480,45 +482,22 @@
* @return the max supported output size for the image format
*/
internal fun getMaxOutputSizeByFormat(imageFormat: Int): Size {
- val outputSizes = getAllOutputSizesByFormat(imageFormat)
- return Collections.max(listOf(*outputSizes), CompareSizesByArea())
- }
-
- /**
- * Get all output sizes for a given image format.
- */
- private fun doGetAllOutputSizesByFormat(imageFormat: Int): Array<Size> {
- val map: StreamConfigurationMap = getStreamConfigurationMap()
- val outputSizes = if (Build.VERSION.SDK_INT < 23 &&
- imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
- ) {
- // This is a little tricky that 0x22 that is internal defined in
- // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
- // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
- // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
- // retrieve the output sizes information.
- map.getOutputSizes(SurfaceTexture::class.java)
- } else {
- map.getOutputSizes(imageFormat)
- }
- // TODO(b/244477758): Exclude problematic sizes
-
- // Sort the output sizes. The Comparator result must be reversed to have a descending order
- // result.
- Arrays.sort(outputSizes, CompareSizesByArea(true))
- return outputSizes
- }
-
- /**
- * Retrieves the output size associated with the given format.
- */
- private fun getAllOutputSizesByFormat(imageFormat: Int): Array<Size> {
- var outputs: Array<Size>? = outputSizesCache[imageFormat]
- if (outputs == null) {
- outputs = doGetAllOutputSizesByFormat(imageFormat)
- outputSizesCache[imageFormat] = outputs
- }
- return outputs
+ // Needs to retrieve the output size from the original stream configuration map without
+ // quirks applied.
+ val map: StreamConfigurationMap =
+ streamConfigurationMapCompat.toStreamConfigurationMap()
+ val outputSizes: Array<Size> =
+ if (imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+ // This is a little tricky that 0x22 that is internal defined in
+ // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
+ // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
+ // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
+ // retrieve the output sizes information.
+ map.getOutputSizes(SurfaceTexture::class.java)
+ } else {
+ map.getOutputSizes(imageFormat)
+ }
+ return Collections.max(outputSizes.asList(), CompareSizesByArea())
}
/**
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
index d9947f5..2a9dd47 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.camera2.pipe.integration.adapter;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
new file mode 100644
index 0000000..2f05905
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/CameraCompatModule.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat
+
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
+import dagger.Module
+
+/** Dependency bindings for adding camera compat related classes (e.g. workarounds, quirks etc.) */
+@Module(
+ includes = [
+ MeteringRegionCorrection.Bindings::class,
+ ],
+)
+abstract class CameraCompatModule
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
new file mode 100644
index 0000000..128b3f7
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import javax.inject.Inject
+
+/**
+ * Helper for accessing features in [StreamConfigurationMap] in a backwards compatible
+ * fashion.
+ *
+ * @param map [StreamConfigurationMap] class to wrap workarounds when output sizes are retrieved.
+ * @param outputSizesCorrector [OutputSizesCorrector] class to perform correction on sizes.
+ */
+@CameraScope
+@RequiresApi(21)
+class StreamConfigurationMapCompat @Inject constructor(
+ map: StreamConfigurationMap,
+ private val outputSizesCorrector: OutputSizesCorrector
+) {
+ private var impl: StreamConfigurationMapCompatImpl
+
+ init {
+ impl = if (Build.VERSION.SDK_INT >= 23) {
+ StreamConfigurationMapCompatApi23Impl(map)
+ } else {
+ StreamConfigurationMapCompatBaseImpl(map)
+ }
+ }
+
+ /**
+ * Get a list of sizes compatible with the requested image `format`.
+ *
+ *
+ * Output sizes related quirks will be applied onto the returned sizes list.
+ *
+ * @param format an image format from [ImageFormat] or [PixelFormat]
+ * @return an array of supported sizes, or `null` if the `format` is not a
+ * supported output
+ */
+ fun getOutputSizes(format: Int): Array<Size>? {
+ return outputSizesCorrector.applyQuirks(impl.getOutputSizes(format), format)
+ }
+
+ /**
+ * Get a list of sizes compatible with `klass` to use as an output.
+ *
+ *
+ * Output sizes related quirks will be applied onto the returned sizes list.
+ *
+ * @param klass a non-`null` [Class] object reference
+ * @return an array of supported sizes for [ImageFormat.PRIVATE] format,
+ * or `null` if the `klass` is not a supported output.
+ * @throws NullPointerException if `klass` was `null`
+ */
+ fun <T> getOutputSizes(klass: Class<T>): Array<Size>? {
+ return outputSizesCorrector.applyQuirks<T>(impl.getOutputSizes<T>(klass), klass)
+ }
+
+ /**
+ * Get a list of supported high resolution sizes, which cannot operate at full
+ * [CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE] rate.
+ *
+ *
+ * Output sizes related quirks will be applied onto the returned sizes list.
+ *
+ * @param format an image format from [ImageFormat] or [PixelFormat]
+ * @return an array of supported sizes, or `null` if the `format` is not a
+ * supported output
+ */
+ fun getHighResolutionOutputSizes(format: Int): Array<Size>? {
+ return outputSizesCorrector.applyQuirks(impl.getHighResolutionOutputSizes(format), format)
+ }
+
+ /**
+ * Returns the [StreamConfigurationMap] represented by this object.
+ */
+ fun toStreamConfigurationMap(): StreamConfigurationMap {
+ return impl.unwrap()
+ }
+
+ internal interface StreamConfigurationMapCompatImpl {
+ fun getOutputSizes(format: Int): Array<Size>?
+ fun <T> getOutputSizes(klass: Class<T>): Array<Size>?
+ fun getHighResolutionOutputSizes(format: Int): Array<Size>?
+ /**
+ * Returns the underlying [StreamConfigurationMap] instance.
+ */
+ fun unwrap(): StreamConfigurationMap
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatApi23Impl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatApi23Impl.kt
new file mode 100644
index 0000000..3bcbfd2
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatApi23Impl.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.util.Size
+import androidx.annotation.RequiresApi
+
+@RequiresApi(23)
+internal class StreamConfigurationMapCompatApi23Impl(map: StreamConfigurationMap) :
+ StreamConfigurationMapCompatBaseImpl(map) {
+ override fun getOutputSizes(format: Int): Array<Size>? {
+ return streamConfigurationMap.getOutputSizes(format)
+ }
+
+ override fun getHighResolutionOutputSizes(format: Int): Array<Size>? {
+ return streamConfigurationMap.getHighResolutionOutputSizes(format)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
new file mode 100644
index 0000000..feeb433
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.ImageFormatConstants
+
+@RequiresApi(21)
+internal open class StreamConfigurationMapCompatBaseImpl(
+ val streamConfigurationMap: StreamConfigurationMap
+) :
+ StreamConfigurationMapCompat.StreamConfigurationMapCompatImpl {
+ override fun getOutputSizes(format: Int): Array<Size>? {
+ val sizes: Array<Size> =
+ if (format == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+ // This is a little tricky that 0x22 that is internal defined in
+ // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
+ // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
+ // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
+ // retrieve the output sizes information.
+ streamConfigurationMap.getOutputSizes(SurfaceTexture::class.java)
+ } else {
+ streamConfigurationMap.getOutputSizes(format)
+ }
+ return sizes
+ }
+
+ override fun <T> getOutputSizes(klass: Class<T>): Array<Size>? {
+ return streamConfigurationMap.getOutputSizes(klass)
+ }
+
+ override fun getHighResolutionOutputSizes(format: Int): Array<Size>? {
+ return null
+ }
+
+ override fun unwrap(): StreamConfigurationMap {
+ return streamConfigurationMap
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/package-info.java
index 878a4d8..8b1057d 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/package-info.java
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.camera2.pipe.integration.compat;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirk.kt
new file mode 100644
index 0000000..bc62450
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirk.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.impl.Quirk
+
+/**
+ * Quirk denoting Af Region is incorrectly flipped horizontally.
+ *
+ * QuirkSummary
+ * - Bug Id: 210548792
+ * - Description: Regions set in [android.hardware.camera2.CaptureRequest.CONTROL_AF_REGIONS] are
+ * incorrectly flipped horizontally when using front-facing cameras.
+ * - Device(s): All Samsung devices.
+ *
+ * TODO(b/270421716): enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class AfRegionFlipHorizontallyQuirk : Quirk {
+ companion object {
+ fun isEnabled(cameraMetadata: CameraMetadata) =
+ Build.BRAND.equals("SAMSUNG", ignoreCase = true) &&
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && // Samsung fixed it in T.
+ (cameraMetadata[CameraCharacteristics.LENS_FACING]
+ == CameraCharacteristics.LENS_FACING_FRONT)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirk.kt
new file mode 100644
index 0000000..4d75e89
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirk.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.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+
+/**
+ * Quirk that should validate the video resolution of [EncoderProfilesProviderAdapter] on legacy
+ * camera.
+ *
+ * QuirkSummary
+ * - Bug Id: 180819729
+ * - Description: When using the Camera 2 API in `LEGACY` mode (i.e. when
+ * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL] is set to
+ * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY]),
+ * [EncoderProfilesProviderAdapter.hasProfile] may return `true` for unsupported resolutions.
+ * To ensure a given resolution is supported in LEGACY mode, the configuration given in
+ * [CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP] must contain the resolution in the
+ * supported output sizes. The recommended way to check this is with
+ * [StreamConfigurationMap.getOutputSizes] with the class of the desired recording endpoint, and
+ * check that the desired resolution is contained in the list returned.
+ * - Device(s): All legacy devices
+ *
+ * TODO: enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class CamcorderProfileResolutionQuirk(
+ private val streamConfigurationMapCompat: StreamConfigurationMapCompat
+) :
+ ProfileResolutionQuirk {
+
+ private val supportedResolution: List<Size> by lazy {
+ val sizes = streamConfigurationMapCompat
+ .getOutputSizes(ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
+
+ val result: List<Size> = sizes?.asList() ?: emptyList()
+ Log.debug { "supportedResolutions = $result" }
+ result
+ }
+
+ /** Returns the supported video resolutions. */
+ override fun getSupportedResolutions(): List<Size> {
+ return supportedResolution.toList()
+ }
+
+ companion object {
+ fun isEnabled(cameraMetadata: CameraMetadata): Boolean {
+ val level = cameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
+ return level != null &&
+ level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
new file mode 100644
index 0000000..c4dad6b
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.Quirks
+import javax.inject.Inject
+
+/** Provider of camera specific quirks. */
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+@CameraScope
+class CameraQuirks @Inject constructor(
+ private val cameraMetadata: CameraMetadata,
+ private val streamConfigurationMapCompat: StreamConfigurationMapCompat
+) {
+
+ /**
+ * Goes through all defined camera specific quirks, then filters them to retrieve quirks
+ * required for the camera identified by the provided [CameraMetadata].
+ */
+ val quirks: Quirks by lazy {
+ val quirks: MutableList<Quirk> = mutableListOf()
+
+ // Go through all defined camera quirks in lexicographical order,
+ // and add them to `quirks` if they should be loaded
+ if (AfRegionFlipHorizontallyQuirk.isEnabled(cameraMetadata)) {
+ quirks.add(AfRegionFlipHorizontallyQuirk())
+ }
+ if (CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata)) {
+ quirks.add(CamcorderProfileResolutionQuirk(streamConfigurationMapCompat))
+ }
+ if (JpegHalCorruptImageQuirk.isEnabled()) {
+ quirks.add(JpegHalCorruptImageQuirk())
+ }
+ if (YuvImageOnePixelShiftQuirk.isEnabled()) {
+ quirks.add(YuvImageOnePixelShiftQuirk())
+ }
+
+ Quirks(quirks)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
index 2092fe4..c9990ce 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
@@ -34,9 +34,27 @@
val quirks: MutableList<Quirk> = mutableListOf()
// Load all device specific quirks, preferably in lexicographical order
+ if (FlashAvailabilityBufferUnderflowQuirk.isEnabled()) {
+ quirks.add(FlashAvailabilityBufferUnderflowQuirk())
+ }
+
+ if (ImageCapturePixelHDRPlusQuirk.isEnabled()) {
+ quirks.add(ImageCapturePixelHDRPlusQuirk())
+ }
+
if (InvalidVideoProfilesQuirk.isEnabled()) {
quirks.add(InvalidVideoProfilesQuirk())
}
+ if (ExcludedSupportedSizesQuirk.load()) {
+ quirks.add(ExcludedSupportedSizesQuirk())
+ }
+ if (ExtraSupportedOutputSizeQuirk.load()) {
+ quirks.add(ExtraSupportedOutputSizeQuirk())
+ }
+
+ if (PreviewPixelHDRnetQuirk.isEnabled()) {
+ quirks.add(PreviewPixelHDRnetQuirk())
+ }
if (RepeatingStreamConstraintForVideoRecordingQuirk.isEnabled()) {
quirks.add(RepeatingStreamConstraintForVideoRecordingQuirk())
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExcludedSupportedSizesQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExcludedSupportedSizesQuirk.kt
new file mode 100644
index 0000000..c942233
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExcludedSupportedSizesQuirk.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.graphics.ImageFormat
+import android.os.Build
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.core.Logger
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.Quirk
+
+/**
+ * Quirk required to exclude certain supported surface sizes that are problematic.
+ *
+ * QuirkSummary
+ * Bug Id: b/157448499, b/192129158, b/245495234
+ * Description: These sizes are dependent on the device, camera and image format.
+ * An example is the resolution size 4000x3000 which is supported on OnePlus 6,
+ * but causes a WYSIWYG issue between preview and image capture. Another example
+ * is on Huawei P20 Lite, the Preview screen will become too bright when 400x400
+ * or 720x720 Preview resolutions are used together with a large zoom in value.
+ * The same symptom happens on ImageAnalysis. On Samsung J7 Prime (SM-G610M) or
+ * J7 (SM-J710MN) API 27 devices, the Preview images will be stretched if
+ * 1920x1080 resolution is used.
+ * Device(s): OnePlus 6, OnePlus 6T, Huawei P20, Samsung J7 Prime (SM-G610M) API 27, Samsung
+ * J7 (SM-J710MN) API 27
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class ExcludedSupportedSizesQuirk() : Quirk {
+ /**
+ * Retrieves problematic supported surface sizes that have to be excluded on the current
+ * device, for the given camera id and image format.
+ */
+ fun getExcludedSizes(cameraId: String, imageFormat: Int): List<Size> {
+ if (isOnePlus6) {
+ return getOnePlus6ExcludedSizes(cameraId, imageFormat)
+ }
+ if (isOnePlus6T) {
+ return getOnePlus6TExcludedSizes(cameraId, imageFormat)
+ }
+ if (isHuaweiP20Lite) {
+ return getHuaweiP20LiteExcludedSizes(cameraId, imageFormat, null)
+ }
+ if (isSamsungJ7PrimeApi27Above) {
+ return getSamsungJ7PrimeApi27AboveExcludedSizes(cameraId, imageFormat, null)
+ }
+ if (isSamsungJ7Api27Above) {
+ return getSamsungJ7Api27AboveExcludedSizes(cameraId, imageFormat, null)
+ }
+ Logger.w(TAG, "Cannot retrieve list of supported sizes to exclude on this device.")
+ return emptyList()
+ }
+
+ /**
+ * Retrieves problematic supported surface sizes that have to be excluded on the current
+ * device, for the given camera id and class type.
+ */
+ fun getExcludedSizes(cameraId: String, klass: Class<*>): List<Size> {
+ if (isHuaweiP20Lite) {
+ return getHuaweiP20LiteExcludedSizes(cameraId, UNKNOWN_IMAGE_FORMAT, klass)
+ }
+ if (isSamsungJ7PrimeApi27Above) {
+ return getSamsungJ7PrimeApi27AboveExcludedSizes(cameraId, UNKNOWN_IMAGE_FORMAT, klass)
+ }
+ if (isSamsungJ7Api27Above) {
+ return getSamsungJ7Api27AboveExcludedSizes(cameraId, UNKNOWN_IMAGE_FORMAT, klass)
+ }
+ Logger.w(TAG, "Cannot retrieve list of supported sizes to exclude on this device.")
+ return emptyList()
+ }
+
+ private fun getOnePlus6ExcludedSizes(cameraId: String, imageFormat: Int): List<Size> {
+ val sizes: MutableList<Size> = ArrayList()
+ if ((cameraId == "0") && imageFormat == ImageFormat.JPEG) {
+ sizes.add(Size(4160, 3120))
+ sizes.add(Size(4000, 3000))
+ }
+ return sizes
+ }
+
+ private fun getOnePlus6TExcludedSizes(cameraId: String, imageFormat: Int): List<Size> {
+ val sizes: MutableList<Size> = ArrayList()
+ if ((cameraId == "0") && imageFormat == ImageFormat.JPEG) {
+ sizes.add(Size(4160, 3120))
+ sizes.add(Size(4000, 3000))
+ }
+ return sizes
+ }
+
+ private fun getHuaweiP20LiteExcludedSizes(
+ cameraId: String,
+ imageFormat: Int,
+ klass: Class<*>?
+ ): List<Size> {
+ val sizes: MutableList<Size> = ArrayList()
+ // When klass is not null, the list for PRIVATE format should be returned.
+ if ((cameraId == "0") &&
+ ((imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) ||
+ (imageFormat == ImageFormat.YUV_420_888) || (klass != null))
+ ) {
+ sizes.add(Size(720, 720))
+ sizes.add(Size(400, 400))
+ }
+ return sizes
+ }
+
+ private fun getSamsungJ7PrimeApi27AboveExcludedSizes(
+ cameraId: String,
+ imageFormat: Int,
+ klass: Class<*>?
+ ): List<Size> {
+ val sizes: MutableList<Size> = ArrayList()
+
+ // When klass is not null, the list for PRIVATE format should be returned.
+ if ((cameraId == "0")) {
+ if ((imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE ||
+ klass != null)
+ ) {
+ sizes.add(Size(4128, 3096))
+ sizes.add(Size(4128, 2322))
+ sizes.add(Size(3088, 3088))
+ sizes.add(Size(3264, 2448))
+ sizes.add(Size(3264, 1836))
+ sizes.add(Size(2048, 1536))
+ sizes.add(Size(2048, 1152))
+ sizes.add(Size(1920, 1080))
+ } else if (imageFormat == ImageFormat.YUV_420_888) {
+ sizes.add(Size(4128, 2322))
+ sizes.add(Size(3088, 3088))
+ sizes.add(Size(3264, 2448))
+ sizes.add(Size(3264, 1836))
+ sizes.add(Size(2048, 1536))
+ sizes.add(Size(2048, 1152))
+ sizes.add(Size(1920, 1080))
+ }
+ } else if ((cameraId == "1")) {
+ if ((imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) ||
+ (imageFormat == ImageFormat.YUV_420_888) || (klass != null)
+ ) {
+ sizes.add(Size(3264, 2448))
+ sizes.add(Size(3264, 1836))
+ sizes.add(Size(2448, 2448))
+ sizes.add(Size(1920, 1920))
+ sizes.add(Size(2048, 1536))
+ sizes.add(Size(2048, 1152))
+ sizes.add(Size(1920, 1080))
+ }
+ }
+ return sizes
+ }
+
+ private fun getSamsungJ7Api27AboveExcludedSizes(
+ cameraId: String,
+ imageFormat: Int,
+ klass: Class<*>?
+ ): List<Size> {
+ val sizes: MutableList<Size> = ArrayList()
+
+ // When klass is not null, the list for PRIVATE format should be returned.
+ if ((cameraId == "0")) {
+ if ((imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE ||
+ klass != null)
+ ) {
+ sizes.add(Size(4128, 3096))
+ sizes.add(Size(4128, 2322))
+ sizes.add(Size(3088, 3088))
+ sizes.add(Size(3264, 2448))
+ sizes.add(Size(3264, 1836))
+ sizes.add(Size(2048, 1536))
+ sizes.add(Size(2048, 1152))
+ sizes.add(Size(1920, 1080))
+ } else if (imageFormat == ImageFormat.YUV_420_888) {
+ sizes.add(Size(2048, 1536))
+ sizes.add(Size(2048, 1152))
+ sizes.add(Size(1920, 1080))
+ }
+ } else if ((cameraId == "1")) {
+ if ((imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) ||
+ (imageFormat == ImageFormat.YUV_420_888) || (klass != null)
+ ) {
+ sizes.add(Size(2576, 1932))
+ sizes.add(Size(2560, 1440))
+ sizes.add(Size(1920, 1920))
+ sizes.add(Size(2048, 1536))
+ sizes.add(Size(2048, 1152))
+ sizes.add(Size(1920, 1080))
+ }
+ }
+ return sizes
+ }
+
+ companion object {
+ private const val TAG: String = "ExcludedSupportedSizesQuirk"
+ private const val UNKNOWN_IMAGE_FORMAT: Int = -1
+ fun load(): Boolean {
+ return (isOnePlus6 || isOnePlus6T || isHuaweiP20Lite || isSamsungJ7PrimeApi27Above ||
+ isSamsungJ7Api27Above)
+ }
+
+ internal val isOnePlus6: Boolean
+ get() = "OnePlus".equals(Build.BRAND, ignoreCase = true) && "OnePlus6".equals(
+ Build.DEVICE, ignoreCase = true
+ )
+ internal val isOnePlus6T: Boolean
+ get() = "OnePlus".equals(Build.BRAND, ignoreCase = true) && "OnePlus6T".equals(
+ Build.DEVICE, ignoreCase = true
+ )
+ internal val isHuaweiP20Lite: Boolean
+ get() {
+ return "HUAWEI".equals(
+ Build.BRAND,
+ ignoreCase = true
+ ) && "HWANE".equals(Build.DEVICE, ignoreCase = true)
+ }
+ internal val isSamsungJ7PrimeApi27Above: Boolean
+ get() {
+ return ("SAMSUNG".equals(Build.BRAND.uppercase(), ignoreCase = true) &&
+ "ON7XELTE".equals(Build.DEVICE.uppercase(), ignoreCase = true) &&
+ (Build.VERSION.SDK_INT >= 27))
+ }
+ internal val isSamsungJ7Api27Above: Boolean
+ get() {
+ return ("SAMSUNG".equals(Build.BRAND.uppercase(), ignoreCase = true) &&
+ "J7XELTE".equals(Build.DEVICE.uppercase(), ignoreCase = true) &&
+ (Build.VERSION.SDK_INT >= 27))
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedOutputSizeQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedOutputSizeQuirk.kt
new file mode 100644
index 0000000..971320f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedOutputSizeQuirk.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.Quirk
+
+/**
+ * QuirkSummary
+ * Bug Id: b/241876294
+ * Description: CamcorderProfile resolutions can not find a match in the output size list of
+ * [CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]. Since these resolutions
+ * can be supported in native camera app, add these resolutions back.
+ * Device(s): Motorola Moto E5 Play.
+ *
+ * TODO: enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class ExtraSupportedOutputSizeQuirk() : Quirk {
+ /**
+ * Returns the extra supported resolutions on the device.
+ */
+ fun getExtraSupportedResolutions(format: Int): Array<Size> {
+ return if ((format == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE &&
+ isMotoE5Play)
+ ) {
+ motoE5PlayExtraSupportedResolutions
+ } else {
+ arrayOf()
+ }
+ }
+
+ /**
+ * Returns the extra supported resolutions on the device.
+ */
+ fun <T> getExtraSupportedResolutions(klass: Class<T>): Array<Size> {
+ return if (StreamConfigurationMap.isOutputSupportedFor(klass) && isMotoE5Play) {
+ motoE5PlayExtraSupportedResolutions
+ } else {
+ arrayOf()
+ }
+ }
+
+ // Both the front and the main cameras support the following resolutions.
+ private val motoE5PlayExtraSupportedResolutions: Array<Size>
+ get() = arrayOf(
+ // FHD
+ Size(1920, 1080),
+ Size(1440, 1080),
+ // HD
+ Size(1280, 720),
+ Size(960, 720),
+ // SD (640:480 is already included in the original list)
+ Size(864, 480),
+ Size(720, 480)
+ )
+
+ companion object {
+ fun load(): Boolean {
+ return isMotoE5Play
+ }
+
+ internal val isMotoE5Play: Boolean
+ get() = "motorola".equals(
+ Build.BRAND,
+ ignoreCase = true
+ ) && "moto e5 play".equals(
+ Build.MODEL, ignoreCase = true
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.kt
new file mode 100644
index 0000000..bc50a37
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import java.nio.BufferUnderflowException
+import java.util.Locale
+
+/**
+ * A quirk for devices that throw a [BufferUnderflowException] when querying the flash availability.
+ *
+ * QuirkSummary
+ * - Bug Id: 216667482
+ * - Description: When attempting to retrieve the
+ * [CameraCharacteristics.FLASH_INFO_AVAILABLE] characteristic, a
+ * [BufferUnderflowException] is thrown. This is an undocumented exception
+ * on the [CameraCharacteristics.get] method, so this violates the API contract.
+ * - Device(s): Spreadtrum devices including LEMFO LEMP and DM20C
+ *
+ * TODO: enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class FlashAvailabilityBufferUnderflowQuirk : Quirk {
+
+ companion object {
+ private val KNOWN_AFFECTED_MODELS = setOf(
+ // Devices enumerated as DeviceInfo(Build.MANUFACTURER, Build.MODEL).
+ DeviceInfo("sprd", "lemp"),
+ DeviceInfo("sprd", "DM20C"),
+ )
+
+ fun isEnabled(): Boolean {
+ return KNOWN_AFFECTED_MODELS.contains(
+ DeviceInfo(Build.MANUFACTURER, Build.MODEL)
+ )
+ }
+ }
+
+ data class DeviceInfo private constructor(val manufacturer: String, val model: String) {
+ companion object {
+ operator fun invoke(manufacturer: String, model: String) =
+ DeviceInfo(manufacturer.lowercase(Locale.US), model.lowercase(Locale.US))
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ImageCapturePixelHDRPlusQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ImageCapturePixelHDRPlusQuirk.kt
new file mode 100644
index 0000000..150fa24
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ImageCapturePixelHDRPlusQuirk.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.impl.Quirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: b/123897971
+ * - Description: Quirk required to turn on/off HDR+ on Pixel devices by enabling/disabling
+ * zero-shutter-lag (ZSL) mode on the capture request, depending on the image
+ * capture use case's capture mode, i.e. prioritizing image capture latency over
+ * quality, or vice versa. This means that when the capture mode is
+ * [ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY], HDR+ is turned off by
+ * disabling ZSL, and when it is
+ * [ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY], HDR+ is turned on by enabling ZSL.
+ * - Device(s): Pixel 2, Pixel 2 XL, Pixel 3, Pixel 3 XL
+ *
+ * TODO: enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class ImageCapturePixelHDRPlusQuirk : Quirk {
+ companion object {
+ private val BUILD_MODELS = listOf(
+ "Pixel 2",
+ "Pixel 2 XL",
+ "Pixel 3",
+ "Pixel 3 XL"
+ )
+
+ fun isEnabled(): Boolean {
+ return BUILD_MODELS.contains(Build.MODEL) && "Google" == Build.MANUFACTURER &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirk.kt
new file mode 100644
index 0000000..764ab9b
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirk.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: 159831206, 242509463
+ * - Description: Corrupt images generally manifest as completely monochrome JPEGs, sometimes
+ * solid green. On the affected devices, this is easier to reproduce
+ * immediately after rebooting the device. If possible, it is preferred
+ * that CameraX produce JPEGs from some other image format rather than
+ * receiving JPEGs directly from the HAL. This issue happens on Samsung Galaxy S7.
+ * The other issue is that the Exif metadata of the captured images might be
+ * incorrect to cause IOException when using ExifInterface to save the updated
+ * attributes. Capturing the images in YUV format and then compress it to JPEG
+ * output images can produce correct Exif metadata to workaround this issue.
+ * - Device(s): Samsung Galaxy S7 (SM-G930T and SM-G930V variants), Alps k61v1_basic_ref
+ */
+@SuppressLint("CameraXQuirksClassDetector") // TODO(b/270421716): enable when kotlin is supported.
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class JpegHalCorruptImageQuirk : SoftwareJpegEncodingPreferredQuirk {
+ companion object {
+ private val KNOWN_AFFECTED_DEVICES = listOf(
+ "heroqltevzw",
+ "heroqltetmo",
+ "k61v1_basic_ref"
+ )
+
+ fun isEnabled(): Boolean {
+ return KNOWN_AFFECTED_DEVICES.contains(Build.DEVICE.lowercase())
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/PreviewPixelHDRnetQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/PreviewPixelHDRnetQuirk.kt
new file mode 100644
index 0000000..655f21b
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/PreviewPixelHDRnetQuirk.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import java.util.Locale
+
+/**
+ * Quirk required to turn on WYSIWYG viewfinder on Pixel devices
+ *
+ * QuirkSummary
+ * - Bug Id: 170598016
+ * - Description: Quirk denotes the devices to turn on WYSIWYG viewfinder. The default setting of
+ * the [android.hardware.camera2.CameraDevice.TEMPLATE_STILL_CAPTURE] enables the HDR+ for the
+ * still image capture request on Pixel phones, it leads to a better still image quality than the
+ * viewfinder output. To align the viewfinder quality with the final photo, we need to set
+ * TONEMAP_MODE to HIGH_QUALITY (the default is FAST) on the viewfinder stream to enable the
+ * WYSIWYG preview.
+ * - Device(s): Pixel 4a, Pixel 4a (5G), Pixel 5, Pixel 5a (5G)
+ *
+ * TODO: enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+class PreviewPixelHDRnetQuirk : Quirk {
+
+ companion object {
+
+ /** The devices that support wysiwyg preview in 3rd party apps (go/p20-wysiwyg-hdr) */
+ private val SUPPORTED_DEVICES = listOf("sunfish", "bramble", "redfin", "barbet")
+
+ fun isEnabled(): Boolean =
+ "Google".equals(Build.MANUFACTURER, ignoreCase = true) && SUPPORTED_DEVICES.contains(
+ Build.DEVICE.lowercase(Locale.getDefault())
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirk.kt
new file mode 100644
index 0000000..8e6bde2
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirk.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.internal.compat.quirk.OnePixelShiftQuirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: 184229033
+ * - Description: On certain devices, one pixel shifted when the HAL layer converts RGB data to
+ * YUV data. It leads to the leftmost column degradation when converting YUV to
+ * RGB in applications.
+ * - Device(s): Motorola MotoG3, Samsung SM-G532F/SM-J700F
+ */
+@SuppressLint("CameraXQuirksClassDetector") // TODO(b/270421716): enable when kotlin is supported.
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class YuvImageOnePixelShiftQuirk : OnePixelShiftQuirk {
+ companion object {
+ fun isEnabled() = isMotorolaMotoG3() || isSamsungSMG532F() || isSamsungSMGJ700F()
+
+ private fun isMotorolaMotoG3() =
+ "motorola".equals(Build.BRAND, ignoreCase = true) &&
+ "MotoG3".equals(Build.MODEL, ignoreCase = true)
+
+ private fun isSamsungSMG532F() =
+ "samsung".equals(Build.BRAND, ignoreCase = true) &&
+ "SM-G532F".equals(Build.MODEL, ignoreCase = true)
+
+ private fun isSamsungSMGJ700F() =
+ "samsung".equals(Build.BRAND, ignoreCase = true) &&
+ "SM-J700F".equals(Build.MODEL, ignoreCase = true)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/package-info.java
index eca73c3..ce9fe25 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/package-info.java
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.camera2.pipe.integration.compat.quirk;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/FlashAvailabilityChecker.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/FlashAvailabilityChecker.kt
new file mode 100644
index 0000000..febe978
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/FlashAvailabilityChecker.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.FlashAvailabilityBufferUnderflowQuirk
+import androidx.camera.camera2.pipe.integration.impl.CameraProperties
+import java.nio.BufferUnderflowException
+
+/**
+ * A workaround for devices which may throw a [BufferUnderflowException] when
+ * checking flash availability.
+ *
+ * @param allowRethrowOnError whether exceptions can be rethrown on devices that are not
+ * known to be problematic. If `false`, these devices will be
+ * logged as an error instead.
+ * @return the value of [CameraCharacteristics.FLASH_INFO_AVAILABLE] if it is contained
+ * in the characteristics, or `false` if it is not or a
+ * [BufferUnderflowException] is thrown while checking.
+ *
+ * @see FlashAvailabilityBufferUnderflowQuirk
+ */
+fun CameraProperties.isFlashAvailable(allowRethrowOnError: Boolean = false): Boolean {
+ val flashAvailable = try {
+ metadata[CameraCharacteristics.FLASH_INFO_AVAILABLE]
+ } catch (e: BufferUnderflowException) {
+ if (DeviceQuirks[FlashAvailabilityBufferUnderflowQuirk::class.java] != null) {
+ Log.debug {
+ "Device is known to throw an exception while checking flash availability. Flash" +
+ " is not available. [Manufacturer: ${Build.MANUFACTURER}, Model:" +
+ " ${Build.MODEL}, API Level: ${Build.VERSION.SDK_INT}]."
+ }
+ } else {
+ Log.error(e) {
+ "Exception thrown while checking for flash availability on device not known to " +
+ "throw exceptions during this check. Please file an issue at " +
+ "https://issuetracker.google.com/issues/new?component=618491&template=1257717" +
+ " with this error message [Manufacturer: ${Build.MANUFACTURER}, Model:" +
+ " ${Build.MODEL}, API Level: ${Build.VERSION.SDK_INT}]. Flash is not available."
+ }
+ }
+
+ if (allowRethrowOnError) {
+ throw e
+ } else {
+ false
+ }
+ }
+ if (flashAvailable == null) {
+ Log.warn {
+ "Characteristics did not contain key FLASH_INFO_AVAILABLE. Flash is not available."
+ }
+ }
+ return flashAvailable ?: false
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ImageCapturePixelHDRPlus.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ImageCapturePixelHDRPlus.kt
new file mode 100644
index 0000000..52aff5a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ImageCapturePixelHDRPlus.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CaptureRequest
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ImageCapturePixelHDRPlusQuirk
+import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.impl.ImageCaptureConfig
+
+/**
+ * Turns on or turns off HDR+ on Pixel devices depending on the image capture use case's
+ * capture mode. When the mode is [ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY], HDR+ is
+ * turned off by disabling ZSL. When the mode is
+ * [ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY], HDR+ is turned on by enabling ZSL.
+ *
+ * @see ImageCapturePixelHDRPlusQuirk
+ */
+@SuppressLint("NewApi")
+fun Camera2ImplConfig.Builder.toggleHDRPlus(imageCaptureConfig: ImageCaptureConfig) {
+
+ DeviceQuirks[ImageCapturePixelHDRPlusQuirk::class.java] ?: return
+ if (!imageCaptureConfig.hasCaptureMode()) return
+
+ when (imageCaptureConfig.captureMode) {
+ // enable ZSL to make sure HDR+ is enabled
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY ->
+ setCaptureRequestOption(CaptureRequest.CONTROL_ENABLE_ZSL, true)
+
+ // disable ZSL to turn off HDR+
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY ->
+ setCaptureRequestOption(CaptureRequest.CONTROL_ENABLE_ZSL, false)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MeteringRegionCorrection.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MeteringRegionCorrection.kt
new file mode 100644
index 0000000..b20884e9
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MeteringRegionCorrection.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.graphics.PointF
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.AfRegionFlipHorizontallyQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.MeteringPoint
+import dagger.Module
+import dagger.Provides
+
+interface MeteringRegionCorrection {
+ fun getCorrectedPoint(
+ meteringPoint: MeteringPoint,
+ @FocusMeteringAction.MeteringMode meteringMode: Int,
+ ): PointF
+
+ @Module
+ abstract class Bindings {
+ companion object {
+ @Provides
+ fun provideMeteringRegionCorrection(
+ cameraQuirks: CameraQuirks
+ ): MeteringRegionCorrection {
+ return if (cameraQuirks.quirks.contains(AfRegionFlipHorizontallyQuirk::class.java))
+ MeteringRegionQuirkCorrection
+ else NoOpMeteringRegionCorrection
+ }
+ }
+ }
+}
+
+object MeteringRegionQuirkCorrection : MeteringRegionCorrection {
+ /**
+ * Return corrected normalized point by given MeteringPoint, MeteringMode and Quirks.
+ */
+ override fun getCorrectedPoint(
+ meteringPoint: MeteringPoint,
+ @FocusMeteringAction.MeteringMode meteringMode: Int,
+ ) = when (meteringMode) {
+ FocusMeteringAction.FLAG_AF -> PointF(1f - meteringPoint.x, meteringPoint.y)
+ else -> PointF(meteringPoint.x, meteringPoint.y)
+ }
+}
+
+object NoOpMeteringRegionCorrection : MeteringRegionCorrection {
+ override fun getCorrectedPoint(meteringPoint: MeteringPoint, meteringMode: Int) =
+ PointF(meteringPoint.x, meteringPoint.y)
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
new file mode 100644
index 0000000..a873e6b
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import javax.inject.Inject
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExcludedSupportedSizesQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraSupportedOutputSizeQuirk
+
+/**
+ * Helper class to provide the StreamConfigurationMap output sizes related correction functions.
+ *
+ * 1. ExtraSupportedOutputSizeQuirk
+ * 2. ExcludedSupportedSizesContainer
+ * 3. TargetAspectRatio
+ */
+@CameraScope
+@RequiresApi(21)
+class OutputSizesCorrector @Inject constructor(
+ private val cameraMetadata: CameraMetadata
+) {
+ private val excludedSupportedSizesQuirk: ExcludedSupportedSizesQuirk? =
+ DeviceQuirks[ExcludedSupportedSizesQuirk::class.java]
+ private val extraSupportedOutputSizeQuirk: ExtraSupportedOutputSizeQuirk? =
+ DeviceQuirks[ExtraSupportedOutputSizeQuirk::class.java]
+
+ /**
+ * Applies the output sizes related quirks onto the input sizes array.
+ */
+ fun applyQuirks(sizes: Array<Size>?, format: Int): Array<Size>? {
+ var result = addExtraSupportedOutputSizesByFormat(sizes, format)
+ result = excludeProblematicOutputSizesByFormat(result, format)
+ return excludeOutputSizesByTargetAspectRatioWorkaround(result)
+ }
+
+ /**
+ * Applies the output sizes related quirks onto the input sizes array.
+ */
+ fun <T> applyQuirks(sizes: Array<Size>?, klass: Class<T>): Array<Size>? {
+ var result = addExtraSupportedOutputSizesByClass(sizes, klass)
+ result = excludeProblematicOutputSizesByClass(result, klass)
+ return excludeOutputSizesByTargetAspectRatioWorkaround(result)
+ }
+
+ /**
+ * Adds extra supported output sizes for the specified format by ExtraSupportedOutputSizeQuirk.
+ */
+ private fun addExtraSupportedOutputSizesByFormat(
+ sizes: Array<Size>?,
+ format: Int
+ ): Array<Size>? {
+ if (sizes == null || extraSupportedOutputSizeQuirk == null) {
+ return sizes
+ }
+ val extraSizes: Array<Size> =
+ extraSupportedOutputSizeQuirk.getExtraSupportedResolutions(format)
+ return concatNullableSizeLists(sizes.toList(), extraSizes.toList()).toTypedArray()
+ }
+
+ /**
+ * Adds extra supported output sizes for the specified class by ExtraSupportedOutputSizeQuirk.
+ */
+ private fun <T> addExtraSupportedOutputSizesByClass(
+ sizes: Array<Size>?,
+ klass: Class<T>
+ ): Array<Size>? {
+ if (sizes == null || extraSupportedOutputSizeQuirk == null) {
+ return sizes
+ }
+ val extraSizes: Array<Size> =
+ extraSupportedOutputSizeQuirk.getExtraSupportedResolutions(klass)
+ return concatNullableSizeLists(sizes.toList(), extraSizes.toList()).toTypedArray()
+ }
+
+ /**
+ * Excludes problematic output sizes for the specified format by
+ * ExcludedSupportedSizesContainer.
+ */
+ private fun excludeProblematicOutputSizesByFormat(
+ sizes: Array<Size>?,
+ format: Int
+ ): Array<Size>? {
+ if (sizes == null || excludedSupportedSizesQuirk == null) {
+ return sizes
+ }
+ val excludedSizes: List<Size> =
+ excludedSupportedSizesQuirk.getExcludedSizes(cameraMetadata.camera.value, format)
+
+ val resultList: MutableList<Size> = sizes.toMutableList()
+ resultList.removeAll(excludedSizes)
+ return resultList.toTypedArray()
+ }
+
+ /**
+ * Excludes problematic output sizes for the specified class type by
+ * ExcludedSupportedSizesContainer.
+ */
+ private fun <T> excludeProblematicOutputSizesByClass(
+ sizes: Array<Size>?,
+ klass: Class<T>
+ ): Array<Size>? {
+ if (sizes == null || excludedSupportedSizesQuirk == null) {
+ return sizes
+ }
+ val excludedSizes: List<Size> =
+ excludedSupportedSizesQuirk.getExcludedSizes(cameraMetadata.camera.value, klass)
+
+ val resultList: MutableList<Size> = sizes.toMutableList()
+ resultList.removeAll(excludedSizes)
+ return resultList.toTypedArray()
+ }
+
+ /**
+ * Excludes output sizes by TargetAspectRatio.
+ */
+ private fun excludeOutputSizesByTargetAspectRatioWorkaround(sizes: Array<Size>?): Array<Size>? {
+ // TODO(b/245622117): Nexus4AndroidLTargetAspectRatioQuirk and AspectRatioLegacyApi21Quirk
+ return sizes
+ }
+
+ private fun concatNullableSizeLists(
+ sizeList1: List<Size>,
+ sizeList2: List<Size>
+ ): List<Size> {
+ val resultList: MutableList<Size> = ArrayList(sizeList1)
+ resultList.addAll(sizeList2)
+ return resultList
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnet.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnet.kt
new file mode 100644
index 0000000..496e17e
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnet.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CaptureRequest
+import android.util.Rational
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.PreviewPixelHDRnetQuirk
+import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
+import androidx.camera.core.impl.SessionConfig
+
+private val ASPECT_RATIO_16_9 = Rational(16, 9)
+
+/**
+ * Turns on WYSIWYG viewfinder on Pixel devices
+ *
+ * @see PreviewPixelHDRnetQuirk
+ */
+fun SessionConfig.Builder.setupHDRnet(resolution: Size) {
+ DeviceQuirks[PreviewPixelHDRnetQuirk::class.java] ?: return
+
+ if (isAspectRatioMatch(resolution, ASPECT_RATIO_16_9)) return
+
+ val camera2ConfigBuilder = Camera2ImplConfig.Builder().apply {
+ setCaptureRequestOption<Int>(
+ CaptureRequest.TONEMAP_MODE,
+ CaptureRequest.TONEMAP_MODE_HIGH_QUALITY
+ )
+ }
+
+ addImplementationOptions(camera2ConfigBuilder.build())
+}
+
+private fun isAspectRatioMatch(
+ resolution: Size,
+ aspectRatio: Rational
+): Boolean {
+ return aspectRatio == Rational(resolution.width, resolution.height)
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/package-info.java
new file mode 100644
index 0000000..556205d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.camera2.pipe.integration.compat.workaround;
+
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
index de3e23f..bd3ebe4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -18,6 +18,8 @@
package androidx.camera.camera2.pipe.integration.config
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.StreamConfigurationMap
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.pipe.CameraId
@@ -27,6 +29,7 @@
import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraInternalAdapter
import androidx.camera.camera2.pipe.integration.compat.Camera2CameraControlCompat
+import androidx.camera.camera2.pipe.integration.compat.CameraCompatModule
import androidx.camera.camera2.pipe.integration.compat.EvCompCompat
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
import androidx.camera.camera2.pipe.integration.impl.CameraPipeCameraProperties
@@ -49,6 +52,7 @@
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
+import javax.inject.Named
import javax.inject.Scope
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -113,9 +117,22 @@
requestListener
)
+ @CameraScope
@Provides
fun provideCameraMetadata(cameraPipe: CameraPipe, config: CameraConfig): CameraMetadata =
checkNotNull(cameraPipe.cameras().awaitCameraMetadata(config.cameraId))
+
+ @CameraScope
+ @Provides
+ @Named("CameraId")
+ fun provideCameraIdString(config: CameraConfig): String = config.cameraId.value
+
+ @CameraScope
+ @Provides
+ fun provideStreamConfigurationMap(cameraMetadata: CameraMetadata): StreamConfigurationMap {
+ return cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
+ ?: throw IllegalArgumentException("Cannot retrieve SCALER_STREAM_CONFIGURATION_MAP")
+ }
}
@Binds
@@ -143,7 +160,8 @@
@Subcomponent(
modules = [
CameraModule::class,
- CameraConfig::class
+ CameraConfig::class,
+ CameraCompatModule::class,
]
)
interface CameraComponent {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
index b7d2f6a..13d60f9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
package androidx.camera.camera2.pipe.integration.config;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
index be5d49f..1ed99be 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
@@ -76,7 +76,6 @@
/**
* Returns all capture request options contained in this configuration.
*
- * @hide
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
val captureRequestOptions: CaptureRequestOptions
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
index 0d8933c..7231322 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
@@ -47,6 +47,7 @@
import androidx.camera.camera2.pipe.RequestMetadata
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
@@ -92,10 +93,14 @@
private val torchControl: TorchControl,
private val threads: UseCaseThreads,
private val requestListener: ComboRequestListener,
+ cameraProperties: CameraProperties,
useCaseGraphConfig: UseCaseGraphConfig,
) : CapturePipeline {
private val graph = useCaseGraphConfig.graph
+ // If there is no flash unit, skip the flash related task instead of failing the pipeline.
+ private val hasFlashUnit = cameraProperties.isFlashAvailable()
+
override var template = CameraDevice.TEMPLATE_PREVIEW
override suspend fun submitStillCaptures(
@@ -114,21 +119,10 @@
captureMode: Int,
flashMode: Int,
): List<Deferred<Void?>> =
- if (isFlashRequired(flashMode)) {
+ if (hasFlashUnit && isFlashRequired(flashMode)) {
torchApplyCapture(requests, captureMode, CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS)
} else {
- val lock3ARequired = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY
- if (lock3ARequired) {
- lock3A(CHECK_3A_TIMEOUT_IN_NS)
- }
- submitRequestInternal(requests).also { captureSignal ->
- if (lock3ARequired) {
- threads.sequentialScope.launch {
- captureSignal.joinAll()
- unlock3A()
- }
- }
- }
+ defaultNoFlashCapture(requests, captureMode)
}
private suspend fun defaultCapture(
@@ -136,14 +130,36 @@
captureMode: Int,
flashMode: Int,
): List<Deferred<Void?>> {
- val isFlashRequired = isFlashRequired(flashMode)
- val timeout =
- if (isFlashRequired) CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS else CHECK_3A_TIMEOUT_IN_NS
+ return if (hasFlashUnit) {
+ val isFlashRequired = isFlashRequired(flashMode)
+ val timeout =
+ if (isFlashRequired) CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS else CHECK_3A_TIMEOUT_IN_NS
- return if (isFlashRequired || captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
- aePreCaptureApplyCapture(requests, timeout)
+ if (isFlashRequired || captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
+ aePreCaptureApplyCapture(requests, timeout)
+ } else {
+ defaultNoFlashCapture(requests, captureMode)
+ }
} else {
- submitRequestInternal(requests)
+ defaultNoFlashCapture(requests, captureMode)
+ }
+ }
+
+ private suspend fun defaultNoFlashCapture(
+ requests: List<Request>,
+ captureMode: Int
+ ): List<Deferred<Void?>> {
+ val lock3ARequired = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY
+ if (lock3ARequired) {
+ lock3A(CHECK_3A_TIMEOUT_IN_NS)
+ }
+ return submitRequestInternal(requests).also { captureSignal ->
+ if (lock3ARequired) {
+ threads.sequentialScope.launch {
+ captureSignal.joinAll()
+ unlock3A()
+ }
+ }
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index d2de017..5541a40 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -26,10 +26,12 @@
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.core.CameraControl.OperationCanceledException
import androidx.camera.core.FocusMeteringAction
@@ -54,6 +56,7 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class FocusMeteringControl @Inject constructor(
private val cameraProperties: CameraProperties,
+ private val meteringRegionCorrection: MeteringRegionCorrection,
private val state3AControl: State3AControl,
private val threads: UseCaseThreads,
private val zoomCompat: ZoomCompat,
@@ -116,19 +119,25 @@
action.meteringPointsAe,
maxAeRegionCount,
cropSensorRegion,
- defaultAspectRatio
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AE,
+ meteringRegionCorrection,
)
val afRectangles = meteringRegionsFromMeteringPoints(
action.meteringPointsAf,
maxAfRegionCount,
cropSensorRegion,
- defaultAspectRatio
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AF,
+ meteringRegionCorrection,
)
val awbRectangles = meteringRegionsFromMeteringPoints(
action.meteringPointsAwb,
maxAwbRegionCount,
cropSensorRegion,
- defaultAspectRatio
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AWB,
+ meteringRegionCorrection,
)
if (aeRectangles.isEmpty() && afRectangles.isEmpty() && awbRectangles.isEmpty()) {
signal.completeExceptionally(
@@ -150,10 +159,21 @@
(false to autoFocusTimeoutMs)
}
withTimeoutOrNull(timeout) {
+ /**
+ * If device does not support a 3A region, we should not update it at all.
+ * If device does support but a region list is empty, it means any previously
+ * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
+ */
useCaseCamera.requestControl.startFocusAndMeteringAsync(
- aeRegions = aeRectangles,
- afRegions = afRectangles,
- awbRegions = awbRectangles,
+ aeRegions = if (maxAeRegionCount > 0)
+ aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ afRegions = if (maxAfRegionCount > 0)
+ afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ awbRegions = if (maxAwbRegionCount > 0)
+ awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON)
).await()
}.let { result3A ->
@@ -200,19 +220,25 @@
action.meteringPointsAe,
maxAeRegionCount,
cropSensorRegion,
- defaultAspectRatio
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AE,
+ meteringRegionCorrection,
)
val rectanglesAf = meteringRegionsFromMeteringPoints(
action.meteringPointsAf,
maxAfRegionCount,
cropSensorRegion,
- defaultAspectRatio
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AF,
+ meteringRegionCorrection,
)
val rectanglesAwb = meteringRegionsFromMeteringPoints(
action.meteringPointsAwb,
maxAwbRegionCount,
cropSensorRegion,
- defaultAspectRatio
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AWB,
+ meteringRegionCorrection,
)
return rectanglesAe.isNotEmpty() || rectanglesAf.isNotEmpty() || rectanglesAwb.isNotEmpty()
}
@@ -315,6 +341,8 @@
maxRegionCount: Int,
cropSensorRegion: Rect,
defaultAspectRatio: Rational,
+ @FocusMeteringAction.MeteringMode meteringMode: Int,
+ meteringRegionCorrection: MeteringRegionCorrection,
): List<MeteringRectangle> {
if (meteringPoints.isEmpty() || maxRegionCount == 0) {
return emptyList()
@@ -331,8 +359,13 @@
if (!isValid(meteringPoint)) {
continue
}
- val adjustedPoint: PointF =
- getFovAdjustedPoint(meteringPoint, cropRegionAspectRatio, defaultAspectRatio)
+ val adjustedPoint: PointF = getFovAdjustedPoint(
+ meteringPoint,
+ cropRegionAspectRatio,
+ defaultAspectRatio,
+ meteringMode,
+ meteringRegionCorrection
+ )
val meteringRectangle: MeteringRectangle =
getMeteringRect(adjustedPoint, meteringPoint.size, cropSensorRegion)
meteringRegions.add(meteringRectangle)
@@ -344,13 +377,19 @@
private fun getFovAdjustedPoint(
meteringPoint: MeteringPoint,
cropRegionAspectRatio: Rational,
- defaultAspectRatio: Rational
+ defaultAspectRatio: Rational,
+ @FocusMeteringAction.MeteringMode meteringMode: Int,
+ meteringRegionCorrection: MeteringRegionCorrection,
): PointF {
// Use default aspect ratio unless there is a custom aspect ratio in MeteringPoint.
val fovAspectRatio = meteringPoint.surfaceAspectRatio ?: defaultAspectRatio
+ val correctedPoint = meteringRegionCorrection.getCorrectedPoint(
+ meteringPoint,
+ meteringMode
+ )
if (fovAspectRatio != cropRegionAspectRatio) {
if (fovAspectRatio.compareTo(cropRegionAspectRatio) > 0) {
- val adjustedPoint = PointF(meteringPoint.x, meteringPoint.y)
+ val adjustedPoint = PointF(correctedPoint.x, correctedPoint.y)
// The crop region is taller than the FOV, top and bottom of the crop region is
// cropped.
val verticalPadding =
@@ -359,7 +398,7 @@
adjustedPoint.y = (topPadding + adjustedPoint.y) * (1f / verticalPadding)
return adjustedPoint
} else {
- val adjustedPoint = PointF(meteringPoint.x, meteringPoint.y)
+ val adjustedPoint = PointF(correctedPoint.x, correctedPoint.y)
// The crop region is wider than the FOV, left and right side of crop region is
// cropped
val horizontalPadding =
@@ -369,7 +408,7 @@
return adjustedPoint
}
}
- return PointF(meteringPoint.x, meteringPoint.y)
+ return PointF(correctedPoint.x, correctedPoint.y)
}
// Given a normalized PointF and normalized size factor for width and height, calculate
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 6294da8..9f2844a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -76,7 +76,7 @@
Builder(cameraProperties, displayInfoManager)
override fun onSuggestedStreamSpecUpdated(suggestedStreamSpec: StreamSpec): StreamSpec {
- updateSessionConfig(createPipeline().build())
+ updateSessionConfig(createPipeline(meteringSurfaceSize).build())
notifyActive()
return StreamSpec.builder(meteringSurfaceSize).build()
}
@@ -95,15 +95,15 @@
updateSuggestedStreamSpec(StreamSpec.builder(DEFAULT_PREVIEW_SIZE).build())
}
- private fun createPipeline(): SessionConfig.Builder {
+ private fun createPipeline(resolution: Size): SessionConfig.Builder {
synchronized(deferrableSurfaceLock) {
val surfaceTexture = SurfaceTexture(0).apply {
- setDefaultBufferSize(meteringSurfaceSize.width, meteringSurfaceSize.height)
+ setDefaultBufferSize(resolution.width, resolution.height)
}
val surface = Surface(surfaceTexture)
deferrableSurface?.close()
- deferrableSurface = ImmediateSurface(surface, meteringSurfaceSize, imageFormat)
+ deferrableSurface = ImmediateSurface(surface, resolution, imageFormat)
deferrableSurface!!.terminationFuture
.addListener(
{
@@ -115,12 +115,12 @@
}
return SessionConfig.Builder
- .createFrom(MeteringRepeatingConfig())
+ .createFrom(MeteringRepeatingConfig(), resolution)
.apply {
setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
addSurface(deferrableSurface!!)
addErrorListener { _, _ ->
- updateSessionConfig(createPipeline().build())
+ updateSessionConfig(createPipeline(resolution).build())
notifyReset()
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
index 21e752a..5822ecd 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
@@ -16,10 +16,10 @@
package androidx.camera.camera2.pipe.integration.impl
-import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CaptureRequest
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
+import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.core.CameraControl
import androidx.camera.core.TorchState
@@ -68,10 +68,7 @@
setTorchAsync(false)
}
- private val hasFlashUnit: Boolean =
- cameraProperties.metadata[CameraCharacteristics.FLASH_INFO_AVAILABLE].let {
- it != null && it
- }
+ private val hasFlashUnit: Boolean = cameraProperties.isFlashAvailable()
private val _torchState = MutableLiveData(TorchState.OFF)
val torchStateLiveData: LiveData<Int>
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index b7ca0cb..3e96a04 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -102,8 +102,7 @@
*
* This method will:
* (1) Stores [config], [tags] and [listeners] by [type] respectively. The new inputs above
- * will take place of the existing value of the [type]. If the [type] isn't set, it will
- * override the config which is stored as the [Type.DEFAULT].
+ * will take place of the existing value of the [type].
* (2) Update the repeating request by merging all the [config], [tags] and [listeners] from
* all the defined types.
*
@@ -130,9 +129,9 @@
// 3A
suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A>
suspend fun startFocusAndMeteringAsync(
- aeRegions: List<MeteringRectangle>,
- afRegions: List<MeteringRectangle>,
- awbRegions: List<MeteringRectangle>,
+ aeRegions: List<MeteringRectangle>? = null,
+ afRegions: List<MeteringRectangle>? = null,
+ awbRegions: List<MeteringRectangle>? = null,
afTriggerStartAeMode: AeMode? = null
): Deferred<Result3A>
@@ -217,9 +216,9 @@
}
override suspend fun startFocusAndMeteringAsync(
- aeRegions: List<MeteringRectangle>,
- afRegions: List<MeteringRectangle>,
- awbRegions: List<MeteringRectangle>,
+ aeRegions: List<MeteringRectangle>?,
+ afRegions: List<MeteringRectangle>?,
+ awbRegions: List<MeteringRectangle>?,
afTriggerStartAeMode: AeMode?
): Deferred<Result3A> = graph.acquireSession().use {
it.lock3A(
@@ -357,4 +356,4 @@
fun TagBundle.toMap(): Map<String, Any> = mutableMapOf<String, Any>().also {
listKeys().forEach { tagKey -> it[tagKey] = getTag(tagKey) as Any }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/package-info.java
index 2092b25..c49f378 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/package-info.java
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
package androidx.camera.camera2.pipe.integration.impl;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/package-info.java b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/package-info.java
index 29b62d3..8391631 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/package-info.java
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.camera2.pipe.integration.internal;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2Interop.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2Interop.kt
index a373bc0..e12fbdc 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2Interop.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2Interop.kt
@@ -87,7 +87,6 @@
*
* @param templateType The template type to set.
* @return The current Extender.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun setCaptureRequestTemplate(templateType: Int): Extender<T> {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapterTest.kt
index 521bea4..05e8e70 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapterTest.kt
@@ -21,6 +21,7 @@
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import android.os.Build
+import android.util.Size
import android.view.Surface
import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
import androidx.camera.camera2.pipe.integration.impl.createCaptureRequestOption
@@ -41,6 +42,8 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraUseCaseAdapterTest {
+ private val resolution: Size = Size(640, 480)
+
@Test
fun shouldApplyOptionsFromConfigToBuilder_whenDefaultConfigSet() {
// Arrange
@@ -60,7 +63,7 @@
val builder = CaptureConfig.Builder()
// Act
- CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.unpack(useCaseConfig, builder)
+ CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE.unpack(useCaseConfig, builder)
// Assert
val config = builder.build()
@@ -115,7 +118,8 @@
val builder = SessionConfig.Builder()
// Act
- CameraUseCaseAdapter.DefaultSessionOptionsUnpacker.unpack(useCaseConfig, builder)
+ CameraUseCaseAdapter.DefaultSessionOptionsUnpacker.unpack(resolution,
+ useCaseConfig, builder)
// Assert
val config = builder.build()
@@ -159,6 +163,7 @@
// Act
val sessionBuilder = SessionConfig.Builder()
CameraUseCaseAdapter.DefaultSessionOptionsUnpacker.unpack(
+ resolution,
imageCaptureBuilder.useCaseConfig,
sessionBuilder
)
@@ -200,7 +205,8 @@
val sessionBuilder = SessionConfig.Builder()
// Act
- CameraUseCaseAdapter.DefaultSessionOptionsUnpacker.unpack(useCaseConfig, sessionBuilder)
+ CameraUseCaseAdapter.DefaultSessionOptionsUnpacker.unpack(resolution,
+ useCaseConfig, sessionBuilder)
val sessionConfig = sessionBuilder.build()
// Assert
@@ -239,7 +245,7 @@
// Act
val captureBuilder = CaptureConfig.Builder()
- CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.unpack(
+ CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE.unpack(
imageCaptureBuilder.useCaseConfig,
captureBuilder
)
@@ -277,7 +283,9 @@
val captureBuilder = CaptureConfig.Builder()
// Act
- CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.unpack(useCaseConfig, captureBuilder)
+ CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE.unpack(
+ useCaseConfig, captureBuilder
+ )
val captureConfig = captureBuilder.build()
// Assert
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 3ea7d96..baccf5f 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -27,9 +27,15 @@
import android.os.Build
import android.util.Rational
import android.util.Size
+import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.Result3A
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
+import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpMeteringRegionCorrection
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.FocusMeteringControl
import androidx.camera.camera2.pipe.integration.impl.State3AControl
@@ -53,6 +59,7 @@
import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeUseCase
+import androidx.concurrent.futures.await
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -81,11 +88,15 @@
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+import org.robolectric.util.ReflectionHelpers
private const val CAMERA_ID_0 = "0" // 640x480 sensor size
private const val CAMERA_ID_1 = "1" // 1920x1080 sensor size
private const val CAMERA_ID_2 = "2" // 640x480 sensor size, not support AF_AUTO.
private const val CAMERA_ID_3 = "3" // camera that does not support 3A regions.
+private const val CAMERA_ID_4 = "4" // camera 0 with LENS_FACING_FRONT
+private const val CAMERA_ID_5 = "5" // camera 0 supporting AF regions only
private const val SENSOR_WIDTH = 640
private const val SENSOR_HEIGHT = 480
@@ -105,6 +116,7 @@
SENSOR_WIDTH,
SENSOR_HEIGHT
)
+
// the following rectangles are for metering point (0, 0)
private val M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480 = Rect(
0, 60 - AREA_HEIGHT / 2,
@@ -169,7 +181,9 @@
listOf(meteringPoint),
1,
Rect(0, 0, 800, 600),
- Rational(4, 3)
+ Rational(4, 3),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles.size).isEqualTo(1)
// Aspect ratio of crop region is same as default aspect ratio. So no padding is needed
@@ -185,7 +199,10 @@
listOf(meteringPoint1),
1,
Rect(0, 0, 800, 600),
- Rational(4, 3)
+ Rational(4, 3),
+ FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE
+ or FocusMeteringAction.FLAG_AWB,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles1.size).isEqualTo(1)
// Aspect ratio of crop region is same as default aspect ratio. So no padding is needed
@@ -200,7 +217,9 @@
listOf(meteringPoint2),
1,
Rect(0, 0, 800, 600),
- Rational(4, 3)
+ Rational(4, 3),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles2.size).isEqualTo(1)
// Aspect ratio of crop region is same as default aspect ratio. So no padding is needed
@@ -219,7 +238,10 @@
listOf(meteringPoint),
1,
Rect(0, 0, 400, 400),
- Rational(4, 3)
+ Rational(4, 3),
+ FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE
+ or FocusMeteringAction.FLAG_AWB,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles.size).isEqualTo(1)
// Default aspect ratio is greater than the aspect ratio of the crop region. So we need
@@ -234,7 +256,9 @@
listOf(meteringPoint1),
1,
Rect(0, 0, 400, 400),
- Rational(4, 3)
+ Rational(4, 3),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles1.size).isEqualTo(1)
val expectedMeteringRectangle1 = MeteringRectangle(
@@ -247,7 +271,9 @@
listOf(meteringPoint2),
1,
Rect(0, 0, 400, 400),
- Rational(4, 3)
+ Rational(4, 3),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles2.size).isEqualTo(1)
// Default aspect ratio is greater than the aspect ratio of the crop region. So we need
@@ -265,7 +291,9 @@
listOf(meteringPoint),
1,
Rect(0, 0, 400, 400),
- Rational(3, 4)
+ Rational(3, 4),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles.size).isEqualTo(1)
val expectedMeteringRectangle = MeteringRectangle(
@@ -278,7 +306,9 @@
listOf(meteringPoint1),
1,
Rect(0, 0, 400, 400),
- Rational(3, 4)
+ Rational(3, 4),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles1.size).isEqualTo(1)
val expectedMeteringRectangle1 = MeteringRectangle(
@@ -291,7 +321,9 @@
listOf(meteringPoint2),
1,
Rect(0, 0, 400, 400),
- Rational(3, 4)
+ Rational(3, 4),
+ FocusMeteringAction.FLAG_AF,
+ NoOpMeteringRegionCorrection,
)
assertThat(meteringRectangles2.size).isEqualTo(1)
val expectedMeteringRectangle2 = MeteringRectangle(
@@ -304,18 +336,11 @@
fun startFocusAndMetering_invalidPoint() = runBlocking {
val invalidPoint = pointFactory.createPoint(1f, 1.1f)
- startFocusMeteringAndAwait(FocusMeteringAction.Builder(invalidPoint).build())
+ val future = focusMeteringControl.startFocusAndMetering(
+ FocusMeteringAction.Builder(invalidPoint).build()
+ )
- // TODO: This will probably throw an invalid argument exception in future instead of
- // passing the parameters to request control, better to assert the exception then.
-
- val meteringRequests = fakeRequestControl.focusMeteringCalls.lastOrNull()
- ?: FakeUseCaseCameraRequestControl.FocusMeteringParams()
- with(meteringRequests) {
- assertWithMessage("Wrong number of AE regions").that(aeRegions.size).isEqualTo(0)
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(0)
- assertWithMessage("Wrong number of AWB regions").that(awbRegions.size).isEqualTo(0)
- }
+ assertFutureFailedWithIllegalArgumentException(future)
}
@Test
@@ -323,14 +348,14 @@
startFocusMeteringAndAwait(FocusMeteringAction.Builder(point1).build())
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AE regions").that(aeRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AE region").that(aeRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AE regions").that(aeRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
- assertWithMessage("Wrong number of AWB regions").that(awbRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AWB region").that(awbRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AWB regions").that(awbRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AWB region").that(awbRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
}
}
@@ -345,18 +370,66 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AE regions").that(aeRegions.size).isEqualTo(3)
- assertWithMessage("Wrong AE region").that(aeRegions[0].rect).isEqualTo(M_RECT_1)
- assertWithMessage("Wrong AE region").that(aeRegions[1].rect).isEqualTo(M_RECT_2)
- assertWithMessage("Wrong AE region").that(aeRegions[2].rect).isEqualTo(M_RECT_3)
+ assertWithMessage("Wrong number of AE regions").that(aeRegions?.size).isEqualTo(3)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(1)?.rect).isEqualTo(M_RECT_2)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(2)?.rect).isEqualTo(M_RECT_3)
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(3)
- assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(M_RECT_1)
- assertWithMessage("Wrong AF region").that(afRegions[1].rect).isEqualTo(M_RECT_2)
- assertWithMessage("Wrong AF region").that(afRegions[2].rect).isEqualTo(M_RECT_3)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(3)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(1)?.rect).isEqualTo(M_RECT_2)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(2)?.rect).isEqualTo(M_RECT_3)
- assertWithMessage("Wrong number of AWB regions").that(awbRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AWB region").that(awbRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AWB regions").that(awbRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AWB region").that(awbRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
+ }
+ }
+
+ @Test
+ @Config(maxSdk = 32)
+ fun startFocusAndMetering_AfRegionCorrectedByQuirk() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "Samsung")
+
+ focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_4)
+
+ startFocusMeteringAndAwait(
+ FocusMeteringAction.Builder(point1)
+ .addPoint(point2)
+ .addPoint(point3)
+ .build()
+ )
+
+ // after flipping horizontally, left / right will be swapped.
+ val flippedRect1 = Rect(
+ SENSOR_WIDTH - M_RECT_1.right, M_RECT_1.top,
+ SENSOR_WIDTH - M_RECT_1.left, M_RECT_1.bottom
+ )
+ val flippedRect2 = Rect(
+ SENSOR_WIDTH - M_RECT_2.right, M_RECT_2.top,
+ SENSOR_WIDTH - M_RECT_2.left, M_RECT_2.bottom
+ )
+ val flippedRect3 = Rect(
+ SENSOR_WIDTH - M_RECT_3.right, M_RECT_3.top,
+ SENSOR_WIDTH - M_RECT_3.left, M_RECT_3.bottom
+ )
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong number of AE regions").that(aeRegions?.size).isEqualTo(3)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(1)?.rect).isEqualTo(M_RECT_2)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(2)?.rect).isEqualTo(M_RECT_3)
+
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(3)
+ assertWithMessage("Wrong AF region")
+ .that(afRegions?.get(0)?.rect).isEqualTo(flippedRect1)
+ assertWithMessage("Wrong AF region")
+ .that(afRegions?.get(1)?.rect).isEqualTo(flippedRect2)
+ assertWithMessage("Wrong AF region")
+ .that(afRegions?.get(2)?.rect).isEqualTo(flippedRect3)
+
+ assertWithMessage("Wrong number of AWB regions")
+ .that(awbRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AWB region").that(awbRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
}
}
@@ -375,16 +448,16 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AE regions").that(aeRegions.size).isEqualTo(2)
- assertWithMessage("Wrong AE region").that(aeRegions[0].rect).isEqualTo(M_RECT_2)
- assertWithMessage("Wrong AE region").that(aeRegions[1].rect).isEqualTo(M_RECT_3)
+ assertWithMessage("Wrong number of AE regions").that(aeRegions?.size).isEqualTo(2)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(0)?.rect).isEqualTo(M_RECT_2)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(1)?.rect).isEqualTo(M_RECT_3)
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(2)
- assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(M_RECT_2)
- assertWithMessage("Wrong AF region").that(afRegions[1].rect).isEqualTo(M_RECT_3)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(2)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_2)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(1)?.rect).isEqualTo(M_RECT_3)
- assertWithMessage("Wrong number of AWB regions").that(awbRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AWB region").that(awbRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AWB regions").that(awbRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AWB region").that(awbRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
}
}
@@ -399,14 +472,14 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AE regions").that(aeRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AE region").that(aeRegions[0].rect).isEqualTo(M_RECT_3)
+ assertWithMessage("Wrong number of AE regions").that(aeRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AE region").that(aeRegions?.get(0)?.rect).isEqualTo(M_RECT_3)
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
- assertWithMessage("Wrong number of AWB regions").that(awbRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AWB region").that(awbRegions[0].rect).isEqualTo(M_RECT_2)
+ assertWithMessage("Wrong number of AWB regions").that(awbRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AWB region").that(awbRegions?.get(0)?.rect).isEqualTo(M_RECT_2)
}
}
@@ -439,9 +512,9 @@
cropRect.centerY() + areaHeight / 2
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
assertWithMessage("Wrong AF region")
- .that(afRegions[0].rect).isEqualTo(adjustedRect)
+ .that(afRegions?.get(0)?.rect).isEqualTo(adjustedRect)
}
}
@@ -480,9 +553,9 @@
cropRect.centerY() + areaHeight / 2
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
assertWithMessage("Wrong AF region")
- .that(afRegions[0].rect).isEqualTo(adjustedRect)
+ .that(afRegions?.get(0)?.rect).isEqualTo(adjustedRect)
}
}
@@ -490,8 +563,8 @@
fun previewFovAdjusted_16by9_to_4by3() {
// use 16:9 preview aspect ratio with sensor region of 4:3 (camera 0)
focusMeteringControl = initFocusMeteringControl(
- CAMERA_ID_0,
- setOf(createPreview(Size(1920, 1080))),
+ cameraId = CAMERA_ID_0,
+ useCases = setOf(createPreview(Size(1920, 1080))),
)
startFocusMeteringAndAwait(
@@ -499,9 +572,9 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
assertWithMessage("Wrong AF region")
- .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
+ .that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
}
}
@@ -519,9 +592,9 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
assertWithMessage("Wrong AF region")
- .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_4x3_SENSOR_1920x1080)
+ .that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_PVIEW_RATIO_4x3_SENSOR_1920x1080)
}
}
@@ -535,8 +608,8 @@
val point = factory.createPoint(0f, 0f)
focusMeteringControl = initFocusMeteringControl(
- CAMERA_ID_0,
- setOf(createPreview(Size(640, 480))),
+ cameraId = CAMERA_ID_0,
+ useCases = setOf(createPreview(Size(640, 480))),
)
startFocusMeteringAndAwait(
@@ -544,9 +617,9 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
assertWithMessage("Wrong AF region")
- .that(afRegions[0].rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
+ .that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_PVIEW_RATIO_16x9_SENSOR_640x480)
}
}
@@ -555,7 +628,7 @@
// add 16:9 aspect ratio Preview with sensor region of 4:3 (camera 0), then remove Preview
focusMeteringControl = initFocusMeteringControl(
CAMERA_ID_0,
- setOf(createPreview(Size(1920, 1080))),
+ useCases = setOf(createPreview(Size(1920, 1080))),
)
fakeUseCaseCamera.runningUseCases = emptySet()
focusMeteringControl.onRunningUseCasesChanged()
@@ -569,8 +642,8 @@
// which is the size of SENSOR_1 in this test. So the point is not adjusted,
// and simply M_RECT_1 (metering rectangle of point1 with SENSOR_1) should be used.
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(1)
- assertWithMessage("Wrong AF region").that(afRegions[0].rect).isEqualTo(M_RECT_1)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong AF region").that(afRegions?.get(0)?.rect).isEqualTo(M_RECT_1)
}
}
@@ -587,22 +660,22 @@
)
with(fakeRequestControl.focusMeteringCalls.last()) {
- assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(3)
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(3)
assertWithMessage("Wrong AF region width")
- .that(afRegions[0].rect.width()).isEqualTo((SENSOR_WIDTH * 1.0f).toInt())
+ .that(afRegions?.get(0)?.rect?.width()).isEqualTo((SENSOR_WIDTH * 1.0f).toInt())
assertWithMessage("Wrong AF region height")
- .that(afRegions[0].rect.height()).isEqualTo((SENSOR_HEIGHT * 1.0f).toInt())
+ .that(afRegions?.get(0)?.rect?.height()).isEqualTo((SENSOR_HEIGHT * 1.0f).toInt())
assertWithMessage("Wrong AF region width")
- .that(afRegions[1].rect.width()).isEqualTo((SENSOR_WIDTH * 0.5f).toInt())
+ .that(afRegions?.get(1)?.rect?.width()).isEqualTo((SENSOR_WIDTH * 0.5f).toInt())
assertWithMessage("Wrong AF region height")
- .that(afRegions[1].rect.height()).isEqualTo((SENSOR_HEIGHT * 0.5f).toInt())
+ .that(afRegions?.get(1)?.rect?.height()).isEqualTo((SENSOR_HEIGHT * 0.5f).toInt())
assertWithMessage("Wrong AF region width")
- .that(afRegions[2].rect.width()).isEqualTo((SENSOR_WIDTH * 0.1f).toInt())
+ .that(afRegions?.get(2)?.rect?.width()).isEqualTo((SENSOR_WIDTH * 0.1f).toInt())
assertWithMessage("Wrong AF region height")
- .that(afRegions[2].rect.height()).isEqualTo((SENSOR_HEIGHT * 0.1f).toInt())
+ .that(afRegions?.get(2)?.rect?.height()).isEqualTo((SENSOR_HEIGHT * 0.1f).toInt())
}
}
@@ -791,11 +864,7 @@
).build()
val future = focusMeteringControl.startFocusAndMetering(action)
- assertThrows(ExecutionException::class.java) {
- future[500, TimeUnit.MILLISECONDS]
- }.also {
- assertThat(it.cause).isInstanceOf(IllegalArgumentException::class.java)
- }
+ assertFutureFailedWithIllegalArgumentException(future)
}
@Test
@@ -807,11 +876,7 @@
).build()
val future = focusMeteringControl.startFocusAndMetering(action)
- assertThrows(ExecutionException::class.java) {
- future[500, TimeUnit.MILLISECONDS]
- }.also {
- assertThat(it.cause).isInstanceOf(IllegalArgumentException::class.java)
- }
+ assertFutureFailedWithIllegalArgumentException(future)
}
@Test
@@ -823,11 +888,7 @@
).build()
val future = focusMeteringControl.startFocusAndMetering(action)
- assertThrows(ExecutionException::class.java) {
- future[500, TimeUnit.MILLISECONDS]
- }.also {
- assertThat(it.cause).isInstanceOf(IllegalArgumentException::class.java)
- }
+ assertFutureFailedWithIllegalArgumentException(future)
}
@Test
@@ -870,11 +931,7 @@
.addPoint(invalidPt3, FocusMeteringAction.FLAG_AWB).build()
val future = focusMeteringControl.startFocusAndMetering(action)
- assertThrows(ExecutionException::class.java) {
- future[500, TimeUnit.MILLISECONDS]
- }.also {
- assertThat(it.cause).isInstanceOf(IllegalArgumentException::class.java)
- }
+ assertFutureFailedWithIllegalArgumentException(future)
}
@Test
@@ -1048,10 +1105,10 @@
val action = FocusMeteringAction.Builder(point1, FocusMeteringAction.FLAG_AF).build()
val state3AControl = createState3AControl(CAMERA_ID_0)
focusMeteringControl = initFocusMeteringControl(
- CAMERA_ID_0,
- setOf(createPreview(Size(1920, 1080))),
- fakeUseCaseThreads,
- state3AControl,
+ cameraId = CAMERA_ID_0,
+ useCases = setOf(createPreview(Size(1920, 1080))),
+ useCaseThreads = fakeUseCaseThreads,
+ state3AControl = state3AControl,
)
// Act.
@@ -1074,10 +1131,10 @@
).build()
val state3AControl = createState3AControl(CAMERA_ID_0)
focusMeteringControl = initFocusMeteringControl(
- CAMERA_ID_0,
- setOf(createPreview(Size(1920, 1080))),
- fakeUseCaseThreads,
- state3AControl,
+ cameraId = CAMERA_ID_0,
+ useCases = setOf(createPreview(Size(1920, 1080))),
+ useCaseThreads = fakeUseCaseThreads,
+ state3AControl = state3AControl,
)
// Act.
@@ -1097,10 +1154,10 @@
val action = FocusMeteringAction.Builder(point1, FocusMeteringAction.FLAG_AF).build()
val state3AControl = createState3AControl(CAMERA_ID_0)
focusMeteringControl = initFocusMeteringControl(
- CAMERA_ID_0,
- setOf(createPreview(Size(1920, 1080))),
- fakeUseCaseThreads,
- state3AControl,
+ cameraId = CAMERA_ID_0,
+ useCases = setOf(createPreview(Size(1920, 1080))),
+ useCaseThreads = fakeUseCaseThreads,
+ state3AControl = state3AControl,
)
// Act.
@@ -1167,10 +1224,69 @@
assertFutureFocusCompleted(future, false)
}
+ @Test
+ fun startFocusMetering_noAePoint_aeRegionsSetToDefault() {
+ startFocusMeteringAndAwait(
+ FocusMeteringAction.Builder(
+ point1, FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AWB
+ ).build()
+ )
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong AE regions").that(aeRegions)
+ .isEqualTo(CameraGraph.Constants3A.METERING_REGIONS_DEFAULT.toList())
+ }
+ }
+
+ @Test
+ fun startFocusMetering_noAfPoint_afRegionsSetToDefault() {
+ startFocusMeteringAndAwait(
+ FocusMeteringAction.Builder(
+ point1, FocusMeteringAction.FLAG_AE or FocusMeteringAction.FLAG_AWB
+ ).build()
+ )
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong AF regions").that(afRegions)
+ .isEqualTo(CameraGraph.Constants3A.METERING_REGIONS_DEFAULT.toList())
+ }
+ }
+
+ @Test
+ fun startFocusMetering_noAwbPoint_awbRegionsSetToDefault() {
+ startFocusMeteringAndAwait(
+ FocusMeteringAction.Builder(
+ point1, FocusMeteringAction.FLAG_AE or FocusMeteringAction.FLAG_AF
+ ).build()
+ )
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong AWB regions").that(awbRegions)
+ .isEqualTo(CameraGraph.Constants3A.METERING_REGIONS_DEFAULT.toList())
+ }
+ }
+
+ @Test
+ fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotSet() {
+ // camera 5 supports 1 AF and 0 AE/AWB regions
+ focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_5)
+
+ startFocusMeteringAndAwait(FocusMeteringAction.Builder(
+ point1,
+ FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or
+ FocusMeteringAction.FLAG_AWB
+ ).build())
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong number of AE regions").that(aeRegions).isNull()
+ assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong number of AWB regions").that(awbRegions).isNull()
+ }
+ }
+
// TODO: Port the following tests once their corresponding logics have been implemented.
// - [b/255679866] triggerAfWithTemplate, triggerAePrecaptureWithTemplate,
// cancelAfAeTriggerWithTemplate
- // - startFocusAndMetering_AfRegionCorrectedByQuirk
private fun assertFutureFocusCompleted(
future: ListenableFuture<FocusMeteringResult>,
@@ -1180,6 +1296,14 @@
assertThat(focusMeteringResult.isFocusSuccessful).isEqualTo(isFocused)
}
+ private fun <T> assertFutureFailedWithIllegalArgumentException(future: ListenableFuture<T>) {
+ assertThrows(ExecutionException::class.java) {
+ future[3, TimeUnit.SECONDS]
+ }.apply {
+ assertThat(cause).isInstanceOf(IllegalArgumentException::class.java)
+ }
+ }
+
private fun <T> assertFutureFailedWithOperationCancellation(future: ListenableFuture<T>) {
assertThrows(ExecutionException::class.java) {
future[3, TimeUnit.SECONDS]
@@ -1267,15 +1391,24 @@
state3AControl: State3AControl = createState3AControl(cameraId),
zoomCompat: ZoomCompat = FakeZoomCompat()
) = FocusMeteringControl(
- cameraPropertiesMap[cameraId]!!,
- state3AControl,
- useCaseThreads,
- zoomCompat
- ).apply {
- fakeUseCaseCamera.runningUseCases = useCases
- useCaseCamera = fakeUseCaseCamera
- onRunningUseCasesChanged()
- }
+ cameraPropertiesMap[cameraId]!!,
+ MeteringRegionCorrection.Bindings.provideMeteringRegionCorrection(
+ CameraQuirks(
+ cameraPropertiesMap[cameraId]!!.metadata,
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(cameraPropertiesMap[cameraId]!!.metadata)
+ )
+ )
+ ),
+ state3AControl,
+ useCaseThreads,
+ zoomCompat
+ ).apply {
+ fakeUseCaseCamera.runningUseCases = useCases
+ useCaseCamera = fakeUseCaseCamera
+ onRunningUseCasesChanged()
+ }
private fun initCameraProperties(
cameraIdStr: String,
@@ -1378,12 +1511,34 @@
CAMERA_ID_3,
characteristics3
)
+
+ // **** Camera 4 characteristics (same as Camera 0, but includes LENS_FACING_FRONT) **** //
+ val characteristics4 = characteristics0 + mapOf(
+ CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT
+ )
+
+ cameraPropertiesMap[CAMERA_ID_4] = initCameraProperties(
+ CAMERA_ID_4,
+ characteristics4
+ )
+
+ // **** Camera 5 characteristics (same as Camera 0, but supports AF regions only) **** //
+ val characteristics5 = characteristics0 + mapOf(
+ CameraCharacteristics.CONTROL_MAX_REGIONS_AF to 3,
+ CameraCharacteristics.CONTROL_MAX_REGIONS_AE to 0,
+ CameraCharacteristics.CONTROL_MAX_REGIONS_AWB to 0
+ )
+
+ cameraPropertiesMap[CAMERA_ID_5] = initCameraProperties(
+ CAMERA_ID_5,
+ characteristics5
+ )
}
private fun createPreview(suggestedStreamSpecResolution: Size) =
Preview.Builder()
.setCaptureOptionUnpacker { _, _ -> }
- .setSessionOptionUnpacker() { _, _ -> }
+ .setSessionOptionUnpacker() { _, _, _ -> }
.build().apply {
setSurfaceProvider(
CameraXExecutors.mainThreadExecutor(),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
new file mode 100644
index 0000000..b81db73
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.ImageFormatConstants
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+import org.robolectric.util.ReflectionHelpers
+
+private const val CAMERA_ID_0 = "0"
+
+private const val MOTOROLA_BRAND_NAME = "motorola"
+private const val MOTOROLA_E5_PLAY_MODEL_NAME = "moto e5 play"
+private const val SAMSUNG_BRAND_NAME = "SAMSUNG"
+private const val SAMSUNG_J7_DEVICE_NAME = "J7XELTE"
+
+private val outputSizes = arrayOf(
+ // Samsung J7 API 27 above excluded sizes
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+ Size(3264, 2448),
+ Size(3264, 1836),
+ Size(2048, 1536),
+ Size(2048, 1152),
+ Size(1920, 1080),
+
+ // Add some other sizes
+ Size(1280, 960),
+ Size(1280, 720),
+ Size(640, 480),
+ Size(320, 240),
+)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class OutputSizesCorrectorTest {
+ @Test
+ fun canAddExtraSupportedSizesForMotoE5PlayByFormat() {
+ val outputSizesCorrector = createOutputSizesCorrector(
+ MOTOROLA_BRAND_NAME,
+ null,
+ MOTOROLA_E5_PLAY_MODEL_NAME,
+ CAMERA_ID_0,
+ CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+
+ val resultList = outputSizesCorrector.applyQuirks(
+ arrayOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+ ),
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ )!!.toList()
+
+ Truth.assertThat(resultList).containsExactlyElementsIn(
+ listOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+
+ // Added extra supported sizes for Motorola E5 Play device
+ Size(1920, 1080),
+ Size(1440, 1080),
+ Size(1280, 720),
+ Size(960, 720),
+ Size(864, 480),
+ Size(720, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canAddExtraSupportedSizesForMotoE5PlayByClass() {
+ val outputSizesCorrector = createOutputSizesCorrector(
+ MOTOROLA_BRAND_NAME,
+ null,
+ MOTOROLA_E5_PLAY_MODEL_NAME,
+ CAMERA_ID_0,
+ CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+
+ val resultList = outputSizesCorrector.applyQuirks(
+ arrayOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+ ),
+ SurfaceTexture::class.java
+ )!!.toList()
+
+ Truth.assertThat(resultList).containsExactlyElementsIn(
+ listOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+
+ // Added extra supported sizes for Motorola E5 Play device
+ Size(1920, 1080),
+ Size(1440, 1080),
+ Size(1280, 720),
+ Size(960, 720),
+ Size(864, 480),
+ Size(720, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ @Config(minSdk = 27)
+ fun canExcludeSamsungJ7Api27AboveProblematicSizesByFormat() {
+ val outputSizesCorrector = createOutputSizesCorrector(
+ SAMSUNG_BRAND_NAME,
+ SAMSUNG_J7_DEVICE_NAME,
+ null,
+ CAMERA_ID_0,
+ CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ )
+
+ val sizesWithQuirks: Array<Size> =
+ outputSizesCorrector.applyQuirks(outputSizes, ImageFormat.YUV_420_888)!!
+ val resultList = mutableListOf<Size>().apply {
+ sizesWithQuirks.forEach { size ->
+ add(size)
+ }
+ }
+
+ Truth.assertThat(resultList).containsExactlyElementsIn(
+ listOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+ Size(3264, 2448),
+ Size(3264, 1836),
+
+ // Size(2048, 1536), Size(2048, 1152), Size(1920, 1080) are excluded for YUV format
+
+ Size(1280, 960),
+ Size(1280, 720),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ @Config(minSdk = 27)
+ fun canExcludeSamsungJ7Api27AboveProblematicSizesByClass() {
+ val outputSizesCorrector = createOutputSizesCorrector(
+ SAMSUNG_BRAND_NAME,
+ SAMSUNG_J7_DEVICE_NAME,
+ null,
+ CAMERA_ID_0,
+ CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ )
+
+ val resultList: List<Size> = outputSizesCorrector.applyQuirks(
+ outputSizes,
+ SurfaceTexture::class.java
+ )!!.toList()
+
+ Truth.assertThat(resultList).containsExactlyElementsIn(
+ listOf(
+ // Size(4128, 3096), Size(4128, 2322), Size(3088, 3088), Size(3264, 2448),
+ // Size(3264, 1836), Size(2048, 1536), Size(2048, 1152), Size(1920, 1080)
+ // are excluded for SurfaceTexture class
+
+ Size(1280, 960),
+ Size(1280, 720),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ ).inOrder()
+ }
+
+ private fun createOutputSizesCorrector(
+ brand: String,
+ device: String?,
+ model: String?,
+ cameraId: String,
+ lensFacing: Int,
+ hardwareLevel: Int
+ ): OutputSizesCorrector {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
+ device?.let {
+ ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", it)
+ }
+ model?.let {
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", it)
+ }
+
+ return OutputSizesCorrector(
+ FakeCameraMetadata(
+ cameraId = CameraId(cameraId), characteristics = mapOf(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
+ CameraCharacteristics.LENS_FACING to lensFacing,
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to
+ StreamConfigurationMapBuilder.newBuilder()
+ .apply {
+ outputSizes.forEach { outputSize ->
+ addOutputSize(outputSize)
+ }
+ }.build()
+ )
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
new file mode 100644
index 0000000..f972ee6
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat
+
+import android.graphics.SurfaceTexture
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.ImageFormatConstants
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+/**
+ * Unit tests for [StreamConfigurationMapCompat].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamConfigurationMapCompatTest {
+
+ companion object {
+ private val SIZE_480P = Size(640, 480)
+ private val SIZE_720P = Size(1080, 720)
+ private val SIZE_1080P = Size(1920, 1080)
+ private const val FORMAT_PRIVATE =
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ }
+
+ private lateinit var streamConfigurationMapCompat: StreamConfigurationMapCompat
+ private val privateFormatOutputSizes = listOf(SIZE_1080P, SIZE_720P, SIZE_480P)
+
+ @Before
+ fun setUp() {
+ val builder = StreamConfigurationMapBuilder.newBuilder().apply {
+ privateFormatOutputSizes.forEach { size ->
+ addOutputSize(FORMAT_PRIVATE, size)
+ }
+ }
+ streamConfigurationMapCompat =
+ StreamConfigurationMapCompat(
+ builder.build(),
+ OutputSizesCorrector(FakeCameraMetadata())
+ )
+ }
+
+ @Test
+ fun getOutputSizes_withFormat_callGetOutputSizes() {
+ Truth.assertThat(
+ streamConfigurationMapCompat.getOutputSizes(FORMAT_PRIVATE)?.toList()
+ ).containsExactlyElementsIn(privateFormatOutputSizes)
+ }
+
+ @Test
+ fun getOutputSizes_withClass_callGetOutputSizes() {
+ Truth.assertThat(
+ streamConfigurationMapCompat.getOutputSizes(SurfaceTexture::class.java)?.toList()
+ ).containsExactlyElementsIn(privateFormatOutputSizes)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
new file mode 100644
index 0000000..5f4c468
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.Quirks
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowBuild
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = 21)
+class AfRegionFlipHorizontallyQuirkTest(
+ private val brand: String,
+ private val lensFacing: Int,
+ private val quirkEnablingExpected: Boolean
+) {
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "Brand: {0}, LensFacing = {1}")
+ fun data() = listOf(
+ arrayOf("Samsung", CameraCharacteristics.LENS_FACING_BACK, false),
+ arrayOf("Samsung", CameraCharacteristics.LENS_FACING_FRONT, true),
+ arrayOf("SAMSUNG", CameraCharacteristics.LENS_FACING_FRONT, true),
+ arrayOf("Google", CameraCharacteristics.LENS_FACING_BACK, false),
+ arrayOf("Google", CameraCharacteristics.LENS_FACING_FRONT, false),
+ arrayOf("Moto", CameraCharacteristics.LENS_FACING_BACK, false),
+ )
+ }
+
+ private fun getCameraQuirks(
+ lensFacing: Int
+ ): Quirks {
+ val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+ val shadowCharacteristics = Shadow.extract<ShadowCameraCharacteristics>(characteristics)
+ shadowCharacteristics.set(
+ CameraCharacteristics.LENS_FACING,
+ lensFacing
+ )
+
+ val cameraMetadata = FakeCameraMetadata(
+ characteristics = mapOf(
+ CameraCharacteristics.LENS_FACING to lensFacing
+ )
+ )
+
+ return CameraQuirks(
+ cameraMetadata,
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(cameraMetadata)
+ )
+ ).quirks
+ }
+
+ @Test
+ @Config(maxSdk = 32)
+ fun canEnableQuirkCorrectly() {
+ // Arrange
+ ShadowBuild.setBrand(brand)
+ ShadowBuild.setModel("DO NOT CARE")
+ ShadowBuild.setDevice("DO NOT CARE")
+
+ // Act
+ val cameraQuirks = getCameraQuirks(lensFacing)
+
+ // Verify
+ Truth.assertThat(cameraQuirks.contains(AfRegionFlipHorizontallyQuirk::class.java))
+ .isEqualTo(quirkEnablingExpected)
+ }
+
+ @Test
+ @Config(minSdk = 33)
+ fun canDisableQuirkOnSamsungAPI33() {
+ // Arrange
+ ShadowBuild.setBrand(brand)
+ ShadowBuild.setModel("DO NOT CARE")
+ ShadowBuild.setDevice("DO NOT CARE")
+
+ // Act
+ val cameraQuirks = getCameraQuirks(lensFacing)
+
+ // Verify
+ Truth.assertThat(cameraQuirks.contains(AfRegionFlipHorizontallyQuirk::class.java))
+ .isEqualTo(false)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
new file mode 100644
index 0000000..7b2f9e2
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.testing.EncoderProfilesUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class CamcorderProfileResolutionQuirkTest {
+
+ @Test
+ fun loadByHardwareLevel() {
+ var cameraMetadata =
+ createCameraMetaData(CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+ assertThat(CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata))
+ .isFalse()
+
+ cameraMetadata =
+ createCameraMetaData(CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_3)
+ assertThat(CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata))
+ .isFalse()
+
+ cameraMetadata =
+ createCameraMetaData(CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
+ assertThat(CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata))
+ .isFalse()
+
+ cameraMetadata =
+ createCameraMetaData(CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
+ assertThat(CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata)).isTrue()
+ }
+
+ @Test
+ fun canGetCorrectSupportedSizes() {
+ val cameraMetadata =
+ createCameraMetaData(
+ supportedSizes = arrayOf(
+ EncoderProfilesUtil.RESOLUTION_2160P,
+ EncoderProfilesUtil.RESOLUTION_1080P
+ )
+ )
+ val quirk = CamcorderProfileResolutionQuirk(
+ StreamConfigurationMapCompat(
+ cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!,
+ OutputSizesCorrector(cameraMetadata)
+ )
+ )
+
+ assertThat(quirk.supportedResolutions[0])
+ .isEqualTo(EncoderProfilesUtil.RESOLUTION_2160P)
+ assertThat(quirk.supportedResolutions[1])
+ .isEqualTo(EncoderProfilesUtil.RESOLUTION_1080P)
+ }
+
+ private fun createCameraMetaData(
+ hardwareLevel: Int = CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ supportedSizes: Array<Size> = emptyArray()
+ ): androidx.camera.camera2.pipe.CameraMetadata {
+ val mockMap = Mockito.mock(StreamConfigurationMap::class.java)
+ // Before Android 23, use {@link SurfaceTexture} will finally mapped to 0x22 in
+ // StreamConfigurationMap to retrieve the output sizes information.
+ Mockito.`when`(mockMap.getOutputSizes(ArgumentMatchers.any<Class<SurfaceTexture>>()))
+ .thenReturn(supportedSizes)
+ Mockito.`when`(mockMap.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
+
+ return FakeCameraMetadata(
+ characteristics = mapOf(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
+ CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to mockMap
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
new file mode 100644
index 0000000..bc371e4
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowBuild
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = 21)
+class JpegHalCorruptImageQuirkTest(
+ private val device: String,
+ private val quirkEnablingExpected: Boolean
+) {
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "Brand: {0}")
+ fun data() = listOf(
+ arrayOf("heroqltevzw", true),
+ arrayOf("heroqltetmo", true),
+ arrayOf("k61v1_basic_ref", true),
+ arrayOf("HEROQLTEVZW", true),
+ arrayOf("Google", false),
+ )
+ }
+
+ @Test
+ fun canEnableQuirkCorrectly() {
+ ShadowBuild.setDevice(device)
+
+ val cameraQuirks = CameraQuirks(
+ FakeCameraMetadata(),
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(FakeCameraMetadata())
+ )
+ ).quirks
+
+ assertThat(cameraQuirks.contains(JpegHalCorruptImageQuirk::class.java))
+ .isEqualTo(quirkEnablingExpected)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
new file mode 100644
index 0000000..43d8e7f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.quirk
+
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.internal.compat.quirk.OnePixelShiftQuirk
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowBuild
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = 21)
+class YuvImageOnePixelShiftQuirkTest(
+ private val brand: String,
+ private val model: String,
+ private val quirkEnablingExpected: Boolean
+) {
+ @Test
+ fun canEnableOnePixelShiftQuirkCorrectly() {
+ ShadowBuild.setBrand(brand)
+ ShadowBuild.setModel(model)
+
+ val cameraQuirks = CameraQuirks(
+ FakeCameraMetadata(),
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(FakeCameraMetadata())
+ )
+ ).quirks
+
+ assertThat(cameraQuirks.contains(OnePixelShiftQuirk::class.java))
+ .isEqualTo(quirkEnablingExpected)
+ }
+
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "Brand: {0}, Model: {1}")
+ fun data() = listOf(
+ arrayOf("motorola", "MotoG3", true),
+ arrayOf("samsung", "SM-G532F", true),
+ arrayOf("samsung", "SM-J700F", true),
+ arrayOf("motorola", "MotoG100", false),
+ )
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/FlashAvailabilityCheckerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/FlashAvailabilityCheckerTest.kt
new file mode 100644
index 0000000..60ddf46
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/FlashAvailabilityCheckerTest.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.camera2.pipe.integration.compat.workaround.FlashAvailabilityCheckerTest.TestCameraMetadata.Mode
+import androidx.camera.camera2.pipe.integration.impl.CameraProperties
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
+import com.google.common.truth.Truth
+import java.nio.BufferUnderflowException
+import kotlin.reflect.KClass
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+// @Config() is left out since there currently aren't any API level dependencies in this workaround
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+class FlashAvailabilityCheckerTest(
+ private val manufacturer: String,
+ private val model: String,
+ private val cameraProperties: CameraProperties
+) {
+ @Before
+ fun setup() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", manufacturer)
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
+ }
+
+ @Test
+ fun isFlashAvailable_doesNotThrow_whenRethrowDisabled() {
+ cameraProperties.isFlashAvailable()
+ }
+
+ @Test
+ fun isFlashAvailable_throwsForUnexpectedDevice() {
+ Assume.assumeTrue(Build.MODEL == "unexpected_throwing_device")
+ Assert.assertThrows(BufferUnderflowException::class.java) {
+ cameraProperties.isFlashAvailable(/*rethrowOnError=*/true)
+ }
+ }
+
+ @Test
+ fun isFlashAvailable_returnsFalse_whenFlashAvailableReturnsNull() {
+ Assume.assumeTrue(Build.MODEL == "null_returning_device")
+
+ Truth.assertThat(cameraProperties.isFlashAvailable()).isFalse()
+ }
+
+ private class TestCameraMetadata(
+ private val mode: Mode = Mode.DEFAULT,
+ private val characteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap(),
+ val metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
+ cameraId: CameraId = CameraId("0"),
+ override val keys: Set<CameraCharacteristics.Key<*>> = emptySet(),
+ override val requestKeys: Set<CaptureRequest.Key<*>> = emptySet(),
+ override val resultKeys: Set<CaptureResult.Key<*>> = emptySet(),
+ override val sessionKeys: Set<CaptureRequest.Key<*>> = emptySet(),
+ val physicalMetadata: Map<CameraId, CameraMetadata> = emptyMap(),
+ override val physicalRequestKeys: Set<CaptureRequest.Key<*>> = emptySet(),
+ ) : CameraMetadata {
+ enum class Mode {
+ ALWAYS_NULL,
+ THROW_BUFFER_UNDERFLOW_EXCEPTION,
+ DEFAULT,
+ }
+
+ override fun <T> get(key: CameraCharacteristics.Key<T>): T? {
+ @Suppress("UNCHECKED_CAST")
+ return when (mode) {
+ Mode.ALWAYS_NULL -> null
+ Mode.THROW_BUFFER_UNDERFLOW_EXCEPTION -> throw BufferUnderflowException()
+ else -> characteristics[key] as T?
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> get(key: Metadata.Key<T>): T? = metadata[key] as T?
+
+ override fun <T> getOrDefault(key: CameraCharacteristics.Key<T>, default: T): T =
+ get(key) ?: default
+
+ override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
+
+ override val camera: CameraId = cameraId
+ override val isRedacted: Boolean = false
+
+ override val physicalCameraIds: Set<CameraId> = physicalMetadata.keys
+ override suspend fun getPhysicalMetadata(cameraId: CameraId): CameraMetadata =
+ physicalMetadata[cameraId]!!
+
+ override fun awaitPhysicalMetadata(cameraId: CameraId): CameraMetadata =
+ physicalMetadata[cameraId]!!
+
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
+ }
+
+ companion object {
+ private const val FAKE_OEM = "fake_oem"
+ private val flashAvailabilityTrueProvider = FakeCameraProperties(
+ metadata = TestCameraMetadata(
+ characteristics = mapOf(CameraCharacteristics.FLASH_INFO_AVAILABLE to true)
+ )
+ )
+ private val bufferUnderflowProvider = FakeCameraProperties(
+ metadata = TestCameraMetadata(mode = Mode.THROW_BUFFER_UNDERFLOW_EXCEPTION)
+ )
+ private val flashAvailabilityNullProvider = FakeCameraProperties(
+ metadata = TestCameraMetadata(mode = Mode.ALWAYS_NULL)
+ )
+
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "manufacturer={0}, model={1}")
+ fun data() = mutableListOf<Array<Any?>>().apply {
+ add(arrayOf("sprd", "LEMP", bufferUnderflowProvider))
+ add(arrayOf("sprd", "DM20C", bufferUnderflowProvider))
+ add(arrayOf(FAKE_OEM, "unexpected_throwing_device", bufferUnderflowProvider))
+ add(arrayOf(FAKE_OEM, "not_a_real_device", flashAvailabilityTrueProvider))
+ add(arrayOf(FAKE_OEM, "null_returning_device", flashAvailabilityNullProvider))
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ImageCaptureOptionUnpackerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ImageCaptureOptionUnpackerTest.kt
new file mode 100644
index 0000000..bd8d503
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ImageCaptureOptionUnpackerTest.kt
@@ -0,0 +1,295 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
+import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeviceProperties
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowBuild
+import org.robolectric.util.ReflectionHelpers
+
+// @Config() is left out since there currently aren't any API level dependencies in this workaround
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+class ImageCaptureOptionUnpackerTest {
+
+ private val unpacker = CameraUseCaseAdapter.ImageCaptureOptionUnpacker.INSTANCE
+
+ @Test
+ fun unpackWithoutCaptureMode() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder().useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_2_API26)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(CaptureRequest.CONTROL_ENABLE_ZSL, null)
+ ).isNull()
+ }
+
+ @Test
+ fun unpackWithValidPixel2AndMinLatency() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_2_API26)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isEqualTo(false)
+ }
+
+ @Test
+ fun unpackWithValidPixel2AndMaxQuality() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_2_API26)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isEqualTo(true)
+ }
+
+ @Test
+ fun unpackWithPixel2NotSupportApiLevelAndMinLatency() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_2_NOT_SUPPORT_API)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isNull()
+ }
+
+ @Test
+ fun unpackWithPixel2NotSupportApiLevelAndMaxQuality() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_2_NOT_SUPPORT_API)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isNull()
+ }
+
+ @Test
+ fun unpackWithValidPixel3AndMinLatency() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_3_API26)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isEqualTo(false)
+ }
+
+ @Test
+ fun unpackWithValidPixel3AndMaxQuality() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_3_API26)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isEqualTo(true)
+ }
+
+ @Test
+ fun unpackWithPixel3NotSupportApiLevelAndMinLatency() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_3_NOT_SUPPORT_API)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isNull()
+ }
+
+ @Test
+ fun unpackWithPixel3NotSupportApiLevelAndMaxQuality() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_PIXEL_3_NOT_SUPPORT_API)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isNull()
+ }
+
+ @Test
+ fun unpackWithNotSupportManufacture() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_NOT_GOOGLE)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isNull()
+ }
+
+ @Test
+ fun unpackWithNotSupportModel() {
+ val captureBuilder = CaptureConfig.Builder()
+ val imageCaptureConfig = ImageCapture.Builder()
+ .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .useCaseConfig
+ setDeviceProperty(PROPERTIES_NOT_SUPPORT_MODEL)
+ unpacker.unpack(imageCaptureConfig, captureBuilder)
+ val captureConfig = captureBuilder.build()
+ val camera2Config = Camera2ImplConfig(
+ captureConfig.implementationOptions
+ )
+ assertThat(
+ camera2Config.getCaptureRequestOption<Boolean>(
+ CaptureRequest.CONTROL_ENABLE_ZSL, null
+ )
+ ).isNull()
+ }
+
+ private fun setDeviceProperty(properties: DeviceProperties) {
+ ShadowBuild.setManufacturer(properties.manufacturer())
+ ShadowBuild.setModel(properties.model())
+ ReflectionHelpers.setStaticField(
+ Build.VERSION::class.java,
+ "SDK_INT",
+ properties.sdkVersion()
+ )
+ }
+
+ companion object {
+ private const val MANUFACTURE_GOOGLE = "Google"
+ private const val MANUFACTURE_NOT_GOOGLE = "ANY"
+ private const val MODEL_PIXEL_2 = "Pixel 2"
+ private const val MODEL_PIXEL_3 = "Pixel 3"
+ private const val MODEL_NOT_SUPPORT_HDR = "ANY"
+ private const val API_LEVEL_25 = Build.VERSION_CODES.N_MR1
+ private const val API_LEVEL_26 = Build.VERSION_CODES.O
+ private val PROPERTIES_PIXEL_2_API26 = DeviceProperties.create(
+ MANUFACTURE_GOOGLE,
+ MODEL_PIXEL_2,
+ API_LEVEL_26
+ )
+ private val PROPERTIES_PIXEL_3_API26 = DeviceProperties.create(
+ MANUFACTURE_GOOGLE,
+ MODEL_PIXEL_3,
+ API_LEVEL_26
+ )
+ private val PROPERTIES_PIXEL_2_NOT_SUPPORT_API = DeviceProperties.create(
+ MANUFACTURE_GOOGLE,
+ MODEL_PIXEL_2,
+ API_LEVEL_25
+ )
+ private val PROPERTIES_PIXEL_3_NOT_SUPPORT_API = DeviceProperties.create(
+ MANUFACTURE_GOOGLE,
+ MODEL_PIXEL_3,
+ API_LEVEL_25
+ )
+ private val PROPERTIES_NOT_GOOGLE = DeviceProperties.create(
+ MANUFACTURE_NOT_GOOGLE,
+ MODEL_PIXEL_2,
+ API_LEVEL_26
+ )
+ private val PROPERTIES_NOT_SUPPORT_MODEL = DeviceProperties.create(
+ MANUFACTURE_GOOGLE,
+ MODEL_NOT_SUPPORT_HDR,
+ API_LEVEL_26
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt
new file mode 100644
index 0000000..a04699a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/PreviewPixelHDRnetQuirkTest.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.camera.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import android.util.Size
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
+import androidx.camera.camera2.pipe.integration.impl.Camera2ImplConfig
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.ImageCaptureConfig
+import androidx.camera.core.impl.PreviewConfig
+import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCoordinator
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+// @Config() is left out since there currently aren't any API level dependencies in this workaround
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+class PreviewPixelHDRnetQuirkTest(
+ private val manufacturer: String,
+ private val device: String,
+ private val shouldApplyQuirk: Boolean,
+) {
+
+ @get:Rule
+ val immediateExecutorRule = object : TestWatcher() {
+ override fun starting(description: Description) {
+ super.starting(description)
+ ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
+ override fun executeOnDiskIO(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun postToMainThread(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun isMainThread(): Boolean {
+ return true
+ }
+ })
+ }
+
+ override fun finished(description: Description) {
+ super.finished(description)
+ ArchTaskExecutor.getInstance().setDelegate(null)
+ }
+ }
+
+ private val resolutionHD: Size = Size(1280, 720)
+ private val resolutionVGA: Size = Size(640, 480)
+
+ private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
+
+ @Before
+ fun setup() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", manufacturer)
+ ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", device)
+ }
+
+ @After
+ fun tearDown() {
+ if (this::cameraUseCaseAdapter.isInitialized) {
+ cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
+ }
+ }
+
+ @Test
+ fun previewShouldApplyToneModeForHDRNet() {
+ // Arrange
+ val cameraUseCaseAdapter = configureCameraUseCaseAdapter(
+ resolutionVGA,
+ configType = PreviewConfig::class.java
+ )
+ val preview = Preview.Builder().build()
+
+ // Act. Update UseCase to create SessionConfig
+ cameraUseCaseAdapter.addUseCases(setOf<UseCase>(preview))
+
+ // Assert.
+ if (shouldApplyQuirk) {
+ assertThat(
+ Camera2ImplConfig(
+ preview.sessionConfig.repeatingCaptureConfig.implementationOptions
+ ).getCaptureRequestOption(CaptureRequest.TONEMAP_MODE)
+ ).isEqualTo(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)
+ } else {
+ assertThat(
+ Camera2ImplConfig(
+ preview.sessionConfig.repeatingCaptureConfig.implementationOptions
+ ).getCaptureRequestOption(CaptureRequest.TONEMAP_MODE)
+ ).isNull()
+ }
+ }
+
+ @Test
+ fun otherUseCasesNotApplyHDRNet() {
+ // Arrange
+ cameraUseCaseAdapter = configureCameraUseCaseAdapter(
+ resolutionVGA,
+ configType = ImageCaptureConfig::class.java
+ )
+
+ // Act. Update UseCase to create SessionConfig
+ val imageCapture = ImageCapture.Builder().build()
+ cameraUseCaseAdapter.addUseCases(setOf<UseCase>(imageCapture))
+
+ assertThat(
+ Camera2ImplConfig(
+ imageCapture.sessionConfig.repeatingCaptureConfig.implementationOptions
+ ).getCaptureRequestOption(CaptureRequest.TONEMAP_MODE)
+ ).isNull()
+ }
+
+ @Test
+ fun resolution16x9NotApplyHDRNet() {
+ // Arrange
+ cameraUseCaseAdapter = configureCameraUseCaseAdapter(
+ resolutionHD,
+ configType = PreviewConfig::class.java
+ )
+
+ // Act. Update UseCase to create SessionConfig
+ val preview = Preview.Builder().build()
+ cameraUseCaseAdapter.addUseCases(setOf<UseCase>(preview))
+
+ assertThat(
+ Camera2ImplConfig(
+ preview.sessionConfig.repeatingCaptureConfig.implementationOptions
+ ).getCaptureRequestOption(CaptureRequest.TONEMAP_MODE)
+ ).isNull()
+ }
+
+ private fun configureCameraUseCaseAdapter(
+ resolution: Size,
+ fakeCameraId: String = "0",
+ configType: Class<out UseCaseConfig<*>?>,
+ ): CameraUseCaseAdapter {
+ return CameraUseCaseAdapter(
+ LinkedHashSet<CameraInternal>(setOf(FakeCamera(fakeCameraId))),
+ FakeCameraCoordinator(),
+ FakeCameraDeviceSurfaceManager().apply {
+ setSuggestedStreamSpec(
+ fakeCameraId,
+ configType,
+ StreamSpec.builder(resolution).build()
+ )
+ },
+ androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter(
+ ApplicationProvider.getApplicationContext()
+ )
+ )
+ }
+
+ companion object {
+ private const val FAKE_OEM = "fake_oem"
+
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(
+ name = "manufacturer={0}, device={1}, shouldApplyQuirk={2}"
+ )
+ fun data() = mutableListOf<Array<Any?>>().apply {
+ add(arrayOf("Google", "sunfish", true))
+ add(arrayOf("Google", "barbet", true))
+ add(arrayOf(FAKE_OEM, "barbet", false))
+ add(arrayOf(FAKE_OEM, "not_a_real_device", false))
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index f6a563e..d8a7607 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -166,6 +166,11 @@
extras = emptyMap(),
template = RequestTemplate(CameraDevice.TEMPLATE_STILL_CAPTURE),
)
+ private val fakeCameraProperties = FakeCameraProperties(
+ FakeCameraMetadata(
+ mapOf(CameraCharacteristics.FLASH_INFO_AVAILABLE to true),
+ )
+ )
private var runningRepeatingStream: ScheduledFuture<*>? = null
set(value) {
runningRepeatingStream?.cancel(false)
@@ -178,11 +183,6 @@
@Before
fun setUp() {
val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
- val fakeCameraProperties = FakeCameraProperties(
- FakeCameraMetadata(
- mapOf(CameraCharacteristics.FLASH_INFO_AVAILABLE to true),
- )
- )
torchControl = TorchControl(
fakeCameraProperties,
@@ -204,6 +204,7 @@
torchControl = torchControl,
threads = fakeUseCaseThreads,
requestListener = comboRequestListener,
+ cameraProperties = fakeCameraProperties,
useCaseGraphConfig = UseCaseGraphConfig(
graph = FakeCameraGraph(fakeCameraGraphSession = fakeCameraGraphSession),
surfaceToStreamMap = emptyMap(),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index a33fc33..008fea7 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -282,7 +282,7 @@
private fun createImageCapture(): ImageCapture =
ImageCapture.Builder()
.setCaptureOptionUnpacker { _, _ -> }
- .setSessionOptionUnpacker() { _, _ -> }
+ .setSessionOptionUnpacker() { _, _, _ -> }
.build().also {
it.simulateActivation()
useCaseList.add(it)
@@ -291,7 +291,7 @@
private fun createPreview(): Preview =
Preview.Builder()
.setCaptureOptionUnpacker { _, _ -> }
- .setSessionOptionUnpacker() { _, _ -> }
+ .setSessionOptionUnpacker() { _, _, _ -> }
.build().apply {
setSurfaceProvider(
CameraXExecutors.mainThreadExecutor(),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilterTest.kt
index a3fc194..3b7478a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraCompatibilityFilterTest.kt
@@ -37,6 +37,7 @@
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowCameraCharacteristics
import org.robolectric.shadows.ShadowCameraManager
+import org.robolectric.shadows.StreamConfigurationMapBuilder
import org.robolectric.util.ReflectionHelpers
@RunWith(RobolectricTestRunner::class)
@@ -125,6 +126,11 @@
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
)
+
+ set(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
}
capabilities?.let {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizerTest.kt
index 0014dc6..df10f33 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraSelectionOptimizerTest.kt
@@ -42,6 +42,7 @@
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowCameraCharacteristics
import org.robolectric.shadows.ShadowCameraManager
+import org.robolectric.shadows.StreamConfigurationMapBuilder
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
@@ -240,6 +241,10 @@
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
)
+ set(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
}
// Add the camera to the camera service
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 8887d97..9528c0e 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -25,6 +25,11 @@
import androidx.camera.camera2.pipe.integration.adapter.CameraControlStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
+import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
@@ -92,6 +97,13 @@
val state3AControl = State3AControl(cameraProperties).apply {
useCaseCamera = fakeUseCaseCamera
}
+ val fakeCameraQuirks = CameraQuirks(
+ FakeCameraMetadata(),
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(FakeCameraMetadata())
+ )
+ )
return CameraInfoAdapter(
cameraProperties,
CameraConfig(cameraId),
@@ -104,12 +116,27 @@
CameraCallbackMap(),
FocusMeteringControl(
cameraProperties,
+ MeteringRegionCorrection.Bindings.provideMeteringRegionCorrection(
+ CameraQuirks(
+ cameraProperties.metadata,
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(cameraProperties.metadata)
+ )
+ )
+ ),
state3AControl,
useCaseThreads,
FakeZoomCompat(),
).apply {
useCaseCamera = fakeUseCaseCamera
- }
+ },
+ fakeCameraQuirks,
+ EncoderProfilesProviderAdapter(cameraId.value),
+ StreamConfigurationMapCompat(
+ streamConfigurationMap,
+ OutputSizesCorrector(cameraProperties.metadata)
+ )
)
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 1b62d59..215e70a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -98,9 +98,9 @@
var cancelFocusMeteringResult = CompletableDeferred(Result3A(status = Result3A.Status.OK))
override suspend fun startFocusAndMeteringAsync(
- aeRegions: List<MeteringRectangle>,
- afRegions: List<MeteringRectangle>,
- awbRegions: List<MeteringRectangle>,
+ aeRegions: List<MeteringRectangle>?,
+ afRegions: List<MeteringRectangle>?,
+ awbRegions: List<MeteringRectangle>?,
afTriggerStartAeMode: AeMode?
): Deferred<Result3A> {
focusMeteringCalls.add(
@@ -124,9 +124,9 @@
}
data class FocusMeteringParams(
- val aeRegions: List<MeteringRectangle> = emptyList(),
- val afRegions: List<MeteringRectangle> = emptyList(),
- val awbRegions: List<MeteringRectangle> = emptyList(),
+ val aeRegions: List<MeteringRectangle>? = null,
+ val afRegions: List<MeteringRectangle>? = null,
+ val awbRegions: List<MeteringRectangle>? = null,
val afTriggerStartAeMode: AeMode? = null
)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt
index c7a4604..f43640a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphState3A.kt
@@ -85,9 +85,9 @@
afMode?.let { this.afMode = it }
awbMode?.let { this.awbMode = it }
flashMode?.let { this.flashMode = it }
- aeRegions?.let { this.aeRegions = it }
- afRegions?.let { this.afRegions = it }
- awbRegions?.let { this.awbRegions = it }
+ aeRegions?.let { this.aeRegions = it.ifEmpty { null } }
+ afRegions?.let { this.afRegions = it.ifEmpty { null } }
+ awbRegions?.let { this.awbRegions = it.ifEmpty { null } }
aeLock?.let { this.aeLock = it }
awbLock?.let { this.awbLock = it }
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index a7a5edc..2d941f0 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -1130,7 +1130,8 @@
@NonNull
protected StreamSpec onSuggestedStreamSpecUpdated(
@NonNull StreamSpec suggestedStreamSpec) {
- SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfig);
+ SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfig,
+ suggestedStreamSpec.getResolution());
builder.setTemplateType(mTemplate);
builder.addRepeatingCameraCaptureCallback(mRepeatingCaptureCallback);
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index ba14a0e..65da641 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -452,7 +452,8 @@
}
private void createPipeline(StreamSpec streamSpec) {
- SessionConfig.Builder builder = SessionConfig.Builder.createFrom(getCurrentConfig());
+ SessionConfig.Builder builder = SessionConfig.Builder.createFrom(getCurrentConfig(),
+ streamSpec.getResolution());
builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
if (mDeferrableSurface != null) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
index 12349de..17ca148 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
@@ -16,16 +16,20 @@
package androidx.camera.camera2.internal;
+import android.util.Size;
+
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
+import androidx.camera.camera2.internal.compat.workaround.PreviewPixelHDRnet;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.PreviewConfig;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
@@ -40,7 +44,9 @@
@OptIn(markerClass = ExperimentalCamera2Interop.class)
@Override
- public void unpack(@NonNull UseCaseConfig<?> config,
+ public void unpack(
+ @NonNull Size resolution,
+ @NonNull UseCaseConfig<?> config,
@NonNull final SessionConfig.Builder builder) {
SessionConfig defaultSessionConfig =
config.getDefaultSessionConfig(/*valueIfMissing=*/ null);
@@ -61,6 +67,11 @@
// Set the any additional implementation options
builder.setImplementationOptions(implOptions);
+ // Apply quirks
+ if (config instanceof PreviewConfig) {
+ PreviewPixelHDRnet.setHDRnet(resolution, builder);
+ }
+
// Get Camera2 extended options
final Camera2ImplConfig camera2Config = new Camera2ImplConfig(config);
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 1689d53..fe5cae3 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
@@ -29,7 +29,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.internal.compat.workaround.PreviewPixelHDRnet;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.CaptureMode;
import androidx.camera.core.impl.CaptureConfig;
@@ -79,11 +78,6 @@
break;
}
- if (captureType == CaptureType.PREVIEW) {
- // Set the WYSIWYG preview for CAPTURE_TYPE_PREVIEW
- PreviewPixelHDRnet.setHDRnet(sessionBuilder);
- }
-
mutableConfig.insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionBuilder.build());
mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
index b323a73..86f8333 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
@@ -80,7 +80,8 @@
meteringSurfaceSize.getHeight());
Surface surface = new Surface(surfaceTexture);
- SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfigWithDefaults);
+ SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfigWithDefaults,
+ meteringSurfaceSize);
builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
mDeferrableSurface = new ImmediateSurface(surface);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 6c25d96..93b1c56 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -17,6 +17,7 @@
package androidx.camera.camera2.internal;
import static android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT;
+import static android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;
@@ -204,6 +205,164 @@
}
/**
+ *
+ * @param range
+ * @return the length of the range
+ */
+ private static int getRangeLength(Range<Integer> range) {
+ return (range.getUpper() - range.getLower()) + 1;
+ }
+
+ /**
+ * @return the distance between the nearest limits of two non-intersecting ranges
+ */
+ private static int getRangeDistance(Range<Integer> firstRange, Range<Integer> secondRange) {
+ Preconditions.checkState(
+ !firstRange.contains(secondRange.getUpper())
+ && !firstRange.contains(secondRange.getLower()),
+ "Ranges must not intersect");
+ if (firstRange.getLower() > secondRange.getUpper()) {
+ return firstRange.getLower() - secondRange.getUpper();
+ } else {
+ return secondRange.getLower() - firstRange.getUpper();
+ }
+ }
+
+ /**
+ * @param targetFps the target frame rate range used while comparing to device-supported ranges
+ * @param storedRange the device-supported range that is currently saved and intersects with
+ * targetFps
+ * @param newRange a new potential device-supported range that intersects with targetFps
+ * @return the device-supported range that better matches the target fps
+ */
+ private static Range<Integer> compareIntersectingRanges(Range<Integer> targetFps,
+ Range<Integer> storedRange, Range<Integer> newRange) {
+ // TODO(b/272075984): some ranges may may have a larger intersection but may also have an
+ // excessively large portion that is non-intersecting. Will want to do further
+ // investigation to find a more optimized way to decide when a potential range has too
+ // much non-intersecting value and discard it
+
+ double storedIntersectionsize = getRangeLength(storedRange.intersect(targetFps));
+ double newIntersectionSize = getRangeLength(newRange.intersect(targetFps));
+
+ double newRangeRatio = newIntersectionSize / getRangeLength(newRange);
+ double storedRangeRatio = storedIntersectionsize / getRangeLength(storedRange);
+
+ if (newIntersectionSize > storedIntersectionsize) {
+ // if new, the new range must have at least 50% of its range intersecting, OR has a
+ // larger percentage of intersection than the previous stored range
+ if (newRangeRatio >= .5 || newRangeRatio >= storedRangeRatio) {
+ return newRange;
+ }
+ } else if (newIntersectionSize == storedIntersectionsize) {
+ // if intersecting ranges have same length... pick the one that has the higher
+ // intersection ratio
+ if (newRangeRatio > storedRangeRatio) {
+ return newRange;
+ } else if (newRangeRatio == storedRangeRatio
+ && newRange.getLower() > storedRange.getLower()) {
+ // if equal intersection size AND ratios pick the higher range
+ return newRange;
+ }
+
+ } else if (storedRangeRatio < .5
+ && newRangeRatio > storedRangeRatio) {
+ // if the new one has a smaller range... only change if existing has an intersection
+ // ratio < 50% and the new one has an intersection ratio > than the existing one
+ return newRange;
+ }
+ return storedRange;
+ }
+
+ /**
+ * Finds a frame rate range supported by the device that is closest to the target framerate
+ *
+ * @param targetFrameRate the Target Frame Rate resolved from all current existing surfaces
+ * and incoming new use cases
+ * @return a frame rate range supported by the device that is closest to targetFrameRate
+ */
+
+ @NonNull
+ private Range<Integer> getClosestSupportedDeviceFrameRate(Range<Integer> targetFrameRate,
+ int maxFps) {
+ if (targetFrameRate == null) {
+ return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
+ }
+
+ // get all fps ranges supported by device
+ Range<Integer>[] availableFpsRanges =
+ mCharacteristics.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
+
+ if (availableFpsRanges == null) {
+ return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
+ }
+ // if whole target framerate range > maxFps of configuration, the target for this
+ // calculation will be [max,max].
+
+ // if the range is partially larger than maxFps, the target for this calculation will be
+ // [target.lower, max] for the sake of this calculation
+ targetFrameRate = new Range<>(
+ Math.min(targetFrameRate.getLower(), maxFps),
+ Math.min(targetFrameRate.getUpper(), maxFps)
+ );
+
+ Range<Integer> bestRange = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
+ int currentIntersectSize = 0;
+
+
+ for (Range<Integer> potentialRange : availableFpsRanges) {
+ // ignore ranges completely larger than configuration's maximum fps
+ if (maxFps >= potentialRange.getLower()) {
+ if (bestRange.equals(StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED)) {
+ bestRange = potentialRange;
+ }
+ // take if range is a perfect match
+ if (potentialRange.equals(targetFrameRate)) {
+ bestRange = potentialRange;
+ break;
+ }
+
+ try {
+ // bias towards a range that intersects on the upper end
+ Range<Integer> newIntersection = potentialRange.intersect(targetFrameRate);
+ int newIntersectSize = getRangeLength(newIntersection);
+ // if this range intersects our target + no other range was already
+ if (currentIntersectSize == 0) {
+ bestRange = potentialRange;
+ currentIntersectSize = newIntersectSize;
+ } else if (newIntersectSize >= currentIntersectSize) {
+ // if the currently stored range + new range both intersect, check to see
+ // which one should be picked over the other
+ bestRange = compareIntersectingRanges(targetFrameRate, bestRange,
+ potentialRange);
+ currentIntersectSize = getRangeLength(targetFrameRate.intersect(bestRange));
+ }
+ } catch (IllegalArgumentException e) {
+ // if no intersection is present, pick the range that is closer to our target
+ if (currentIntersectSize == 0) {
+ if (getRangeDistance(potentialRange, targetFrameRate)
+ < getRangeDistance(bestRange, targetFrameRate)) {
+ bestRange = potentialRange;
+ } else if (getRangeDistance(potentialRange, targetFrameRate)
+ == getRangeDistance(bestRange, targetFrameRate)) {
+ if (potentialRange.getLower() > bestRange.getUpper()) {
+ // if they both have the same distance, pick the higher range
+ bestRange = potentialRange;
+ } else if (getRangeLength(potentialRange) < getRangeLength(bestRange)) {
+ // if one isn't higher than the other, pick the range with the
+ // shorter length
+ bestRange = potentialRange;
+ }
+ }
+ }
+ }
+ }
+
+ }
+ return bestRange;
+ }
+
+ /**
* @param newTargetFramerate an incoming framerate range
* @param storedTargetFramerate a stored framerate range to be modified
* @return adjusted target frame rate
@@ -405,12 +564,22 @@
// Map the saved supported SurfaceConfig combination
if (savedSizes != null) {
+ Range<Integer> targetFramerateForDevice = null;
+ if (targetFramerateForConfig != null) {
+ targetFramerateForDevice =
+ getClosestSupportedDeviceFrameRate(targetFramerateForConfig,
+ savedConfigMaxFps);
+ }
suggestedStreamSpecMap = new HashMap<>();
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
suggestedStreamSpecMap.put(
useCaseConfig,
- StreamSpec.builder(savedSizes.get(useCasesPriorityOrder.indexOf(
- newUseCaseConfigs.indexOf(useCaseConfig)))).build());
+ targetFramerateForDevice != null
+ ? StreamSpec.builder(savedSizes.get(useCasesPriorityOrder.indexOf(
+ newUseCaseConfigs.indexOf(useCaseConfig))))
+ .setExpectedFrameRateRange(targetFramerateForDevice).build()
+ : StreamSpec.builder(savedSizes.get(useCasesPriorityOrder.indexOf(
+ newUseCaseConfigs.indexOf(useCaseConfig)))).build());
}
} else {
throw new IllegalArgumentException(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
index 23716af..d25a899 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
@@ -23,7 +23,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-@RequiresApi(21)
+@RequiresApi(23)
class StreamConfigurationMapCompatApi23Impl extends StreamConfigurationMapCompatBaseImpl {
StreamConfigurationMapCompatApi23Impl(@NonNull StreamConfigurationMap map) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/PreviewPixelHDRnetQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/PreviewPixelHDRnetQuirk.java
index 3c5827a..1cf284e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/PreviewPixelHDRnetQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/PreviewPixelHDRnetQuirk.java
@@ -35,6 +35,8 @@
* image quality than the viewfinder output. To align the viewfinder quality
* with the final photo, we need to set TONEMAP_MODE to HIGH_QUALITY (the
* default is FAST) on the viewfinder stream to enable the WYSIWYG preview.
+ * This quirk will not be applied when preview resolution is 16:9, more details in
+ * b/266459202.
* Device(s): Pixel 4a, Pixel 4a (5G), Pixel 5, Pixel 5a (5G)
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/PreviewPixelHDRnet.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/PreviewPixelHDRnet.java
index 0465d6d..7615d3d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/PreviewPixelHDRnet.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/PreviewPixelHDRnet.java
@@ -17,6 +17,8 @@
package androidx.camera.camera2.internal.compat.workaround;
import android.hardware.camera2.CaptureRequest;
+import android.util.Rational;
+import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
@@ -35,6 +37,8 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class PreviewPixelHDRnet {
+ public static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
+
private PreviewPixelHDRnet() {
}
@@ -42,15 +46,27 @@
* Turns on WYSIWYG viewfinder on Pixel devices
*/
@OptIn(markerClass = ExperimentalCamera2Interop.class)
- public static void setHDRnet(@NonNull SessionConfig.Builder sessionBuilder) {
+ public static void setHDRnet(
+ @NonNull Size resolution,
+ @NonNull SessionConfig.Builder sessionBuilder) {
final PreviewPixelHDRnetQuirk quirk = DeviceQuirks.get(PreviewPixelHDRnetQuirk.class);
if (quirk == null) {
return;
}
+ if (isAspectRatioMatch(resolution, ASPECT_RATIO_16_9)) {
+ return;
+ }
+
Camera2ImplConfig.Builder camera2ConfigBuilder = new Camera2ImplConfig.Builder();
camera2ConfigBuilder.setCaptureRequestOption(CaptureRequest.TONEMAP_MODE,
CaptureRequest.TONEMAP_MODE_HIGH_QUALITY);
sessionBuilder.addImplementationOptions(camera2ConfigBuilder.build());
}
+
+ private static boolean isAspectRatioMatch(
+ @NonNull Size resolution,
+ @NonNull Rational aspectRatio) {
+ return aspectRatio.equals(new Rational(resolution.getWidth(), resolution.getHeight()));
+ }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
index 50bb91d..bc4a710 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
@@ -26,6 +26,7 @@
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
+import android.util.Size;
import androidx.annotation.OptIn;
import androidx.camera.camera2.impl.Camera2ImplConfig;
@@ -33,10 +34,12 @@
import androidx.camera.camera2.interop.Camera2Interop;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.Preview;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.Config.OptionPriority;
import androidx.camera.core.impl.ImageCaptureConfig;
+import androidx.camera.core.impl.PreviewConfig;
import androidx.camera.core.impl.SessionConfig;
import org.junit.Before;
@@ -44,6 +47,7 @@
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@@ -51,6 +55,9 @@
instrumentedPackages = { "androidx.camera.camera2.impl" })
public final class Camera2SessionOptionUnpackerTest {
+ private static final Size RESOLUTION_HD = new Size(1280, 720);
+ private static final Size RESOLUTION_VGA = new Size(640, 480);
+
private Camera2SessionOptionUnpacker mUnpacker;
@Before
@@ -77,7 +84,7 @@
.setCameraEventCallback(cameraEventCallbacks);
SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
- mUnpacker.unpack(imageCaptureBuilder.getUseCaseConfig(), sessionBuilder);
+ mUnpacker.unpack(RESOLUTION_VGA, imageCaptureBuilder.getUseCaseConfig(), sessionBuilder);
SessionConfig sessionConfig = sessionBuilder.build();
CameraCaptureCallback interopCallback =
@@ -116,7 +123,7 @@
CaptureRequest.FLASH_MODE);
SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
- mUnpacker.unpack(useCaseConfig, sessionBuilder);
+ mUnpacker.unpack(RESOLUTION_VGA, useCaseConfig, sessionBuilder);
SessionConfig sessionConfig = sessionBuilder.build();
Camera2ImplConfig config = new Camera2ImplConfig(sessionConfig.getImplementationOptions());
@@ -133,6 +140,45 @@
.isEqualTo(priorityAfMode);
assertThat(getCaptureRequestOptionPriority(config, CaptureRequest.CONTROL_AF_MODE))
.isEqualTo(priorityFlashMode);
+
+ assertThat(config.getCaptureRequestOption(CaptureRequest.TONEMAP_MODE)).isNull();
+ }
+
+ @Test
+ public void unpackerExtractsOptionsForPreviewResolution16x9() {
+ ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+ ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+ Preview.Builder previewConfigBuilder = new Preview.Builder();
+
+ PreviewConfig useCaseConfig = previewConfigBuilder.getUseCaseConfig();
+
+ SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+ mUnpacker.unpack(RESOLUTION_HD, useCaseConfig, sessionBuilder);
+ SessionConfig sessionConfig = sessionBuilder.build();
+
+ Camera2ImplConfig config = new Camera2ImplConfig(sessionConfig.getImplementationOptions());
+
+ assertThat(config.getCaptureRequestOption(CaptureRequest.TONEMAP_MODE)).isNull();
+ }
+
+ @Test
+ public void unpackerExtractsOptionsForPreviewResolution4x3() {
+ ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "Google");
+ ReflectionHelpers.setStaticField(Build.class, "DEVICE", "sunfish");
+
+ Preview.Builder previewConfigBuilder = new Preview.Builder();
+
+ PreviewConfig useCaseConfig = previewConfigBuilder.getUseCaseConfig();
+
+ SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
+ mUnpacker.unpack(RESOLUTION_VGA, useCaseConfig, sessionBuilder);
+ SessionConfig sessionConfig = sessionBuilder.build();
+
+ Camera2ImplConfig config = new Camera2ImplConfig(sessionConfig.getImplementationOptions());
+
+ assertThat(config.getCaptureRequestOption(CaptureRequest.TONEMAP_MODE))
+ .isEqualTo(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY);
}
private OptionPriority getCaptureRequestOptionPriority(Config config,
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index d81d6890..fad6f14 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -43,6 +43,7 @@
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraDeviceSurfaceManager
import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceConfig.ConfigSize
@@ -1332,7 +1333,8 @@
attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
capabilities: IntArray? = null,
- compareWithAtMost: Boolean = false
+ compareWithAtMost: Boolean = false,
+ compareExpectedFps: Range<Int>? = null
) {
setupCameraAndInitCameraX(
hardwareLevel = hardwareLevel,
@@ -1359,6 +1361,11 @@
} else {
assertThat(sizeIsAtMost(resultSize, expectedSize)).isTrue()
}
+
+ if (compareExpectedFps != null) {
+ assertThat(suggestedStreamSpecs[useCaseConfigMap[it]]!!.expectedFrameRateRange
+ == compareExpectedFps)
+ }
}
}
@@ -1383,14 +1390,6 @@
return resultMap
}
- /**
- * Helper function that returns whether size is <= maxSize
- *
- */
- private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
- return (size.height * size.width) <= (maxSize.height * maxSize.width)
- }
-
// //////////////////////////////////////////////////////////////////////////////////////////
//
// Resolution selection tests for FPS settings
@@ -1411,7 +1410,7 @@
}
@Test
- fun getSupportedOutputSizes_single_invalid_targetFPS() {
+ fun getSuggestedStreamSpec_single_invalid_targetFPS() {
// an invalid target means the device would neve be able to reach that fps
val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(65, 70))
val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
@@ -1424,7 +1423,7 @@
}
@Test
- fun getSupportedOutputSizes_multiple_targetFPS_first_is_larger() {
+ fun getSuggestedStreamSpec_multiple_targetFPS_first_is_larger() {
// a valid target means the device is capable of that fps
val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(15, 25))
@@ -1441,7 +1440,7 @@
}
@Test
- fun getSupportedOutputSizes_multiple_targetFPS_first_is_smaller() {
+ fun getSuggestedStreamSpec_multiple_targetFPS_first_is_smaller() {
// a valid target means the device is capable of that fps
val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(45, 50))
@@ -1458,7 +1457,7 @@
}
@Test
- fun getSupportedOutputSizes_multiple_targetFPS_intersect() {
+ fun getSuggestedStreamSpec_multiple_targetFPS_intersect() {
// first and second new use cases have target fps that intersect each other
val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 40))
val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(35, 45))
@@ -1476,7 +1475,7 @@
}
@Test
- fun getSupportedOutputSizes_multiple_cases_first_has_targetFPS() {
+ fun getSuggestedStreamSpec_multiple_cases_first_has_targetFPS() {
// first new use case has a target fps, second new use case does not
val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
val useCase2 = createUseCase(CaptureType.PREVIEW)
@@ -1493,7 +1492,7 @@
}
@Test
- fun getSupportedOutputSizes_multiple_cases_second_has_targetFPS() {
+ fun getSuggestedStreamSpec_multiple_cases_second_has_targetFPS() {
// second new use case does not have a target fps, first new use case does not
val useCase1 = createUseCase(CaptureType.PREVIEW)
val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
@@ -1510,7 +1509,7 @@
}
@Test
- fun getSupportedOutputSizes_attached_with_targetFPS_no_new_targetFPS() {
+ fun getSuggestedStreamSpec_attached_with_targetFPS_no_new_targetFPS() {
// existing surface with target fps + new use case without a target fps
val useCase = createUseCase(CaptureType.PREVIEW)
val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
@@ -1534,7 +1533,7 @@
}
@Test
- fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
+ fun getSuggestedStreamSpec_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
// existing surface with target fps + new use case with target fps that does not intersect
val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
@@ -1558,7 +1557,7 @@
}
@Test
- fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
+ fun getSuggestedStreamSpec_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
// existing surface with target fps + new use case with target fps that intersect each other
val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(45, 50))
val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
@@ -1581,6 +1580,154 @@
)
}
+ @Test
+ fun getSuggestedStreamSpec_has_device_supported_expectedFrameRateRange() {
+ // use case with target fps
+ val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(15, 25))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(4032, 3024))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true,
+ compareExpectedFps = Range(10, 22)
+ )
+ // expected fps 10,22 because it has the largest intersection
+ }
+
+ @Test
+ fun getSuggestedStreamSpec_has_exact_device_supported_expectedFrameRateRange() {
+ // use case with target fps
+ val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 40))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(1920, 1440))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true,
+ compareExpectedFps = Range(30, 40)
+ )
+ // expected fps 30,40 because it is an exact intersection
+ }
+
+ @Test
+ fun getSuggestedStreamSpec_has_no_device_supported_expectedFrameRateRange() {
+ // use case with target fps
+ val useCase1 = createUseCase(
+ CaptureType.PREVIEW,
+ targetFrameRate = Range<Int>(65, 65)
+ )
+
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(800, 450))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true,
+ compareExpectedFps = Range(60, 60)
+ )
+ // expected fps 60,60 because it is the closest range available
+ }
+
+ @Test
+ fun getSuggestedStreamSpec_has_multiple_device_supported_expectedFrameRateRange() {
+
+ // use case with target fps
+ val useCase1 = createUseCase(
+ CaptureType.PREVIEW,
+ targetFrameRate = Range<Int>(36, 45)
+ )
+
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(1280, 960))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true,
+ compareExpectedFps = Range(30, 40)
+ )
+ // expected size will give a maximum of 40 fps
+ // expected range 30,40. another range with the same intersection size was 30,50, but 30,40
+ // was selected instead because its range has a larger ratio of intersecting value vs
+ // non-intersecting
+ }
+
+ @Test
+ fun getSuggestedStreamSpec_has_no_device_intersection_expectedFrameRateRange() {
+ // target fps is between ranges, but within device capability (for some reason lol)
+
+ // use case with target fps
+ val useCase1 = createUseCase(
+ CaptureType.PREVIEW,
+ targetFrameRate = Range<Int>(26, 27)
+ )
+
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(1920, 1440))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true,
+ compareExpectedFps = Range(30, 30)
+ )
+ // 30,30 was expected because it is the closest and shortest range to our target fps
+ }
+
+ @Test
+ fun getSuggestedStreamSpec_has_no_device_intersection_equidistant_expectedFrameRateRange() {
+
+ // use case with target fps
+ val useCase1 = createUseCase(
+ CaptureType.PREVIEW,
+ targetFrameRate = Range<Int>(26, 26)
+ )
+
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(1920, 1440))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true,
+ compareExpectedFps = Range(30, 30)
+ )
+ // 30,30 selected because although there are other ranges that have the same distance to
+ // the target, 30,30 is the shortest range that also happens to be on the upper side of the
+ // target range
+ }
+
+ @Test
+ fun getSuggestedStreamSpec_has_no_expectedFrameRateRange() {
+ // a valid target means the device is capable of that fps
+
+ // use case with no target fps
+ val useCase1 = createUseCase(CaptureType.PREVIEW)
+
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase1, Size(4032, 3024))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareExpectedFps = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+ )
+ // since no target fps present, no specific device fps will be selected, and is set to
+ // unspecified: (0,0)
+ }
+
+ /**
+ * Helper function that returns whether size is <= maxSize
+ *
+ */
+ private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
+ return (size.height * size.width) <= (maxSize.height * maxSize.width)
+ }
+
// //////////////////////////////////////////////////////////////////////////////////////////
//
// Other tests
@@ -1751,13 +1898,13 @@
ArgumentMatchers.anyInt(),
ArgumentMatchers.eq(Size(1920, 1440))
))
- .thenReturn(30000000L) // 30
+ .thenReturn(33333333L) // 30
Mockito.`when`(it.getOutputMinFrameDuration(
ArgumentMatchers.anyInt(),
ArgumentMatchers.eq(Size(1920, 1080))
))
- .thenReturn(28000000L) // 35
+ .thenReturn(28571428L) // 35
Mockito.`when`(it.getOutputMinFrameDuration(
ArgumentMatchers.anyInt(),
@@ -1769,7 +1916,7 @@
ArgumentMatchers.anyInt(),
ArgumentMatchers.eq(Size(1280, 720))
))
- .thenReturn(22000000L) // 45, size preview/display
+ .thenReturn(22222222L) // 45, size preview/display
Mockito.`when`(it.getOutputMinFrameDuration(
ArgumentMatchers.anyInt(),
@@ -1781,13 +1928,13 @@
ArgumentMatchers.anyInt(),
ArgumentMatchers.eq(Size(800, 450))
))
- .thenReturn(16666000L) // 60fps
+ .thenReturn(16666666L) // 60fps
Mockito.`when`(it.getOutputMinFrameDuration(
ArgumentMatchers.anyInt(),
ArgumentMatchers.eq(Size(640, 480))
))
- .thenReturn(16666000L) // 60fps
+ .thenReturn(16666666L) // 60fps
// Sets up the supported high resolution sizes
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -1796,6 +1943,16 @@
}
}
+ val deviceFPSRanges: Array<Range<Int>?> = arrayOf(
+ Range(10, 22),
+ Range(22, 22),
+ Range(30, 30),
+ Range(30, 50),
+ Range(30, 40),
+ Range(30, 60),
+ Range(50, 60),
+ Range(60, 60))
+
val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
@@ -1803,6 +1960,8 @@
set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
+ set(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, deviceFPSRanges)
+
capabilities?.let {
set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 85809c2c..a385c37 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -540,7 +540,7 @@
ImageCapture imageCapture = new ImageCapture.Builder()
.setCaptureOptionUnpacker((config, builder) -> {
})
- .setSessionOptionUnpacker((config, builder) -> {
+ .setSessionOptionUnpacker((resolution, config, builder) -> {
}).build();
imageCapture.setCropAspectRatio(new Rational(16, 9));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
index b6d5748..cd27148 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
@@ -97,7 +97,6 @@
/**
* Bitmask options for the effect targets.
*
- * @hide
*/
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -227,7 +226,6 @@
* <p>Throws {@link IllegalArgumentException} if the effect does not contain a
* {@link SurfaceProcessor}.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java
index 79e1c4a..d790a43 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java
@@ -70,7 +70,6 @@
*
* @return list of combinations of {@link CameraInfo}.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
@@ -81,7 +80,6 @@
*
* @return true if concurrent mode is enabled, otherwise false.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
boolean isConcurrentCameraModeOn();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 506de73..8456825 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -358,7 +358,8 @@
imageReaderProxy.setOnImageAvailableListener(mImageAnalysisAbstractAnalyzer,
backgroundExecutor);
- SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
+ streamSpec.getResolution());
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
@@ -1241,7 +1242,6 @@
/**
* setMirrorMode is not supported on ImageAnalysis.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index ee6811d..f670f0d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -373,7 +373,8 @@
if (isNodeEnabled()) {
return createPipelineWithNode(cameraId, config, streamSpec);
}
- SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
+ streamSpec.getResolution());
if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
getCameraControl().addZslConfig(sessionConfigBuilder);
@@ -1660,7 +1661,8 @@
}
mTakePictureManager.setImagePipeline(mImagePipeline);
- SessionConfig.Builder sessionConfigBuilder = mImagePipeline.createSessionConfigBuilder();
+ SessionConfig.Builder sessionConfigBuilder =
+ mImagePipeline.createSessionConfigBuilder(streamSpec.getResolution());
if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
getCameraControl().addZslConfig(sessionConfigBuilder);
}
@@ -2729,7 +2731,6 @@
/**
* setMirrorMode is not supported on ImageCapture.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
index a4ca7f7..72e2efa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
@@ -27,7 +27,6 @@
/**
* The mirror mode.
*
- * @hide
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -48,7 +47,6 @@
}
/**
- * @hide
*/
@IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_FRONT_ON})
@Retention(RetentionPolicy.SOURCE)
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 35eff5f..862dce9 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
@@ -216,7 +216,8 @@
}
checkMainThread();
- SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
+ streamSpec.getResolution());
// Close previous session's deferrable surface before creating new one
clearPipeline();
@@ -285,7 +286,8 @@
}
// Send the camera Surface to the camera2.
- SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
+ streamSpec.getResolution());
addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
return sessionConfigBuilder;
}
@@ -645,7 +647,6 @@
/**
* @inheritDoc
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -959,7 +960,6 @@
/**
* setMirrorMode is not supported on Preview.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index b83f3e2..be20322 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -68,7 +68,6 @@
/**
* This field indicates the format of the {@link Surface}.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@CameraEffect.Formats
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 86465a5..de9e3bc 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
@@ -325,7 +325,6 @@
*
* <p>If mirror mode is not set, defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@MirrorMode.Mirror
@@ -336,7 +335,6 @@
/**
* Returns if the mirroring is required with the associated camera.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public boolean isMirroringRequired(@NonNull CameraInternal camera) {
@@ -378,7 +376,6 @@
/**
* Gets the relative rotation degrees given whether the output should be mirrored.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@IntRange(from = 0, to = 359)
@@ -585,7 +582,6 @@
/**
* Offers suggested stream specification for the UseCase.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public void updateSuggestedStreamSpec(@NonNull StreamSpec suggestedStreamSpec) {
@@ -791,7 +787,6 @@
* Sets the {@link CameraEffect} associated with this use case.
*
* @throws IllegalArgumentException if the effect targets are not supported by this use case.
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public void setEffect(@Nullable CameraEffect effect) {
@@ -802,7 +797,6 @@
/**
* Gets the {@link CameraEffect} associated with this use case.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
@@ -915,7 +909,6 @@
* <p>The method returns an empty set if this {@link UseCase} does not support effects. By
* default, this method returns an empty set.
*
- * @hide
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -926,7 +919,6 @@
/**
* Returns whether the targets can be applied to this {@link UseCase} or one of its ancestors.
*
- * @hide
* @see #getSupportedEffectTargets()
*/
@RestrictTo(Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
index 7b37c34..d9a231e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
@@ -38,7 +38,6 @@
* All camera devices intended to be operated concurrently, must be opened before configuring
* sessions on any of the camera devices.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(21)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
index 7f18d0f..fc8a4af 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
@@ -118,8 +118,9 @@
* Creates a {@link SessionConfig.Builder} for configuring camera.
*/
@NonNull
- public SessionConfig.Builder createSessionConfigBuilder() {
- SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mUseCaseConfig);
+ public SessionConfig.Builder createSessionConfigBuilder(@NonNull Size resolution) {
+ SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mUseCaseConfig,
+ resolution);
builder.addNonRepeatingSurface(mPipelineIn.getSurface());
return builder;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
index 2809cde..8662d08 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
@@ -22,6 +22,7 @@
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.InputConfiguration;
import android.util.Range;
+import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -318,10 +319,14 @@
/**
* Apply the options from the config onto the builder
*
+ * @param resolution the suggested resolution
* @param config the set of options to apply
* @param builder the builder on which to apply the options
*/
- void unpack(@NonNull UseCaseConfig<?> config, @NonNull SessionConfig.Builder builder);
+ void unpack(
+ @NonNull Size resolution,
+ @NonNull UseCaseConfig<?> config,
+ @NonNull SessionConfig.Builder builder);
}
/**
@@ -349,7 +354,9 @@
* <p>Populates the builder with all the properties defined in the base configuration.
*/
@NonNull
- public static Builder createFrom(@NonNull UseCaseConfig<?> config) {
+ public static Builder createFrom(
+ @NonNull UseCaseConfig<?> config,
+ @NonNull Size resolution) {
OptionUnpacker unpacker = config.getSessionOptionUnpacker(null);
if (unpacker == null) {
throw new IllegalStateException(
@@ -360,7 +367,7 @@
Builder builder = new Builder();
// Unpack the configuration into this builder
- unpacker.unpack(config, builder);
+ unpacker.unpack(resolution, config, builder);
return builder;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 784a21f..3556349 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -18,7 +18,6 @@
import static androidx.camera.core.CameraEffect.PREVIEW;
import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
-import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
import static androidx.camera.core.processing.TargetUtils.getNumberOfTargets;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
@@ -551,14 +550,12 @@
suggestedStreamSpecs.put(useCase, useCase.getAttachedStreamSpec());
}
- Rect sensorRect = ((CameraControlInternal) getCameraControl()).getSensorRect();
- SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
- (CameraInfoInternal) getCameraInfo(), rectToSize(sensorRect));
-
// Calculate resolution for new use cases.
if (!newUseCases.isEmpty()) {
Map<UseCaseConfig<?>, UseCase> configToUseCaseMap = new HashMap<>();
Map<UseCaseConfig<?>, List<Size>> configToSupportedSizesMap = new HashMap<>();
+ SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
+ (CameraInfoInternal) getCameraInfo());
for (UseCase useCase : newUseCases) {
ConfigPair configPair = configPairMap.get(useCase);
// Combine with default configuration.
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 225604c..3072d63 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
@@ -22,6 +22,7 @@
import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
+import android.graphics.ImageFormat;
import android.util.Pair;
import android.util.Rational;
import android.util.Size;
@@ -54,17 +55,35 @@
class SupportedOutputSizesSorter {
private static final String TAG = "SupportedOutputSizesCollector";
private final CameraInfoInternal mCameraInfoInternal;
- private final Size mActiveArraySize;
+ private final Rational mFullFovRatio;
private final boolean mIsSensorLandscapeResolution;
private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
- SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
- @NonNull Size activeArraySize) {
+ SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal) {
mCameraInfoInternal = cameraInfoInternal;
- mActiveArraySize = activeArraySize;
- mIsSensorLandscapeResolution = mActiveArraySize.getWidth() >= mActiveArraySize.getHeight();
+ mFullFovRatio = calculateFullFovRatio(mCameraInfoInternal);
+ // Determines the sensor resolution orientation info by the full FOV ratio.
+ mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
+ >= mFullFovRatio.getDenominator() : true;
mSupportedOutputSizesSorterLegacy =
- new SupportedOutputSizesSorterLegacy(cameraInfoInternal, activeArraySize);
+ new SupportedOutputSizesSorterLegacy(cameraInfoInternal, mFullFovRatio);
+ }
+
+ /**
+ * Calculates the full FOV ratio by the output sizes retrieved from CameraInfoInternal.
+ *
+ * <p>For most devices, the full FOV ratio should match the aspect ratio of the max supported
+ * output sizes. The active pixel array info is not used because it may cause robolectric
+ * test to fail if it is not set in the test environment.
+ */
+ @Nullable
+ private Rational calculateFullFovRatio(@NonNull CameraInfoInternal cameraInfoInternal) {
+ List<Size> jpegOutputSizes = cameraInfoInternal.getSupportedResolutions(ImageFormat.JPEG);
+ if (jpegOutputSizes.isEmpty()) {
+ return null;
+ }
+ Size maxSize = Collections.max(jpegOutputSizes, new CompareSizesByArea());
+ return new Rational(maxSize.getWidth(), maxSize.getHeight());
}
@NonNull
@@ -285,11 +304,9 @@
// Sort the aspect ratio key set by the target aspect ratio.
List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
- Rational fullFovRatio = mActiveArraySize != null ? new Rational(
- mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
Collections.sort(aspectRatios,
new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
- aspectRatio, fullFovRatio));
+ aspectRatio, mFullFovRatio));
List<Size> resultList = new ArrayList<>();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
index 674a6a4..2329794 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
@@ -54,15 +54,17 @@
private static final String TAG = "SupportedOutputSizesCollector";
private final int mSensorOrientation;
private final int mLensFacing;
- private final Size mActiveArraySize;
+ private final Rational mFullFovRatio;
private final boolean mIsSensorLandscapeResolution;
SupportedOutputSizesSorterLegacy(@NonNull CameraInfoInternal cameraInfoInternal,
- @NonNull Size activeArraySize) {
+ @Nullable Rational fullFovRatio) {
mSensorOrientation = cameraInfoInternal.getSensorRotationDegrees();
mLensFacing = cameraInfoInternal.getLensFacing();
- mActiveArraySize = activeArraySize;
- mIsSensorLandscapeResolution = mActiveArraySize.getWidth() >= mActiveArraySize.getHeight();
+ mFullFovRatio = fullFovRatio;
+ // Determines the sensor resolution orientation info by the full FOV ratio.
+ mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
+ >= mFullFovRatio.getDenominator() : true;
}
/**
@@ -161,11 +163,9 @@
// Sort the aspect ratio key set by the target aspect ratio.
List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
- Rational fullFovRatio = mActiveArraySize != null ? new Rational(
- mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
Collections.sort(aspectRatios,
new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
- aspectRatio, fullFovRatio));
+ aspectRatio, mFullFovRatio));
// Put available sizes into final result list by aspect ratio distance to target ratio.
for (Rational rational : aspectRatios) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java
index e1f52298..2479820 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java
@@ -16,6 +16,8 @@
package androidx.camera.core.processing;
+import static android.opengl.GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
+
import static java.util.Objects.requireNonNull;
import android.opengl.EGL14;
@@ -24,7 +26,6 @@
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
-import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.util.Log;
import android.util.Size;
@@ -86,8 +87,8 @@
private static final float[] VERTEX_COORDS = {
-1.0f, -1.0f, // 0 bottom left
1.0f, -1.0f, // 1 bottom right
- -1.0f, 1.0f, // 2 top left
- 1.0f, 1.0f, // 3 top right
+ -1.0f, 1.0f, // 2 top left
+ 1.0f, 1.0f, // 3 top right
};
private static final FloatBuffer VERTEX_BUF = createFloatBuffer(VERTEX_COORDS);
@@ -100,7 +101,6 @@
private static final FloatBuffer TEX_BUF = createFloatBuffer(TEX_COORDS);
private static final int SIZEOF_FLOAT = 4;
- private static final int TEX_TARGET = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
private static final OutputSurface NO_OUTPUT_SURFACE =
OutputSurface.of(EGL14.EGL_NO_SURFACE, 0, 0);
@@ -119,7 +119,7 @@
private EGLSurface mTempSurface = EGL14.EGL_NO_SURFACE;
@Nullable
private Surface mCurrentSurface;
- private int mTexId = -1;
+ private int mExternalTextureId = -1;
private int mProgramHandle = -1;
private int mTexMatrixLoc = -1;
private int mPositionLoc = -1;
@@ -133,10 +133,10 @@
* thread as this method, so called GL thread, otherwise an {@link IllegalStateException}
* will be thrown.
*
- * @throws IllegalStateException if the renderer is already initialized or failed to be
- * initialized.
+ * @throws IllegalStateException if the renderer is already initialized or failed to be
+ * initialized.
* @throws IllegalArgumentException if the ShaderProvider fails to create shader or provides
- * invalid shader string.
+ * invalid shader string.
*/
public void init(@NonNull ShaderProvider shaderProvider) {
checkInitializedOrThrow(false);
@@ -147,6 +147,7 @@
createProgram(shaderProvider);
loadLocations();
createTexture();
+ useAndConfigureProgram();
} catch (IllegalStateException | IllegalArgumentException e) {
releaseInternal();
throw e;
@@ -172,7 +173,7 @@
* Register the output surface.
*
* @throws IllegalStateException if the renderer is not initialized or the caller doesn't run
- * on the GL thread.
+ * on the GL thread.
*/
public void registerOutputSurface(@NonNull Surface surface) {
checkInitializedOrThrow(true);
@@ -187,7 +188,7 @@
* Unregister the output surface.
*
* @throws IllegalStateException if the renderer is not initialized or the caller doesn't run
- * on the GL thread.
+ * on the GL thread.
*/
public void unregisterOutputSurface(@NonNull Surface surface) {
checkInitializedOrThrow(true);
@@ -201,20 +202,21 @@
*
* @return the texture name
* @throws IllegalStateException if the renderer is not initialized or the caller doesn't run
- * on the GL thread.
+ * on the GL thread.
*/
public int getTextureName() {
checkInitializedOrThrow(true);
checkGlThreadOrThrow();
- return mTexId;
+ return mExternalTextureId;
}
/**
* Renders the texture image to the output surface.
*
* @throws IllegalStateException if the renderer is not initialized, the caller doesn't run
- * on the GL thread or the surface is not registered by {@link #registerOutputSurface(Surface)}.
+ * on the GL thread or the surface is not registered by
+ * {@link #registerOutputSurface(Surface)}.
*/
public void render(long timestampNs, @NonNull float[] textureTransform,
@NonNull Surface surface) {
@@ -241,53 +243,16 @@
GLES20.glScissor(0, 0, outputSurface.getWidth(), outputSurface.getHeight());
}
- // Select the program.
- GLES20.glUseProgram(mProgramHandle);
- checkGlErrorOrThrow("glUseProgram");
-
- // Set the texture.
- GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
- GLES20.glBindTexture(TEX_TARGET, mTexId);
-
// TODO(b/245855601): Upload the matrix to GPU when textureTransform is changed.
// Copy the texture transformation matrix over.
GLES20.glUniformMatrix4fv(mTexMatrixLoc, /*count=*/1, /*transpose=*/false, textureTransform,
/*offset=*/0);
checkGlErrorOrThrow("glUniformMatrix4fv");
- // Enable the "aPosition" vertex attribute.
- GLES20.glEnableVertexAttribArray(mPositionLoc);
- checkGlErrorOrThrow("glEnableVertexAttribArray");
-
- // Connect vertexBuffer to "aPosition".
- int coordsPerVertex = 2;
- int vertexStride = 0;
- GLES20.glVertexAttribPointer(mPositionLoc, coordsPerVertex, GLES20.GL_FLOAT,
- /*normalized=*/false, vertexStride, VERTEX_BUF);
- checkGlErrorOrThrow("glVertexAttribPointer");
-
- // Enable the "aTextureCoord" vertex attribute.
- GLES20.glEnableVertexAttribArray(mTexCoordLoc);
- checkGlErrorOrThrow("glEnableVertexAttribArray");
-
- // Connect texBuffer to "aTextureCoord".
- int coordsPerTex = 2;
- int texStride = 0;
- GLES20.glVertexAttribPointer(mTexCoordLoc, coordsPerTex, GLES20.GL_FLOAT,
- /*normalized=*/false, texStride, TEX_BUF);
- checkGlErrorOrThrow("glVertexAttribPointer");
-
// Draw the rect.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /*firstVertex=*/0, /*vertexCount=*/4);
checkGlErrorOrThrow("glDrawArrays");
- // TODO(b/245855601): Figure out if these calls are necessary.
- // Done -- disable vertex array, texture, and program.
- GLES20.glDisableVertexAttribArray(mPositionLoc);
- GLES20.glDisableVertexAttribArray(mTexCoordLoc);
- GLES20.glUseProgram(0);
- GLES20.glBindTexture(TEX_TARGET, 0);
-
// Set timestamp
EGLExt.eglPresentationTimeANDROID(mEglDisplay, outputSurface.getEglSurface(), timestampNs);
@@ -391,6 +356,38 @@
}
}
+ private void useAndConfigureProgram() {
+ // Select the program.
+ GLES20.glUseProgram(mProgramHandle);
+ checkGlErrorOrThrow("glUseProgram");
+
+ // Set the texture.
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mExternalTextureId);
+
+ // Enable the "aPosition" vertex attribute.
+ GLES20.glEnableVertexAttribArray(mPositionLoc);
+ checkGlErrorOrThrow("glEnableVertexAttribArray");
+
+ // Connect vertexBuffer to "aPosition".
+ int coordsPerVertex = 2;
+ int vertexStride = 0;
+ GLES20.glVertexAttribPointer(mPositionLoc, coordsPerVertex, GLES20.GL_FLOAT,
+ /*normalized=*/false, vertexStride, VERTEX_BUF);
+ checkGlErrorOrThrow("glVertexAttribPointer");
+
+ // Enable the "aTextureCoord" vertex attribute.
+ GLES20.glEnableVertexAttribArray(mTexCoordLoc);
+ checkGlErrorOrThrow("glEnableVertexAttribArray");
+
+ // Connect texBuffer to "aTextureCoord".
+ int coordsPerTex = 2;
+ int texStride = 0;
+ GLES20.glVertexAttribPointer(mTexCoordLoc, coordsPerTex, GLES20.GL_FLOAT,
+ /*normalized=*/false, texStride, TEX_BUF);
+ checkGlErrorOrThrow("glVertexAttribPointer");
+ }
+
private void loadLocations() {
mPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition");
checkLocationOrThrow(mPositionLoc, "aPosition");
@@ -406,16 +403,20 @@
checkGlErrorOrThrow("glGenTextures");
int texId = textures[0];
- GLES20.glBindTexture(TEX_TARGET, texId);
+ GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, texId);
checkGlErrorOrThrow("glBindTexture " + texId);
- GLES20.glTexParameterf(TEX_TARGET, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
- GLES20.glTexParameterf(TEX_TARGET, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
- GLES20.glTexParameteri(TEX_TARGET, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
- GLES20.glTexParameteri(TEX_TARGET, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_NEAREST);
+ GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
+ GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
+ GLES20.GL_CLAMP_TO_EDGE);
checkGlErrorOrThrow("glTexParameter");
- mTexId = texId;
+ mExternalTextureId = texId;
}
private int loadFragmentShader(@NonNull ShaderProvider shaderProvider) {
@@ -493,7 +494,7 @@
mTexMatrixLoc = -1;
mPositionLoc = -1;
mTexCoordLoc = -1;
- mTexId = -1;
+ mExternalTextureId = -1;
mCurrentSurface = null;
mGlThread = null;
}
@@ -609,7 +610,7 @@
}
private static int querySurface(@NonNull EGLDisplay eglDisplay, @NonNull EGLSurface eglSurface,
- int what) {
+ int what) {
int[] value = new int[1];
EGL14.eglQuerySurface(eglDisplay, eglSurface, what, value, /*offset=*/0);
return value[0];
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 623c019..66ca7ab4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -127,7 +127,7 @@
@NonNull OutConfig outConfig) {
SurfaceEdge outputSurface;
Rect cropRect = outConfig.getCropRect();
- int rotationDegrees = input.getRotationDegrees();
+ int rotationDegrees = outConfig.getRotationDegrees();
boolean mirroring = outConfig.getMirroring();
// Calculate sensorToBufferTransform
@@ -156,7 +156,7 @@
/*hasCameraTransform=*/false,
// Crop rect is always the full size.
sizeToRect(outConfig.getSize()),
- /*rotationDegrees=*/0,
+ /*rotationDegrees=*/input.getRotationDegrees() - rotationDegrees,
/*mirroring=*/input.getMirroring() != mirroring);
return outputSurface;
@@ -168,10 +168,7 @@
private void sendSurfaceRequest(@NonNull SurfaceEdge input,
@NonNull Map<OutConfig, SurfaceEdge> outputs) {
SurfaceRequest surfaceRequest = input.createSurfaceRequest(mCameraInternal);
- setUpRotationUpdates(
- surfaceRequest,
- outputs,
- input.getRotationDegrees());
+ setUpRotationUpdates(surfaceRequest, outputs);
try {
mSurfaceProcessor.onInputSurface(surfaceRequest);
} catch (ProcessingException e) {
@@ -201,7 +198,7 @@
input.getStreamSpec().getResolution(),
output.getKey().getFormat(),
output.getKey().getCropRect(),
- input.getRotationDegrees(),
+ output.getKey().getRotationDegrees(),
output.getKey().getMirroring(),
mCameraInternal);
Futures.addCallback(future, new FutureCallback<SurfaceOutput>() {
@@ -235,17 +232,16 @@
*
* @param inputSurfaceRequest {@link SurfaceRequest} of the input edge.
* @param outputs the output edges.
- * @param rotatedDegrees how much the node rotates the buffer.
*/
void setUpRotationUpdates(
@NonNull SurfaceRequest inputSurfaceRequest,
- @NonNull Map<OutConfig, SurfaceEdge> outputs,
- int rotatedDegrees) {
+ @NonNull Map<OutConfig, SurfaceEdge> outputs) {
inputSurfaceRequest.setTransformationInfoListener(mainThreadExecutor(), info -> {
for (Map.Entry<OutConfig, SurfaceEdge> output : outputs.entrySet()) {
// To obtain the rotation degrees delta, the rotation performed by the node must be
// eliminated.
- int rotationDegrees = info.getRotationDegrees() - rotatedDegrees;
+ int rotationDegrees =
+ info.getRotationDegrees() - output.getKey().getRotationDegrees();
if (output.getKey().getMirroring()) {
// The order of transformation is cropping -> rotation -> mirroring. To
// change the rotation, one must consider the mirroring.
@@ -368,6 +364,11 @@
public abstract Size getSize();
/**
+ * How the input should be rotated clockwise.
+ */
+ abstract int getRotationDegrees();
+
+ /**
* The whether the stream should be mirrored.
*/
public abstract boolean getMirroring();
@@ -383,6 +384,7 @@
inputEdge.getFormat(),
inputEdge.getCropRect(),
getRotatedSize(inputEdge.getCropRect(), inputEdge.getRotationDegrees()),
+ inputEdge.getRotationDegrees(),
inputEdge.getMirroring());
}
@@ -393,9 +395,11 @@
public static OutConfig of(@CameraEffect.Targets int targets,
@CameraEffect.Formats int format,
@NonNull Rect cropRect,
- @NonNull Size size, boolean mirroring) {
+ @NonNull Size size,
+ int rotationDegrees,
+ boolean mirroring) {
return new AutoValue_SurfaceProcessorNode_OutConfig(randomUUID(), targets, format,
- cropRect, size, mirroring);
+ cropRect, size, rotationDegrees, mirroring);
}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index da96fe7..5eabff9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -223,7 +223,8 @@
mVirtualCamera.setChildrenEdges(outputEdges);
// Send the camera edge Surface to the camera2.
- SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config);
+ SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config,
+ streamSpec.getResolution());
builder.addSurface(mCameraEdge.getDeferrableSurface());
builder.addRepeatingCameraCaptureCallback(mVirtualCamera.getParentMetadataCallback());
addCameraErrorListener(builder, cameraId, config, streamSpec);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index e124da4..b652892 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -19,6 +19,7 @@
import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
@@ -113,6 +114,7 @@
null,
useCase.getDefaultConfig(true, mUseCaseConfigFactory)));
}
+
// Merge resolution configs.
List<Size> supportedResolutions =
new ArrayList<>(mParentCamera.getCameraInfoInternal().getSupportedResolutions(
@@ -121,6 +123,10 @@
mutableConfig.insertOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS,
getMergedResolutions(supportedResolutions, sensorSize,
childrenConfigs));
+
+ // Merge Surface occupancy priority.
+ mutableConfig.insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY,
+ getHighestSurfacePriority(childrenConfigs));
}
void bindChildren() {
@@ -170,6 +176,8 @@
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, // TODO: use JPEG for ImageCapture
cameraEdge.getCropRect(),
rectToSize(cameraEdge.getCropRect()),
+ // TODO: set rotation degrees for ImageCapture.
+ 0,
mirroring));
}
return outConfigs;
@@ -289,6 +297,15 @@
}
// --- private methods ---
+
+ private static int getHighestSurfacePriority(Set<UseCaseConfig<?>> childrenConfigs) {
+ int highestPriority = 0;
+ for (UseCaseConfig<?> childConfig : childrenConfigs) {
+ highestPriority = Math.max(highestPriority, childConfig.getSurfaceOccupancyPriority());
+ }
+ return highestPriority;
+ }
+
@NonNull
private SurfaceEdge getUseCaseEdge(@NonNull UseCase useCase) {
return requireNonNull(mChildrenEdges.get(useCase));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java
index fd92612..eef66e9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.core.streamsharing;
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 60482af..0a69a3f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -492,7 +492,7 @@
height, format, queueDepth, usage);
return mFakeImageReaderProxy;
})
- .setSessionOptionUnpacker((config, builder) -> {
+ .setSessionOptionUnpacker((resolution, config, builder) -> {
})
.setOnePixelShiftEnabled(false)
.build();
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index 5df4693..8c392d5 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -25,6 +25,7 @@
import android.os.Looper.getMainLooper
import android.util.Pair
import android.util.Rational
+import android.util.Size
import android.view.Surface
import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
import androidx.camera.core.CameraEffect.PREVIEW
@@ -95,6 +96,9 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class ImageCaptureTest {
+
+ private val resolution = Size(640, 480)
+
private lateinit var callbackHandler: Handler
private lateinit var callbackThread: HandlerThread
private lateinit var executor: Executor
@@ -247,8 +251,8 @@
private fun assertTakePictureManagerHasTheSameSurface(imageCapture: ImageCapture) {
val takePictureManagerSurface =
- imageCapture.takePictureManager.imagePipeline.createSessionConfigBuilder()
- .build().surfaces.single().surface.get()
+ imageCapture.takePictureManager.imagePipeline.createSessionConfigBuilder(
+ resolution).build().surfaces.single().surface.get()
val useCaseSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
assertThat(takePictureManagerSurface).isEqualTo(useCaseSurface)
}
@@ -696,7 +700,8 @@
.setCaptureMode(captureMode)
.setFlashMode(ImageCapture.FLASH_MODE_OFF)
.setCaptureOptionUnpacker { _: UseCaseConfig<*>?, _: CaptureConfig.Builder? -> }
- .setSessionOptionUnpacker { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+ .setSessionOptionUnpacker { _: Size, _: UseCaseConfig<*>?,
+ _: SessionConfig.Builder? -> }
builder.setBufferFormat(bufferFormat)
if (imageReaderProxyProvider != null) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 1c0be28..6ef3e65 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -229,7 +229,7 @@
// Arrange: attach Preview without a SurfaceProvider.
// Build and bind use case.
val sessionOptionUnpacker =
- { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+ { _: Size, _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
val preview = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setSessionOptionUnpacker(sessionOptionUnpacker)
@@ -511,7 +511,7 @@
fun setTargetRotation_transformationInfoUpdated() {
// Arrange: set up preview and verify target rotation in TransformationInfo.
val sessionOptionUnpacker =
- { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+ { _: Size, _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
val preview = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setSessionOptionUnpacker(sessionOptionUnpacker)
@@ -544,7 +544,7 @@
fun setSurfaceProviderAfterAttachment_receivesSurfaceProviderCallbacks() {
// Arrange: attach Preview without a SurfaceProvider.
val sessionOptionUnpacker =
- { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+ { _: Size, _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
val preview = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setSessionOptionUnpacker(sessionOptionUnpacker)
@@ -597,7 +597,7 @@
fun setSurfaceProviderAfterDetach_receivesSurfaceRequestAfterAttach() {
// Arrange: attach Preview without a SurfaceProvider.
val sessionOptionUnpacker =
- { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+ { _: Size, _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
val preview = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setSessionOptionUnpacker(sessionOptionUnpacker)
@@ -668,7 +668,7 @@
TransformationInfo> {
// Arrange.
val sessionOptionUnpacker =
- { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+ { _: Size, _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
val preview = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setSessionOptionUnpacker(sessionOptionUnpacker)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt
index 1304995..9d18516 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt
@@ -23,6 +23,7 @@
import androidx.camera.core.AspectRatio
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.AspectRatioUtil
import androidx.camera.core.impl.utils.CompareSizesByArea
import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.fakes.FakeUseCaseConfig
@@ -34,7 +35,6 @@
import org.robolectric.annotation.internal.DoNotInstrument
private const val TARGET_ASPECT_RATIO_NONE = -99
-private val LANDSCAPE_ACTIVE_ARRAY_SIZE = Size(4032, 3024)
private val DEFAULT_SUPPORTED_SIZES = listOf(
Size(4032, 3024), // 4:3
Size(3840, 2160), // 16:9
@@ -59,7 +59,7 @@
class SupportedOutputSizesSorterLegacyTest {
private val cameraInfoInternal = FakeCameraInfoInternal()
private val supportedOutputSizesSorterLegacy =
- SupportedOutputSizesSorterLegacy(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
+ SupportedOutputSizesSorterLegacy(cameraInfoInternal, AspectRatioUtil.ASPECT_RATIO_4_3)
@Test
fun checkFilterOutSmallSizesByDefaultSize640x480() {
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 fd9e4ea..c18ffdc 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
@@ -29,7 +29,6 @@
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.internal.DoNotInstrument
-private val LANDSCAPE_ACTIVE_ARRAY_SIZE = Size(4032, 3024)
private val DEFAULT_SUPPORTED_SIZES = listOf(
Size(4032, 3024), // 4:3
Size(3840, 2160), // 16:9
@@ -60,8 +59,7 @@
val cameraInfoInternal = FakeCameraInfoInternal().apply {
setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
}
- val supportedOutputSizesSorter =
- SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
+ val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
// Sets up the custom ordered resolutions
val useCaseConfig =
FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
@@ -94,8 +92,7 @@
val cameraInfoInternal = FakeCameraInfoInternal().apply {
setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
}
- val supportedOutputSizesSorter =
- SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
+ val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
// Sets up the custom supported resolutions
val useCaseConfig =
FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index c0d152c..1107797 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -36,7 +36,6 @@
import androidx.camera.core.impl.utils.TransformUtils
import androidx.camera.core.impl.utils.TransformUtils.is90or270
import androidx.camera.core.impl.utils.TransformUtils.rectToSize
-import androidx.camera.core.impl.utils.TransformUtils.rotateSize
import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig
@@ -62,12 +61,13 @@
class SurfaceProcessorNodeTest {
companion object {
- private const val ROTATION_DEGREES = 90
+ private const val INPUT_ROTATION_DEGREES = 90
+ private const val VIDEO_ROTATION_DEGREES = 0
private const val MIRRORING = false
private val INPUT_SIZE = Size(640, 480)
private val PREVIEW_CROP_RECT = Rect(0, 0, 600, 400)
private val VIDEO_CROP_RECT = Rect(0, 0, 300, 200)
- private val VIDEO_SIZE = Size(20, 30)
+ private val VIDEO_SIZE = Size(30, 20)
}
private lateinit var surfaceProcessorInternal: FakeSurfaceProcessorInternal
@@ -134,6 +134,7 @@
ImageFormat.JPEG,
inputEdge.cropRect,
TransformUtils.getRotatedSize(inputEdge.cropRect, inputEdge.rotationDegrees),
+ inputEdge.rotationDegrees,
inputEdge.mirroring
)
nodeInput = SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig))
@@ -207,10 +208,8 @@
for (rotationDegrees in arrayOf(0, 90, 180, 270)) {
// Arrange.
createSurfaceProcessorNode()
- val videoOutputSize = rotateSize(VIDEO_SIZE, rotationDegrees - ROTATION_DEGREES)
createInputEdge(
- previewRotationDegrees = rotationDegrees,
- videoOutputSize = videoOutputSize
+ inputRotationDegrees = rotationDegrees,
)
// The result cropRect should have zero left and top.
val expectedCropRect = if (is90or270(rotationDegrees))
@@ -229,9 +228,9 @@
assertThat(previewOutput.rotationDegrees).isEqualTo(0)
assertThat(previewOutput.mirroring).isFalse()
val videoOutput = nodeOutput[videoOutConfig]!!
- assertThat(videoOutput.streamSpec.resolution).isEqualTo(videoOutputSize)
- assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(videoOutputSize))
- assertThat(videoOutput.rotationDegrees).isEqualTo(0)
+ assertThat(videoOutput.streamSpec.resolution).isEqualTo(VIDEO_SIZE)
+ assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
+ assertThat(videoOutput.rotationDegrees).isEqualTo(rotationDegrees)
assertThat(videoOutput.mirroring).isTrue()
// Clean up.
@@ -282,7 +281,7 @@
// output surface will receive 0 degrees.
val previewSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
- assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+ assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(INPUT_ROTATION_DEGREES)
assertThat(previewSurfaceOutput.size).isEqualTo(Size(400, 600))
assertThat(previewSurfaceOutput.inputCropRect).isEqualTo(PREVIEW_CROP_RECT)
assertThat(previewTransformInfo.cropRect).isEqualTo(Rect(0, 0, 400, 600))
@@ -292,11 +291,11 @@
val videoSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
- assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+ assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(VIDEO_ROTATION_DEGREES)
assertThat(videoSurfaceOutput.size).isEqualTo(VIDEO_SIZE)
assertThat(videoSurfaceOutput.inputCropRect).isEqualTo(VIDEO_CROP_RECT)
assertThat(videoTransformInfo.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
- assertThat(videoTransformInfo.rotationDegrees).isEqualTo(0)
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(270)
assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(videoSurfaceOutput.mirroring).isTrue()
}
@@ -305,7 +304,7 @@
fun setRotationToInput_applyCropRotateAndMirroring_rotationIsPropagated() {
// Arrange.
createSurfaceProcessorNode()
- createInputEdge(previewRotationDegrees = 90)
+ createInputEdge(inputRotationDegrees = 90)
val inputSurface = nodeInput.surfaceEdge
val nodeOutput = node.transform(nodeInput)
provideSurfaces(nodeOutput)
@@ -319,14 +318,14 @@
// output surface will receive the remaining degrees.
val previewSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
- assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(90)
+ assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(INPUT_ROTATION_DEGREES)
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(180)
assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(previewSurfaceOutput.mirroring).isFalse()
val videoSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
- assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(90)
- assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
+ assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(VIDEO_ROTATION_DEGREES)
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(90)
assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(videoSurfaceOutput.mirroring).isTrue()
@@ -335,14 +334,14 @@
shadowOf(getMainLooper()).idle()
// Assert: video rotation degrees is opposite of preview because it's not mirrored.
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(90)
- assertThat(videoTransformInfo.rotationDegrees).isEqualTo(270)
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
}
@Test
fun setRotationAndMirroringToInput_applyCropRotateAndMirroring_rotationIsPropagated() {
// Arrange.
createSurfaceProcessorNode()
- createInputEdge(previewRotationDegrees = 90, mirroring = true)
+ createInputEdge(inputRotationDegrees = 90, mirroring = true)
val inputSurface = nodeInput.surfaceEdge
val nodeOutput = node.transform(nodeInput)
provideSurfaces(nodeOutput)
@@ -356,14 +355,14 @@
// output surface will receive the remaining degrees.
val previewSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
- assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(90)
+ assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(INPUT_ROTATION_DEGREES)
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(180)
assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(previewSurfaceOutput.mirroring).isTrue()
val videoSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
- assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(90)
- assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
+ assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(VIDEO_ROTATION_DEGREES)
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(90)
assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(videoSurfaceOutput.mirroring).isTrue()
@@ -372,7 +371,7 @@
shadowOf(getMainLooper()).idle()
// Assert: video rotation is the same as preview and are compensated by mirroring.
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(270)
- assertThat(videoTransformInfo.rotationDegrees).isEqualTo(270)
+ assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
}
@Test
@@ -419,7 +418,7 @@
sensorToBufferTransform: Matrix = Matrix(),
hasCameraTransform: Boolean = true,
previewCropRect: Rect = PREVIEW_CROP_RECT,
- previewRotationDegrees: Int = ROTATION_DEGREES,
+ inputRotationDegrees: Int = INPUT_ROTATION_DEGREES,
mirroring: Boolean = MIRRORING,
videoOutputSize: Size = VIDEO_SIZE,
frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
@@ -431,7 +430,7 @@
sensorToBufferTransform,
hasCameraTransform,
previewCropRect,
- previewRotationDegrees,
+ inputRotationDegrees,
mirroring,
)
videoOutConfig = OutConfig.of(
@@ -439,6 +438,7 @@
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
VIDEO_CROP_RECT,
videoOutputSize,
+ VIDEO_ROTATION_DEGREES,
true
)
previewOutConfig = OutConfig.of(inputEdge)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index 97e23d0..1c5c01c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -38,6 +38,7 @@
import androidx.camera.testing.fakes.FakeSurfaceEffect
import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.testing.fakes.FakeUseCaseConfig
import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CompletableDeferred
@@ -60,8 +61,12 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class StreamSharingTest {
- private val child1 = FakeUseCase()
- private val child2 = FakeUseCase()
+ private val child1 = FakeUseCase(
+ FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(1).useCaseConfig
+ )
+ private val child2 = FakeUseCase(
+ FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(2).useCaseConfig
+ )
private val useCaseConfigFactory = FakeUseCaseConfigFactory()
private val camera = FakeCamera()
private lateinit var streamSharing: StreamSharing
@@ -91,6 +96,15 @@
}
@Test
+ fun getParentSurfacePriority_isHighestChildrenPriority() {
+ assertThat(
+ streamSharing.mergeConfigs(
+ camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+ ).surfaceOccupancyPriority
+ ).isEqualTo(2)
+ }
+
+ @Test
fun verifySupportedEffects() {
assertThat(streamSharing.isEffectTargetsSupported(PREVIEW or VIDEO_CAPTURE)).isTrue()
assertThat(
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionMode.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionMode.java
index bdafc4f..39edfa3 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionMode.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionMode.java
@@ -65,7 +65,6 @@
* not a specific Camera supports an extension mode use
* {@link ExtensionsManager#isExtensionAvailable(CameraSelector, int)}.
*
- * @hide
*/
@IntDef({NONE, BOKEH, HDR, NIGHT, FACE_RETOUCH, AUTO})
@Retention(RetentionPolicy.SOURCE)
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/package-info.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/package-info.java
index b3ced1b..7ba5870 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/package-info.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.extensions.internal.compat.quirk;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/package-info.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/package-info.java
index cc91045..5613f74 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/package-info.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.extensions.internal.compat.workaround;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/package-info.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/package-info.java
index 878eaaf..42ce603 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/package-info.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.extensions.internal;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/package-info.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/package-info.java
index b62b600..ded39ce 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/package-info.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/package-info.java
@@ -16,7 +16,6 @@
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.extensions.internal.sessionprocessor;
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index 1bf7e70..88864f7 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -87,7 +87,7 @@
mainThreadExecutor(),
surfaceProcessor
)
- val preview = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val preview = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val useCaseGroup = UseCaseGroup.Builder().addUseCase(preview).addEffect(effect).build()
runBlocking(MainScope().coroutineContext) {
@@ -215,7 +215,7 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(
lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA,
@@ -234,8 +234,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(
lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA,
@@ -263,7 +263,7 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(
lifecycleOwner0,
@@ -285,8 +285,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(
lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA,
@@ -310,7 +310,7 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(
lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA, useCase
@@ -331,8 +331,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(
lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA, useCase0, useCase1
@@ -351,13 +351,13 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val camera0 = provider.bindToLifecycle(
lifecycleOwner0,
CameraSelector.DEFAULT_BACK_CAMERA, useCase0
)
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val camera1 = provider.bindToLifecycle(
lifecycleOwner1,
CameraSelector.DEFAULT_BACK_CAMERA, useCase1
@@ -390,8 +390,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val camera0 = provider.bindToLifecycle(
lifecycleOwner0,
@@ -418,8 +418,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
provider.bindToLifecycle(lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA, useCase0)
@@ -441,8 +441,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val camera0 = provider.bindToLifecycle(
lifecycleOwner0,
@@ -494,7 +494,7 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
// The front camera is not defined, we should get the IllegalArgumentException when it
// tries to get the camera.
@@ -542,7 +542,7 @@
ProcessCameraProvider.configureInstance(FakeAppConfig.create())
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val camera: LifecycleCamera =
provider.bindToLifecycle(
lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA,
@@ -559,7 +559,7 @@
ProcessCameraProvider.configureInstance(FakeAppConfig.create())
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
lifecycleOwner0.startAndResume()
val camera: LifecycleCamera =
provider.bindToLifecycle(
@@ -576,7 +576,7 @@
ProcessCameraProvider.configureInstance(FakeAppConfig.create())
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
lifecycleOwner0.startAndResume()
val camera: LifecycleCamera =
provider.bindToLifecycle(
@@ -595,7 +595,7 @@
ProcessCameraProvider.configureInstance(FakeAppConfig.create())
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
lifecycleOwner0.startAndResume()
val camera: LifecycleCamera =
provider.bindToLifecycle(
@@ -673,8 +673,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val singleCameraConfig0 = SingleCameraConfig.Builder()
.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
@@ -711,9 +711,9 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase2 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase2 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val singleCameraConfig0 = SingleCameraConfig.Builder()
.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
@@ -770,7 +770,7 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val singleCameraConfig0 = SingleCameraConfig.Builder()
.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
@@ -797,8 +797,8 @@
runBlocking(MainScope().coroutineContext) {
provider = ProcessCameraProvider.getInstance(context).await()
- val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
- val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
val singleCameraConfig0 = SingleCameraConfig.Builder()
.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
index 5688338..a87cc14 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
@@ -590,7 +590,6 @@
*
* @param context The context used to initialize CameraX
* @param cameraSelector The selector to select cameras with.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.TESTS)
@NonNull
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
index 128c998..4f642bc 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
@@ -32,7 +32,6 @@
* A {@link CameraCoordinator} implementation that contains concurrent camera mode and camera id
* information.
*
- * @hide
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
index 7a28551..6b5abe0 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
@@ -42,6 +42,8 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class FakeUseCase extends UseCase {
+ private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 0;
+
private volatile boolean mIsDetached = false;
private final AtomicInteger mStateAttachedCount = new AtomicInteger(0);
private final CaptureType mCaptureType;
@@ -69,7 +71,9 @@
* Creates a new instance of a {@link FakeUseCase} with a default configuration.
*/
public FakeUseCase() {
- this(new FakeUseCaseConfig.Builder().getUseCaseConfig());
+ this(new FakeUseCaseConfig.Builder()
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+ .getUseCaseConfig());
}
/**
@@ -81,7 +85,7 @@
@Override
public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
return new FakeUseCaseConfig.Builder(config)
- .setSessionOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> {
+ .setSessionOptionUnpacker((resolution, useCaseConfig, sessionConfigBuilder) -> {
});
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java
index 8073a00..ce4b5f5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfigFactory.java
@@ -52,7 +52,8 @@
MutableOptionsBundle mutableConfig = MutableOptionsBundle.create();
mutableConfig.insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, (config, builder) -> {});
- mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER, (config, builder) -> {});
+ mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
+ (resolution, config, builder) -> {});
return OptionsBundle.from(mutableConfig);
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 2296d9f..a4c1997 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -278,7 +278,6 @@
*
* @return The mirror mode of the intended target.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@MirrorMode.Mirror
@@ -527,7 +526,8 @@
// MediaCodec class instead.
mDeferrableSurface.setContainerClass(MediaCodec.class);
- SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
+ streamSpec.getResolution());
sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
sessionConfigBuilder.addErrorListener(
(sessionConfig, error) -> resetPipeline(cameraId, config, streamSpec));
@@ -592,7 +592,6 @@
}
/**
- * @hide
*/
@Nullable
@RestrictTo(Scope.TESTS)
@@ -1128,7 +1127,6 @@
/**
* @inheritDoc
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -1329,7 +1327,6 @@
* @param mirrorMode The mirror mode of the intended target.
* @return The current Builder.
*
- * @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/audio/package-info.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/audio/package-info.java
index 3cf3391..e507a70 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/audio/package-info.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/audio/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
package androidx.camera.video.internal.audio;
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 8c5c619..c2fd8a8 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -1327,7 +1327,7 @@
videoEncoderInfoFinder: Function<VideoEncoderConfig, VideoEncoderInfo> =
Function { createVideoEncoderInfo() },
): VideoCapture<VideoOutput> = VideoCapture.Builder(videoOutput)
- .setSessionOptionUnpacker { _, _ -> }
+ .setSessionOptionUnpacker { _, _, _ -> }
.apply {
targetRotation?.let { setTargetRotation(it) }
mirrorMode?.let { setMirrorMode(it) }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
index c4eddbe..0288497 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
@@ -63,6 +63,7 @@
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -138,6 +139,7 @@
assertThat(lastState).isEqualTo(DeviceState.Closed)
}
+ @Ignore // b/269523374
@Test
fun cameraSessionListener_receivesClose_afterUnbindAll(): Unit = runBlocking {
val imageCaptureBuilder = ImageCapture.Builder()
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
index 1bcfbd0..f96b0b1 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
@@ -52,7 +52,6 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.hamcrest.CoreMatchers.equalTo
-import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Assert
import org.junit.Assume
@@ -257,11 +256,6 @@
@Test
fun futureCompletes_whenFocusMeteringWithAe() {
assumeThat(
- "b/270520932: IllegalArgumentException from Camera2 in CameraPipe",
- implName, not(CameraPipeConfig::class.simpleName),
- )
-
- assumeThat(
"No AE region available on this device!",
hasMeteringRegion(cameraSelector, FLAG_AE), equalTo(true)
)
@@ -275,11 +269,6 @@
@Test
fun futureCompletes_whenFocusMeteringWithAwb() {
assumeThat(
- "b/270520932: IllegalArgumentException from Camera2 in CameraPipe",
- implName, not(CameraPipeConfig::class.simpleName),
- )
-
- assumeThat(
"No AWB region available on this device!",
hasMeteringRegion(cameraSelector, FLAG_AWB), equalTo(true)
)
@@ -293,11 +282,6 @@
@Test
fun futureCompletes_whenFocusMeteringWithAeAwb() {
assumeThat(
- "b/270520932: IllegalArgumentException from Camera2 in CameraPipe",
- implName, not(CameraPipeConfig::class.simpleName),
- )
-
- assumeThat(
"No AE/AWB region available on this device!",
hasMeteringRegion(cameraSelector, FLAG_AE or FLAG_AWB), equalTo(true)
)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
index 857db58..fed1f5b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
@@ -74,6 +74,7 @@
.getLaunchIntentForPackage(CORE_TEST_APP_PACKAGE)!!.apply {
putExtra(INTENT_EXTRA_CAMERA_ID, cameraId)
putExtra(INTENT_EXTRA_USE_CASE_COMBINATION, useCaseCombination)
+ setClassName(CORE_TEST_APP_PACKAGE, CameraXActivity::class.java.name)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
index 6caecd9..c313041 100644
--- a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -18,6 +18,17 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
+ android:name=".ConcurrentCameraActivity"
+ android:screenOrientation="sensorPortrait"
+ android:exported="true"
+ android:label="Concurrent Camera Test App"
+ android:taskAffinity="androidx.camera.integration.core.ConcurrentCameraActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".CameraXActivity"
android:exported="true"
android:label="Camera Core Test App"
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
new file mode 100644
index 0000000..c9fb45a
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
@@ -0,0 +1,334 @@
+/*
+ * 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.camera.integration.core;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.Preview;
+import androidx.camera.core.UseCaseGroup;
+import androidx.camera.core.concurrent.ConcurrentCameraConfig;
+import androidx.camera.core.concurrent.SingleCameraConfig;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+/**
+ * Concurrent camera activity.
+ */
+public class ConcurrentCameraActivity extends AppCompatActivity {
+ private static final String TAG = "ConcurrentCameraActivity";
+ private static final int REQUEST_CODE_PERMISSIONS = 1001;
+ private static final String[] REQUIRED_PERMISSIONS = new String[] {
+ "android.permission.CAMERA"
+ };
+
+ @NonNull private PreviewView mSinglePreviewView;
+ @NonNull private PreviewView mFrontPreviewView;
+ @NonNull private PreviewView mBackPreviewView;
+ @NonNull private FrameLayout mFrontPreviewViewForPip;
+ @NonNull private FrameLayout mBackPreviewViewForPip;
+ @NonNull private FrameLayout mFrontPreviewViewForSideBySide;
+ @NonNull private FrameLayout mBackPreviewViewForSideBySide;
+ @NonNull private ToggleButton mModeButton;
+ @NonNull private ToggleButton mLayoutButton;
+ @NonNull private ToggleButton mToggleButton;
+ @NonNull private LinearLayout mSideBySideLayout;
+ @NonNull private FrameLayout mPiPLayout;
+ @Nullable private ProcessCameraProvider mCameraProvider;
+ private boolean mIsConcurrentModeOn = false;
+ private boolean mIsLayoutPiP = true;
+ private boolean mIsFrontPrimary = true;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_concurrent_camera);
+
+ mFrontPreviewViewForPip = findViewById(R.id.camera_front_pip);
+ mBackPreviewViewForPip = findViewById(R.id.camera_back_pip);
+ mBackPreviewViewForSideBySide = findViewById(R.id.camera_back_side_by_side);
+ mFrontPreviewViewForSideBySide = findViewById(R.id.camera_front_side_by_side);
+ mSideBySideLayout = findViewById(R.id.layout_side_by_side);
+ mPiPLayout = findViewById(R.id.layout_pip);
+ mModeButton = findViewById(R.id.mode_button);
+ mLayoutButton = findViewById(R.id.layout_button);
+ mToggleButton = findViewById(R.id.toggle_button);
+
+ boolean isConcurrentCameraSupported =
+ getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_CONCURRENT);
+ mModeButton.setEnabled(isConcurrentCameraSupported);
+ mLayoutButton.setEnabled(false);
+ if (!isConcurrentCameraSupported) {
+ Toast.makeText(this, getString(R.string.concurrent_not_supported_warning),
+ Toast.LENGTH_SHORT).show();
+ }
+ mModeButton.setOnClickListener(view -> {
+ if (mCameraProvider == null) {
+ return;
+ }
+ mFrontPreviewView = null;
+ mBackPreviewView = null;
+ // Switch the concurrent mode
+ if (mCameraProvider != null && mIsConcurrentModeOn) {
+ mIsFrontPrimary = true;
+ mIsLayoutPiP = true;
+ bindPreviewForSingle(mCameraProvider);
+ mIsConcurrentModeOn = false;
+ } else {
+ mIsLayoutPiP = true;
+ bindPreviewForPiP(mCameraProvider);
+ mIsConcurrentModeOn = true;
+ }
+ mLayoutButton.setEnabled(mCameraProvider != null && mIsConcurrentModeOn);
+ mToggleButton.setEnabled(mCameraProvider != null && mIsConcurrentModeOn);
+ });
+ mLayoutButton.setOnClickListener(view -> {
+ if (mIsLayoutPiP) {
+ bindPreviewForSideBySide();
+ } else {
+ bindPreviewForPiP(mCameraProvider);
+ }
+ mIsLayoutPiP = !mIsLayoutPiP;
+ });
+ mToggleButton.setOnClickListener(view -> {
+ mIsFrontPrimary = !mIsFrontPrimary;
+ if (mIsConcurrentModeOn) {
+ if (mIsLayoutPiP) {
+ bindPreviewForPiP(mCameraProvider);
+ } else {
+ bindPreviewForSideBySide();
+ }
+ } else {
+ bindPreviewForSingle(mCameraProvider);
+ }
+ });
+ if (allPermissionsGranted()) {
+ if (mCameraProvider != null) {
+ mCameraProvider.unbindAll();
+ }
+ startCamera();
+ } else {
+ ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+ }
+ }
+
+ private void startCamera() {
+ final ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
+ ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ mCameraProvider = cameraProviderFuture.get();
+ bindPreviewForSingle(mCameraProvider);
+ } catch (ExecutionException | InterruptedException e) {
+ // No errors need to be handled for this Future.
+ // This should never be reached.
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+ void bindPreviewForSingle(@NonNull ProcessCameraProvider cameraProvider) {
+ cameraProvider.unbindAll();
+ mSideBySideLayout.setVisibility(GONE);
+ mFrontPreviewViewForPip.setVisibility(VISIBLE);
+ mBackPreviewViewForPip.setVisibility(GONE);
+ mPiPLayout.setVisibility(VISIBLE);
+ // Front
+ mSinglePreviewView = new PreviewView(this);
+ mSinglePreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+ mFrontPreviewViewForPip.addView(mSinglePreviewView);
+ Preview previewFront = new Preview.Builder()
+ .build();
+ CameraSelector cameraSelectorFront = new CameraSelector.Builder()
+ .requireLensFacing(mIsFrontPrimary
+ ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK)
+ .build();
+ previewFront.setSurfaceProvider(mSinglePreviewView.getSurfaceProvider());
+ cameraProvider.bindToLifecycle(
+ this, cameraSelectorFront, previewFront);
+ }
+ void bindPreviewForPiP(@NonNull ProcessCameraProvider cameraProvider) {
+ mSideBySideLayout.setVisibility(GONE);
+ mFrontPreviewViewForPip.setVisibility(VISIBLE);
+ mBackPreviewViewForPip.setVisibility(VISIBLE);
+ mPiPLayout.setVisibility(VISIBLE);
+ if (mFrontPreviewView == null && mBackPreviewView == null) {
+ // Front
+ mFrontPreviewView = new PreviewView(this);
+ mFrontPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+ mFrontPreviewViewForPip.removeAllViews();
+ mFrontPreviewViewForPip.addView(mFrontPreviewView,
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ // Back
+ mBackPreviewView = new PreviewView(this);
+ mBackPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+ mBackPreviewViewForPip.removeAllViews();
+ mBackPreviewViewForPip.addView(mBackPreviewView,
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ cameraProvider.unbindAll();
+ bindToLifecycleForConcurrentCamera(
+ cameraProvider,
+ this,
+ mFrontPreviewView,
+ mBackPreviewView);
+ } else {
+ updateFrontAndBackView(
+ mIsFrontPrimary,
+ mFrontPreviewViewForPip,
+ mBackPreviewViewForPip,
+ mFrontPreviewView,
+ mBackPreviewView);
+ }
+ }
+ void bindPreviewForSideBySide() {
+ mSideBySideLayout.setVisibility(VISIBLE);
+ mPiPLayout.setVisibility(GONE);
+ if (mFrontPreviewView == null && mBackPreviewView == null) {
+ mFrontPreviewView = new PreviewView(this);
+ mFrontPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+ mBackPreviewView = new PreviewView(this);
+ mBackPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+ }
+ updateFrontAndBackView(
+ mIsFrontPrimary,
+ mFrontPreviewViewForSideBySide,
+ mBackPreviewViewForSideBySide,
+ mFrontPreviewView,
+ mBackPreviewView);
+ }
+ private static void bindToLifecycleForConcurrentCamera(
+ @NonNull ProcessCameraProvider cameraProvider,
+ @NonNull LifecycleOwner lifecycleOwner,
+ @NonNull PreviewView frontPreviewView,
+ @NonNull PreviewView backPreviewView) {
+ Preview previewFront = new Preview.Builder()
+ .build();
+ CameraSelector cameraSelectorPrimary = null;
+ CameraSelector cameraSelectorSecondary = null;
+ for (List<CameraInfo> cameraInfoList : cameraProvider.getAvailableConcurrentCameraInfos()) {
+ for (CameraInfo cameraInfo : cameraInfoList) {
+ if (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_FRONT) {
+ cameraSelectorPrimary = cameraInfo.getCameraSelector();
+ } else if (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_BACK) {
+ cameraSelectorSecondary = cameraInfo.getCameraSelector();
+ }
+ }
+ }
+ if (cameraSelectorPrimary == null || cameraSelectorSecondary == null) {
+ return;
+ }
+ previewFront.setSurfaceProvider(frontPreviewView.getSurfaceProvider());
+ SingleCameraConfig primary = new SingleCameraConfig.Builder()
+ .setLifecycleOwner(lifecycleOwner)
+ .setUseCaseGroup(new UseCaseGroup.Builder()
+ .addUseCase(previewFront)
+ .build())
+ .setCameraSelector(cameraSelectorPrimary)
+ .build();
+ Preview previewBack = new Preview.Builder()
+ .build();
+ previewBack.setSurfaceProvider(backPreviewView.getSurfaceProvider());
+ SingleCameraConfig secondary = new SingleCameraConfig.Builder()
+ .setLifecycleOwner(lifecycleOwner)
+ .setUseCaseGroup(new UseCaseGroup.Builder()
+ .addUseCase(previewBack)
+ .build())
+ .setCameraSelector(cameraSelectorSecondary)
+ .build();
+ ConcurrentCameraConfig concurrentCameraConfig =
+ new ConcurrentCameraConfig.Builder()
+ .setCameraConfigs(ImmutableList.of(primary, secondary))
+ .build();
+ cameraProvider.bindToLifecycle(concurrentCameraConfig);
+ }
+ private static void updateFrontAndBackView(
+ boolean isFrontPrimary,
+ @NonNull ViewGroup frontParent,
+ @NonNull ViewGroup backParent,
+ @NonNull View frontChild,
+ @NonNull View backChild) {
+ frontParent.removeAllViews();
+ if (frontChild.getParent() != null) {
+ ((ViewGroup) frontChild.getParent()).removeView(frontChild);
+ }
+ backParent.removeAllViews();
+ if (backChild.getParent() != null) {
+ ((ViewGroup) backChild.getParent()).removeView(backChild);
+ }
+ if (isFrontPrimary) {
+ frontParent.addView(frontChild,
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ backParent.addView(backChild,
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ } else {
+ frontParent.addView(backChild,
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ backParent.addView(frontChild,
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ }
+ }
+ private boolean allPermissionsGranted() {
+ for (String permission : REQUIRED_PERMISSIONS) {
+ if (ContextCompat.checkSelfPermission(this, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ startCamera();
+ } else {
+ Toast.makeText(this, getString(R.string.permission_warning),
+ Toast.LENGTH_SHORT).show();
+ this.finish();
+ }
+ }
+ }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml
new file mode 100644
index 0000000..45e472a
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ 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="match_parent"
+ tools:context="androidx.camera.integration.core.ConcurrentCameraActivity">
+
+ <FrameLayout
+ android:id="@+id/layout_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:visibility="gone"
+ android:id="@+id/layout_pip"
+ android:orientation="vertical"
+ android:clipToPadding="false"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/camera_front_pip"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="false"
+ android:background="#000"
+ android:contentDescription="@string/preview_front"
+ android:importantForAccessibility="no"/>
+
+ <FrameLayout
+ android:id="@+id/camera_back_pip"
+ android:layout_width="120dp"
+ android:layout_height="200dp"
+ android:layout_centerInParent="false"
+ android:layout_gravity="bottom|right"
+ android:elevation="30dp"
+ android:layout_marginBottom="30dp"
+ android:layout_marginRight="30dp"
+ android:contentDescription="@string/preview_back"
+ android:importantForAccessibility="no"/>
+ </FrameLayout>
+
+
+ <LinearLayout
+ android:visibility="gone"
+ android:id="@+id/layout_side_by_side"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/camera_front_side_by_side"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ app:implementationMode="compatible"
+ android:layout_centerInParent="false"
+ android:background="#000"
+ android:contentDescription="@string/preview_front"
+ android:importantForAccessibility="no"/>
+
+ <FrameLayout
+ android:id="@+id/camera_back_side_by_side"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ app:implementationMode="compatible"
+ android:layout_centerInParent="false"
+ android:background="#000"
+ android:contentDescription="@string/preview_back"
+ android:importantForAccessibility="no"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ToggleButton
+ android:id="@+id/mode_button"
+ android:textOn="@string/switch_mode"
+ android:textOff="@string/switch_mode"
+ android:layout_width="100dp"
+ android:layout_height="70dp"
+ android:layout_gravity="top|right"
+ android:layout_marginRight="15dp"
+ android:layout_marginTop="15dp" />
+
+ <ToggleButton
+ android:id="@+id/layout_button"
+ android:textOn="@string/change_layout"
+ android:textOff="@string/change_layout"
+ android:layout_width="100dp"
+ android:layout_height="70dp"
+ android:layout_gravity="top|right"
+ android:layout_marginRight="15dp"
+ android:layout_marginTop="15dp" />
+
+ <ToggleButton
+ android:id="@+id/toggle_button"
+ android:textOn="@string/toggle_camera"
+ android:textOff="@string/toggle_camera"
+ android:layout_width="100dp"
+ android:layout_height="70dp"
+ android:layout_gravity="top|right"
+ android:layout_marginRight="15dp"
+ android:layout_marginTop="15dp" />
+ </LinearLayout>
+ </FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
+<!--</LinearLayout>-->
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/values/strings.xml b/camera/integration-tests/coretestapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d54e0d1
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/values/strings.xml
@@ -0,0 +1,28 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="preview_front">Front Camera</string>
+ <string name="preview_back">Back Camera</string>
+ <string name="switch_mode">Switch Mode</string>
+ <string name="change_layout">Change Layout</string>
+ <string name="toggle_camera">Toggle Camera</string>
+ <string name="toggle">Toggle</string>
+ <string name="finish">Finish</string>
+ <string name="is_front_primary">IsFrontPrimary</string>
+ <string name="permission_warning">Permissions not granted by the user.</string>
+ <string name="concurrent_not_supported_warning">Concurrent camera not supported</string>
+</resources>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/values/style.xml b/camera/integration-tests/coretestapp/src/main/res/values/style.xml
index c1d6296f..92cb421 100644
--- a/camera/integration-tests/coretestapp/src/main/res/values/style.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/values/style.xml
@@ -21,4 +21,8 @@
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
+ <style name="AppTheme.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
</resources>
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/PlaceListNavigationTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/PlaceListNavigationTemplateDemoScreen.java
index 550aa51..479d959 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/PlaceListNavigationTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/PlaceListNavigationTemplateDemoScreen.java
@@ -103,20 +103,27 @@
.setTitle(getCarContext().getString(R.string.place_list_nav_template_demo_title))
.build();
- return new PlaceListNavigationTemplate.Builder()
- .setItemList(mPlaces.getPlaceList(/* randomOrder =*/isAppDrivenRefreshEnabled))
- .setHeader(header)
- .setMapActionStrip(RoutingDemoModels.getMapActionStrip(getCarContext()))
- .setActionStrip(
- new ActionStrip.Builder()
- .addAction(
- new Action.Builder()
- .setTitle(getCarContext().getString(
- R.string.search_action_title))
- .setOnClickListener(() -> {
- })
- .build())
- .build())
- .build();
+ PlaceListNavigationTemplate.Builder placeListNavigationTemplateBuilder =
+ new PlaceListNavigationTemplate.Builder()
+ .setItemList(
+ mPlaces.getPlaceList(/* randomOrder= */ true))
+ .setHeader(header)
+ .setMapActionStrip(RoutingDemoModels.getMapActionStrip(getCarContext()))
+ .setActionStrip(
+ new ActionStrip.Builder()
+ .addAction(
+ new Action.Builder()
+ .setTitle(getCarContext().getString(
+ R.string.search_action_title))
+ .setOnClickListener(() -> {
+ })
+ .build())
+ .build());
+
+ if (!isAppDrivenRefreshEnabled) {
+ placeListNavigationTemplateBuilder.setOnContentRefreshListener(this::invalidate);
+ }
+
+ return placeListNavigationTemplateBuilder.build();
}
}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/tabtemplates/TabTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/tabtemplates/TabTemplateDemoScreen.java
index af4b1c3..63300fa 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/tabtemplates/TabTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/tabtemplates/TabTemplateDemoScreen.java
@@ -26,6 +26,7 @@
import androidx.car.app.CarToast;
import androidx.car.app.Screen;
import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.GridItem;
import androidx.car.app.model.GridTemplate;
@@ -142,6 +143,7 @@
}
return new ListTemplate.Builder()
.setSingleList(listBuilder.build())
+ .addAction(createFabBackAction())
.build();
}
@@ -162,6 +164,7 @@
}
return new GridTemplate.Builder()
.setSingleList(listBuilder.build())
+ .addAction(createFabBackAction())
.build();
}
@@ -199,4 +202,13 @@
.build();
}
+ private Action createFabBackAction() {
+ Action action = new Action.Builder()
+ .setIcon(CarIcon.BACK)
+ .setBackgroundColor(CarColor.BLUE)
+ .setOnClickListener(() -> getScreenManager().pop())
+ .build();
+ return action;
+ }
+
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 5a12844..3d6e7aa 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -17,6 +17,7 @@
implementation(project(":compose:ui:ui-text"))
implementation(project(":compose:animation:animation"))
implementation(project(":compose:animation:animation-graphics"))
+ implementation(project(":compose:ui:ui:ui-samples"))
implementation(project(":compose:animation:animation:animation-samples"))
implementation(project(":compose:animation:animation-core:animation-core-samples"))
implementation(project(":compose:foundation:foundation"))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 4e98ae6..91b2731 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -38,7 +38,10 @@
import androidx.compose.animation.demos.lookahead.AnimateBoundsModifierDemo
import androidx.compose.animation.demos.lookahead.CraneDemo
import androidx.compose.animation.demos.lookahead.LookaheadLayoutWithAlignmentLinesDemo
+import androidx.compose.animation.demos.lookahead.LookaheadSamplesDemo
+import androidx.compose.animation.demos.lookahead.LookaheadWithDisappearingMovableContentDemo
import androidx.compose.animation.demos.lookahead.LookaheadWithFlowRowDemo
+import androidx.compose.animation.demos.lookahead.LookaheadWithIntrinsicsDemo
import androidx.compose.animation.demos.lookahead.LookaheadWithMovableContentDemo
import androidx.compose.animation.demos.lookahead.ScreenSizeChangeDemo
import androidx.compose.animation.demos.singlevalue.SingleValueAnimationDemo
@@ -105,13 +108,22 @@
},
ComposableDemo("Crane Nested Shared Element") { CraneDemo() },
ComposableDemo("Screen Size Change Demo") { ScreenSizeChangeDemo() },
- ComposableDemo("Lookahead With Movable Content") {
- LookaheadWithMovableContentDemo()
+ ComposableDemo("Lookahead Samples Demo") {
+ LookaheadSamplesDemo()
},
ComposableDemo("Lookahead With Alignment Lines") {
LookaheadLayoutWithAlignmentLinesDemo()
},
- ComposableDemo("Flow Row Lookahead") { LookaheadWithFlowRowDemo() },
+ ComposableDemo("Lookahead With Flow Row") { LookaheadWithFlowRowDemo() },
+ ComposableDemo("Lookahead With Intrinsics") {
+ LookaheadWithIntrinsicsDemo()
+ },
+ ComposableDemo("Lookahead With Movable Content") {
+ LookaheadWithMovableContentDemo()
+ },
+ ComposableDemo("Lookahead With Disappearing Movable Content") {
+ LookaheadWithDisappearingMovableContentDemo()
+ },
)
),
DemoCategory(
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt
index 801a1ff..96a707a 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifier.kt
@@ -20,6 +20,7 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.TwoWayConverter
@@ -28,15 +29,14 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LookaheadLayoutScope
-import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -44,9 +44,8 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-context(LookaheadLayoutScope)
fun Modifier.animateBounds(
- modifier: Modifier,
+ modifier: Modifier = Modifier,
sizeAnimationSpec: FiniteAnimationSpec<IntSize> = spring(
Spring.DampingRatioNoBouncy,
Spring.StiffnessMediumLow
@@ -55,114 +54,116 @@
Spring.DampingRatioNoBouncy,
Spring.StiffnessMediumLow
),
+ lookaheadScope: (closestLookaheadScope: LookaheadScope) -> LookaheadScope = { it }
) = composed {
- val coroutineScope = rememberCoroutineScope()
- var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) }
- val offsetAnimation = remember {
- DeferredAnimation(coroutineScope, IntOffset.VectorConverter)
- }
- val sizeAnimation = remember {
- DeferredAnimation(coroutineScope, IntSize.VectorConverter)
- }
- val outerSizeAnimation = remember {
- DeferredAnimation(coroutineScope, IntSize.VectorConverter)
- }
+ val outerOffsetAnimation = remember { DeferredAnimation(IntOffset.VectorConverter) }
+ val outerSizeAnimation = remember { DeferredAnimation(IntSize.VectorConverter) }
+
+ val offsetAnimation = remember { DeferredAnimation(IntOffset.VectorConverter) }
+ val sizeAnimation = remember { DeferredAnimation(IntSize.VectorConverter) }
+
// The measure logic in `intermediateLayout` is skipped in the lookahead pass, as
// intermediateLayout is expected to produce intermediate stages of a layout transform.
// When the measure block is invoked after lookahead pass, the lookahead size of the
// child will be accessible as a parameter to the measure block.
this
- .intermediateLayout { measurable, constraints, lookaheadSize ->
- outerSizeAnimation.updateTarget(lookaheadSize, sizeAnimationSpec)
- val (w, h) = outerSizeAnimation.value ?: lookaheadSize
+ .intermediateLayout { measurable, constraints ->
+ val (w, h) = outerSizeAnimation.updateTarget(
+ lookaheadSize,
+ sizeAnimationSpec,
+ )
measurable
.measure(constraints)
.run {
layout(w, h) {
- place(0, 0)
+ val (x, y) = outerOffsetAnimation.updateTargetBasedOnCoordinates(
+ positionAnimationSpec
+ )
+ place(x, y)
}
}
}
.then(modifier)
- .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
- // This block of code has the LookaheadCoordinates of the LookaheadLayout
- // as the first parameter, and the coordinates of this modifier as the second
- // parameter.
-
- // localLookaheadPositionOf returns the *target* position of this
- // modifier in the LookaheadLayout's local coordinates.
- val targetOffset = lookaheadScopeCoordinates
- .localLookaheadPositionOf(
- layoutCoordinates
- )
- .round()
- offsetAnimation.updateTarget(targetOffset, positionAnimationSpec)
-
- // localPositionOf returns the *current* position of this
- // modifier in the LookaheadLayout's local coordinates.
- placementOffset = lookaheadScopeCoordinates
- .localPositionOf(
- layoutCoordinates, Offset.Zero
- )
- .round()
- }
- .intermediateLayout { measurable, _, lookaheadSize ->
+ .intermediateLayout { measurable, _ ->
// When layout changes, the lookahead pass will calculate a new final size for the
// child modifier. This lookahead size can be used to animate the size
// change, such that the animation starts from the current size and gradually
// change towards `lookaheadSize`.
- sizeAnimation.updateTarget(lookaheadSize, sizeAnimationSpec)
- // Reads the animation size if the animation is set up. Otherwise (i.e. first
- // frame), use the lookahead size without animation.
- val (width, height) = sizeAnimation.value ?: lookaheadSize
+ val (width, height) = sizeAnimation.updateTarget(
+ lookaheadSize,
+ sizeAnimationSpec,
+ )
// Creates a fixed set of constraints using the animated size
val animatedConstraints = Constraints.fixed(width, height)
// Measure child/children with animated constraints.
val placeable = measurable.measure(animatedConstraints)
layout(placeable.width, placeable.height) {
- // offsetAnimation will animate the target position whenever it changes.
- // In order to place the child at the animated position, we need to offset
- // the child based on the target and current position in LookaheadLayout.
- val (x, y) = offsetAnimation.value?.let { it - placementOffset }
- // If offsetAnimation has not been set up yet (i.e. in the first frame),
- // skip the animation
- ?: (offsetAnimation.target!! - placementOffset)
+ val (x, y) = with(lookaheadScope(this@intermediateLayout)) {
+ offsetAnimation.updateTargetBasedOnCoordinates(
+ positionAnimationSpec,
+ )
+ }
placeable.place(x, y)
}
}
}
+context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
+ internal fun DeferredAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+ animationSpec: FiniteAnimationSpec<IntOffset>,
+): IntOffset {
+ coordinates?.let { coordinates ->
+ with(this@PlacementScope) {
+ val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+ val animOffset = updateTarget(
+ targetOffset.round(),
+ animationSpec,
+ )
+ val current = lookaheadScopeCoordinates.localPositionOf(
+ coordinates,
+ Offset.Zero
+ ).round()
+ return (animOffset - current)
+ }
+ }
+
+ return IntOffset.Zero
+}
+
// Experimenting with a way to initialize animation during measurement && only take the last target
// change in a frame (if the target was changed multiple times in the same frame) as the
// animation target.
internal class DeferredAnimation<T, V : AnimationVector>(
- coroutineScope: CoroutineScope,
- vectorConverter: TwoWayConverter<T, V>
+ private val vectorConverter: TwoWayConverter<T, V>
) {
val value: T?
get() = animatable?.value ?: target
var target: T? by mutableStateOf(null)
private set
- private var animationSpec: FiniteAnimationSpec<T> = spring()
private var animatable: Animatable<T, V>? = null
- init {
- coroutineScope.launch {
- snapshotFlow { target }.collect { target ->
- if (target != null && target != animatable?.targetValue) {
- animatable?.run {
- launch { animateTo(target, animationSpec) }
- } ?: Animatable(target, vectorConverter).let {
- animatable = it
- }
+ internal val isActive: Boolean
+ get() = target != animatable?.targetValue || animatable?.isRunning == true
+
+ context (CoroutineScope)
+ fun updateTarget(
+ targetValue: T,
+ animationSpec: FiniteAnimationSpec<T>,
+ ): T {
+ target = targetValue
+ if (target != null && target != animatable?.targetValue) {
+ animatable?.run {
+ launch {
+ animateTo(
+ targetValue,
+ animationSpec
+ )
}
+ } ?: Animatable(targetValue, vectorConverter).let {
+ animatable = it
}
}
+ return animatable?.value ?: targetValue
}
-
- fun updateTarget(targetValue: T, animationSpec: FiniteAnimationSpec<T>) {
- target = targetValue
- this.animationSpec = animationSpec
- }
-}
\ No newline at end of file
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt
index 2f89ffb..789d3f4 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/AnimateBoundsModifierDemo.kt
@@ -35,10 +35,8 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.LookaheadLayout
-import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.unit.dp
-import java.lang.Integer.max
import kotlin.random.Random
@OptIn(ExperimentalComposeUiApi::class)
@@ -63,71 +61,57 @@
mutableStateOf(2f)
}
- LookaheadLayout(
- modifier = Modifier
- .fillMaxSize()
- .clickable {
- height = Random.nextInt(10, 300)
- weight = Random
- .nextDouble(0.5, 4.5)
- .toFloat()
+ LookaheadScope {
+ Column(
+ Modifier
+ .fillMaxSize()
+ .clickable {
+ height = Random.nextInt(10, 300)
+ weight = Random
+ .nextDouble(0.5, 4.5)
+ .toFloat()
- left = Random.nextInt(0, 200)
- top = Random.nextInt(0, 100)
- right = Random.nextInt(0, 200)
- bottom = Random.nextInt(0, 100)
- },
- content = {
- Column {
+ left = Random.nextInt(0, 200)
+ top = Random.nextInt(0, 100)
+ right = Random.nextInt(0, 200)
+ bottom = Random.nextInt(0, 100)
+ }
+ ) {
+ Box(
+ Modifier
+ .fillMaxHeight(0.5f)
+ .fillMaxSize()
+ ) {
Box(
Modifier
- .fillMaxHeight(0.5f)
+ .background(Color.Gray)
+ .animateBounds(
+ Modifier.padding(left.dp, top.dp, right.dp, bottom.dp)
+ )
+ .background(Color.Red)
.fillMaxSize()
- ) {
- Box(
- Modifier
- .background(Color.Gray)
- .animateBounds(
- Modifier.padding(left.dp, top.dp, right.dp, bottom.dp)
- )
- .background(Color.Red)
- .fillMaxSize()
- )
- }
- Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
- Box(
- Modifier
- .animateBounds(
- Modifier
- .weight(weight)
- .height(height.dp)
- )
- .background(Color(0xffa2d2ff), RoundedCornerShape(5.dp))
- )
- Box(
- Modifier
- .animateBounds(
- Modifier
- .weight(1f)
- .height(height.dp)
- )
- .background(Color(0xfffff3b0))
- )
- }
+ )
}
- }, measurePolicy = lookaheadMeasurePolicy
- )
-}
-
-internal val lookaheadMeasurePolicy = MeasurePolicy { measurables, constraints ->
- val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
- val placeables = measurables.map { it.measure(contentConstraints) }
- val maxWidth: Int = max(placeables.maxOf { it.width }, constraints.minWidth)
- val maxHeight = max(placeables.maxOf { it.height }, constraints.minHeight)
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
+ Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
+ Box(
+ Modifier
+ .animateBounds(
+ Modifier
+ .weight(weight)
+ .height(height.dp)
+ )
+ .background(Color(0xffa2d2ff), RoundedCornerShape(5.dp))
+ )
+ Box(
+ Modifier
+ .animateBounds(
+ Modifier
+ .weight(1f)
+ .height(height.dp)
+ )
+ .background(Color(0xfffff3b0))
+ )
+ }
}
}
}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
index cbf1466..4754294 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
@@ -20,15 +20,19 @@
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
@@ -40,18 +44,32 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.IntermediateMeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.unit.toSize
+import kotlin.math.roundToInt
@Composable
fun CraneDemo() {
+ val progressProvider = remember { ProgressProviderImpl(false) }
val avatar = remember {
movableContentWithReceiverOf<SceneScope> {
Box(
Modifier
- .sharedElement()
+ .sharedElementBasedOnProgress(progressProvider)
.background(Color(0xffff6f69), RoundedCornerShape(20))
.fillMaxSize()
)
@@ -62,7 +80,7 @@
movableContentWithReceiverOf<SceneScope, @Composable () -> Unit> { child ->
Surface(
modifier = Modifier
- .sharedElement()
+ .sharedElementBasedOnProgress(progressProvider)
.background(Color(0xfffdedac)),
color = Color(0xfffdedac),
shape = RoundedCornerShape(10.dp)
@@ -72,16 +90,14 @@
}
}
- var fullScreen by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
- .padding(10.dp)
- .clickable { fullScreen = !fullScreen },
+ .padding(10.dp),
contentAlignment = Alignment.Center
) {
SceneHost(Modifier.fillMaxSize()) {
- if (fullScreen) {
+ if (progressProvider.targetState) {
Box(Modifier.offset(100.dp, 150.dp)) {
parent {
Box(
@@ -98,7 +114,7 @@
parent {
Column(Modifier.fillMaxSize()) {
val alpha = produceState(0f) {
- animate(0f, 1f, animationSpec = tween(200)) { value, _ ->
+ animate(0f, 1f, animationSpec = tween(2000)) { value, _ ->
this.value = value
}
}
@@ -122,5 +138,86 @@
}
}
}
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .width(96.dp)
+ .background(Color(0x88CCCCCC))
+ .align(Alignment.CenterEnd)
+ .draggable(
+ rememberDraggableState(onDelta = {
+ progressProvider.progress =
+ (-it / 300f + progressProvider.progress).coerceIn(0f, 1f)
+ }),
+ onDragStarted = {
+ progressProvider.targetState = !progressProvider.targetState
+ progressProvider.progress = 0f
+ },
+ onDragStopped = {
+ with(progressProvider) {
+ if (progress < 0.5f) {
+ targetState = initialState
+ } else {
+ initialState = targetState
+ }
+ progressProvider.progress = 1f
+ }
+ },
+ orientation = Orientation.Horizontal
+ )
+ )
+ }
+}
+
+class ProgressProviderImpl<T>(initialState: T) : ProgressProvider<T> {
+ override var initialState: T by mutableStateOf(initialState)
+ override var targetState: T by mutableStateOf(initialState)
+ override var progress: Float by mutableStateOf(0f)
+}
+
+interface ProgressProvider<T> {
+ val initialState: T
+ val targetState: T
+ val progress: Float
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun <T> Modifier.sharedElementBasedOnProgress(provider: ProgressProvider<T>) = composed {
+ val sizeMap = remember { mutableMapOf<T, IntSize>() }
+ val offsetMap = remember { mutableMapOf<T, Offset>() }
+ val calculateSize: (IntSize) -> IntSize =
+ {
+ sizeMap[provider.targetState] = it
+ val (width, height) = lerp(
+ sizeMap[provider.initialState]!!.toSize(),
+ sizeMap[provider.targetState]!!.toSize(), provider.progress
+ )
+ IntSize(width.roundToInt(), height.roundToInt())
+ }
+
+ val calculateOffset: Placeable.PlacementScope.(IntermediateMeasureScope) -> IntOffset = {
+ with(it) {
+ coordinates?.let {
+ offsetMap[provider.targetState] =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(
+ it
+ )
+ val lerpedOffset = lerp(
+ offsetMap[provider.initialState]!!,
+ offsetMap[provider.targetState]!!,
+ provider.progress
+ )
+ val currentOffset = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
+ (lerpedOffset - currentOffset).round()
+ } ?: IntOffset(0, 0)
+ }
+ }
+ this.intermediateLayout { measurable, _ ->
+ val (width, height) = calculateSize(lookaheadSize)
+ val animatedConstraints = Constraints.fixed(width, height)
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(calculateOffset(this@intermediateLayout))
+ }
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt
new file mode 100644
index 0000000..2e9048a
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadSamplesDemo.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.samples.IntermediateLayoutSample
+import androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
+
+@Composable
+fun LookaheadSamplesDemo() {
+ Column {
+ IntermediateLayoutSample()
+ LookaheadLayoutCoordinatesSample()
+ }
+}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
new file mode 100644
index 0000000..7efce9a
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalComposeUiApi::class)
+
+package androidx.compose.animation.demos.lookahead
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.delay
+
+@Composable
+fun LookaheadWithDisappearingMovableContentDemo() {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .padding(start = 50.dp, top = 200.dp)
+ ) {
+ LookaheadScope {
+ val icon = remember {
+ movableContentOf<Boolean> {
+ MyIcon(it, Modifier.animatePosition())
+ }
+ }
+ val title = remember {
+ movableContentOf<Boolean> {
+ Title(visible = it, Modifier.animatePosition())
+ }
+ }
+ val details = remember {
+ movableContentOf<Boolean> {
+ Details(visible = it, Modifier.animatePosition())
+ }
+ }
+
+ val isCompact by produceState(initialValue = false) {
+ while (true) {
+ delay(2000)
+ value = !value
+ }
+ }
+ Row(Modifier.background(Color.Yellow), verticalAlignment = Alignment.CenterVertically) {
+ if (isCompact) {
+ icon(true)
+ Column {
+ title(true)
+ details(true)
+ }
+ } else {
+ icon(false)
+ Column {
+ title(true)
+ details(false)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MyIcon(visible: Boolean, modifier: Modifier = Modifier) {
+ AV2(visible, modifier) {
+ Box(
+ modifier
+ .size(40.dp)
+ .background(color = Color.Red, CircleShape)
+ )
+ }
+}
+
+@Composable
+fun Title(visible: Boolean, modifier: Modifier = Modifier) {
+ AV2(visible, modifier) {
+ Text("Text", modifier, fontSize = 30.sp)
+ }
+}
+
+@Composable
+fun Details(visible: Boolean, modifier: Modifier = Modifier) {
+ AV2(visible, modifier) {
+ Text("Detailed Text", fontSize = 18.sp)
+ }
+}
+
+@Composable
+fun AV2(visible: Boolean, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ AnimatedVisibility(
+ visible, enter = fadeIn(), exit = fadeOut(), modifier = modifier.then(
+ if (!visible) Modifier
+ .size(0.dp)
+ .wrapContentSize() else Modifier
+ )
+ ) {
+ content()
+ }
+}
+
+context(LookaheadScope)
+ @SuppressLint("UnnecessaryComposedModifier")
+fun Modifier.animatePosition(): Modifier = composed {
+ val offsetAnimation = remember {
+ DeferredAnimation(IntOffset.VectorConverter)
+ }
+ this.intermediateLayout { measurable, constraints ->
+ measurable.measure(constraints).run {
+ layout(width, height) {
+ val (x, y) =
+ coordinates?.let { coordinates ->
+ offsetAnimation.updateTarget(
+ lookaheadScopeCoordinates.localLookaheadPositionOf(
+ coordinates
+ )
+ .round(),
+ spring(),
+ )
+ val currentOffset =
+ lookaheadScopeCoordinates.localPositionOf(
+ coordinates,
+ Offset.Zero
+ )
+ (offsetAnimation.value
+ ?: offsetAnimation.target!!) - currentOffset.round()
+ } ?: IntOffset.Zero
+ place(x, y)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt
index 8a767c5..1ef7ba1 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithFlowRowDemo.kt
@@ -40,7 +40,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.LookaheadLayout
+import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
@@ -55,48 +55,51 @@
horizontalAlignment = Alignment.CenterHorizontally
) {
var isHorizontal by remember { mutableStateOf(true) }
+
+ Button(modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
+ onClick = { isHorizontal = !isHorizontal }) {
+ Text("Toggle")
+ }
Column(
Modifier
.background(Color(0xfffdedac), RoundedCornerShape(10))
.padding(10.dp)
) {
Text("LookaheadLayout + Modifier.animateBounds")
- LookaheadLayout(
- measurePolicy = lookaheadMeasurePolicy,
- content = {
- MyFlowRow(
- modifier = Modifier
- .height(200.dp)
- .fillMaxWidth()
- .wrapContentSize(Alignment.CenterStart)
- ) {
- Box(
- Modifier
- .height(50.dp)
- .animateBounds(
- Modifier.fillMaxWidth(if (isHorizontal) 0.4f else 1f)
- )
- .background(colors[0], RoundedCornerShape(10))
- )
- Box(
- Modifier
- .height(50.dp)
- .animateBounds(
- Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f)
- )
- .background(colors[1], RoundedCornerShape(10))
- )
- Box(
- Modifier
- .height(50.dp)
- .animateBounds(
- Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f)
- )
- .background(colors[2], RoundedCornerShape(10))
- )
- }
- Box(Modifier.size(if (isHorizontal) 200.dp else 100.dp))
- })
+ LookaheadScope {
+ MyFlowRow(
+ modifier = Modifier
+ .height(200.dp)
+ .fillMaxWidth()
+ .wrapContentSize(Alignment.CenterStart)
+ ) {
+ Box(
+ Modifier
+ .height(50.dp)
+ .animateBounds(
+ Modifier.fillMaxWidth(if (isHorizontal) 0.4f else 1f)
+ )
+ .background(colors[0], RoundedCornerShape(10))
+ )
+ Box(
+ Modifier
+ .height(50.dp)
+ .animateBounds(
+ Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f)
+ )
+ .background(colors[1], RoundedCornerShape(10))
+ )
+ Box(
+ Modifier
+ .height(50.dp)
+ .animateBounds(
+ Modifier.fillMaxWidth(if (isHorizontal) 0.2f else 0.4f)
+ )
+ .background(colors[2], RoundedCornerShape(10))
+ )
+ }
+ Box(Modifier.size(if (isHorizontal) 100.dp else 60.dp))
+ }
}
Spacer(Modifier.size(50.dp))
@@ -133,11 +136,6 @@
)
}
}
-
- Button(modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
- onClick = { isHorizontal = !isHorizontal }) {
- Text("Toggle")
- }
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt
new file mode 100644
index 0000000..58dce48
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithIntrinsicsDemo.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun LookaheadWithIntrinsicsDemo() {
+ Column {
+ LookaheadScope {
+ var isWide by remember { mutableStateOf(true) }
+ Column {
+ Button(modifier = Modifier.padding(top = 20.dp, bottom = 20.dp),
+ onClick = { isWide = !isWide }) {
+ Text("Toggle")
+ }
+ Text("IntrinsicSize.Min Column")
+ Spacer(Modifier.size(5.dp))
+ Column(
+ Modifier
+ .background(Color(0xfffdedac), RoundedCornerShape(10))
+ .padding(20.dp)
+ .width(IntrinsicSize.Min)
+ ) {
+ Box(
+ Modifier
+ .animateBounds(
+ if (isWide) Modifier.width(300.dp) else Modifier.width(150.dp)
+ )
+ .height(50.dp)
+ .background(colors[1])
+ ) {
+ Text("Width: ${if (isWide) 300 else 150}.dp")
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ .background(colors[2])
+ ) {
+ Text("Match parent")
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ .background(colors[3])
+ ) {
+ Text("Match parent", color = Color.White)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MatchParentDividerForText() {
+ // Builds a layout containing two pieces of text separated by a divider, where the divider
+ // is sized according to the height of the longest text.
+ //
+ // Here height min intrinsic is adding a height premeasurement pass for the Row,
+ // whose minimum intrinsic height will correspond to the height of the largest Text. Then
+ // height min intrinsic will measure the Row with tight height, the same as the
+ // premeasured minimum intrinsic height, which due to fillMaxHeight will force the Texts and
+ // the divider to use the same height.
+ Box {
+ Row(Modifier.height(IntrinsicSize.Min)) {
+ Text(
+ text = "This is a really short text",
+ modifier = Modifier
+ .fillMaxHeight()
+ )
+ Box(
+ Modifier
+ .width(1.dp)
+ .fillMaxHeight()
+ .background(Color.Black))
+ Text(
+ text = "This is a much much much much much much much much much much" +
+ " much much much much much much longer text",
+ modifier = Modifier
+ .fillMaxHeight()
+ )
+ }
+ }
+}
+
+@Composable
+fun SameWidthTextBoxes() {
+ // Builds a layout containing three Text boxes having the same width as the widest one.
+ //
+ // Here width max intrinsic is adding a width premeasurement pass for the Column,
+ // whose maximum intrinsic width will correspond to the preferred width of the largest
+ // Box. Then width max intrinsic will measure the Column with tight width, the
+ // same as the premeasured maximum intrinsic width, which due to fillMaxWidth modifiers will
+ // force the Boxs to use the same width.
+
+ Box {
+ Column(
+ Modifier
+ .width(IntrinsicSize.Min)
+ .fillMaxHeight()) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .background(Color.Gray)) {
+ Text("Short text")
+ }
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .background(Color.Blue)) {
+ Text("Extremely long text giving the width of its siblings")
+ }
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .background(Color.Magenta)) {
+ Text("Medium length text")
+ }
+ }
+ }
+}
+
+@Composable
+fun MatchParentDividerForAspectRatio() {
+ // Builds a layout containing two aspectRatios separated by a divider, where the divider
+ // is sized according to the height of the taller aspectRatio.
+ //
+ // Here height max intrinsic is adding a height premeasurement pass for the
+ // Row, whose maximum intrinsic height will correspond to the height of the taller
+ // aspectRatio. Then height max intrinsic will measure the Row with tight height,
+ // the same as the premeasured maximum intrinsic height, which due to fillMaxHeight modifier
+ // will force the aspectRatios and the divider to use the same height.
+ //
+ Box {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Max)) {
+ val modifier = Modifier
+ Box(
+ modifier
+ .width(160.dp)
+ .aspectRatio(2f)
+ .background(Color.Gray))
+ Box(
+ Modifier
+ .width(1.dp)
+ .fillMaxHeight()
+ .background(Color.Black))
+ Box(
+ modifier
+ .widthIn(120.dp, 200.dp)
+ .aspectRatio(1f)
+ .background(Color.Blue))
+ }
+ }
+}
+
+private val colors = listOf(
+ Color(0xffff6f69),
+ Color(0xffffcc5c),
+ Color(0xff2a9d84),
+ Color(0xff264653)
+)
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
index cb5c2e0..3100d36 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
@@ -14,8 +14,12 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalComposeUiApi::class)
+
package androidx.compose.animation.demos.lookahead
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
import androidx.compose.animation.demos.statetransition.InfiniteProgress
import androidx.compose.animation.demos.statetransition.InfinitePulsingHeart
import androidx.compose.animation.demos.fancy.AnimatedDotsDemo
@@ -39,10 +43,19 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
@Composable
fun LookaheadWithMovableContentDemo() {
@@ -52,7 +65,9 @@
var isSingleColumn by remember { mutableStateOf(true) }
Column(
- Modifier.padding(100.dp).fillMaxWidth(),
+ Modifier
+ .padding(100.dp)
+ .fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(modifier = Modifier.clickable {
@@ -71,11 +86,13 @@
val items = remember {
colors.mapIndexed { id, color ->
- movableContentWithReceiverOf<SceneScope, Float> { weight ->
+ movableContentWithReceiverOf<LookaheadScope, Float> { weight ->
Box(
- Modifier.padding(15.dp).height(80.dp)
+ Modifier
+ .padding(15.dp)
+ .height(80.dp)
.fillMaxWidth(weight)
- .sharedElement()
+ .animateBoundsInScope()
.background(color, RoundedCornerShape(20)),
contentAlignment = Alignment.Center
) {
@@ -88,36 +105,43 @@
}) {
AnimatedDotsDemo()
}
+
2 -> Box(Modifier.graphicsLayer {
scaleX = 0.5f
scaleY = 0.5f
}) { InfinitePulsingHeart() }
+
else -> InfiniteProgress()
}
}
}
}
}
- SceneHost(Modifier.fillMaxSize()) {
- if (isSingleColumn) {
- Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
- items.forEach {
- it(0.8f)
- }
- }
- } else {
- Row {
- Column(Modifier.weight(1f)) {
- items.forEachIndexed { id, item ->
- if (id % 2 == 0) {
- item(1f)
- }
+ Box(Modifier.fillMaxSize()) {
+ LookaheadScope {
+ if (isSingleColumn) {
+ Column(
+ Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ items.forEach {
+ it(0.8f)
}
}
- Column(Modifier.weight(1f)) {
- items.forEachIndexed { id, item ->
- if (id % 2 != 0) {
- item(1f)
+ } else {
+ Row {
+ Column(Modifier.weight(1f)) {
+ items.forEachIndexed { id, item ->
+ if (id % 2 == 0) {
+ item(1f)
+ }
+ }
+ }
+ Column(Modifier.weight(1f)) {
+ items.forEachIndexed { id, item ->
+ if (id % 2 != 0) {
+ item(1f)
+ }
}
}
}
@@ -127,6 +151,40 @@
}
}
+context (LookaheadScope)
+fun Modifier.animateBoundsInScope(): Modifier = composed {
+ val sizeAnim = remember { DeferredAnimation(IntSize.VectorConverter) }
+ val offsetAnim = remember { DeferredAnimation(IntOffset.VectorConverter) }
+ this.intermediateLayout { measurable, _ ->
+ sizeAnim.updateTarget(
+ lookaheadSize,
+ spring()
+ )
+ measurable.measure(
+ Constraints.fixed(
+ sizeAnim.value!!.width,
+ sizeAnim.value!!.height
+ )
+ )
+ .run {
+ layout(width, height) {
+ coordinates?.let {
+ val target =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it)
+ .round()
+ offsetAnim.updateTarget(target, spring())
+ val current = lookaheadScopeCoordinates.localPositionOf(
+ it,
+ Offset.Zero
+ ).round()
+ val (x, y) = offsetAnim.value!! - current
+ place(x, y)
+ } ?: place(0, 0)
+ }
+ }
+ }
+}
+
private val colors = listOf(
Color(0xffff6f69),
Color(0xffffcc5c),
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/NestedSceneHostDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/NestedSceneHostDemo.kt
index 5c73e52..543ff01 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/NestedSceneHostDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/NestedSceneHostDemo.kt
@@ -28,25 +28,31 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.unit.dp
@Composable
fun NestedSceneHostDemo() {
SceneHost {
- Box(Modifier.padding(top = 100.dp).fillMaxSize()
- .intermediateLayout { measurable, constraints, _ ->
- println("SPEC, actually measure parent")
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- println("SPEC, actually place parent")
- placeable.place(0, 0)
- }
- }) {
+ Box(
+ Modifier
+ .padding(top = 100.dp)
+ .fillMaxSize()
+ .intermediateLayout { measurable, constraints ->
+ println("SPEC, actually measure parent")
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ println("SPEC, actually place parent")
+ placeable.place(0, 0)
+ }
+ }) {
SceneHost {
Column {
Box(
- Modifier.size(100.dp).background(Color.Red)
- .intermediateLayout { measurable, constraints, _ ->
+ Modifier
+ .size(100.dp)
+ .background(Color.Red)
+ .intermediateLayout { measurable, constraints ->
println("SPEC, actually measure child")
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
@@ -55,7 +61,9 @@
}
})
Box(
- Modifier.size(100.dp).background(Color.Green)
+ Modifier
+ .size(100.dp)
+ .background(Color.Green)
)
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
index de389a3..2e70edb 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
@@ -20,14 +20,15 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -35,8 +36,8 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.layout.LookaheadLayout
-import androidx.compose.ui.layout.LookaheadLayoutScope
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -47,148 +48,96 @@
@Composable
fun SceneHost(modifier: Modifier = Modifier, content: @Composable SceneScope.() -> Unit) {
- LookaheadLayout(
- modifier = modifier,
- content = {
+ Box(modifier) {
+ LookaheadScope {
val sceneScope = remember { SceneScope(this) }
sceneScope.content()
- },
- measurePolicy = { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
- }
- }
- })
+ }
+ }
}
-private const val debugSharedElement = false
+private const val debugSharedElement = true
class SceneScope internal constructor(
- lookaheadLayoutScope: LookaheadLayoutScope
-) : LookaheadLayoutScope by lookaheadLayoutScope {
+ lookaheadScope: LookaheadScope
+) : LookaheadScope by lookaheadScope {
fun Modifier.sharedElement(): Modifier = composed {
- var offsetAnimation: Animatable<IntOffset, AnimationVector2D>?
- by remember { mutableStateOf(null) }
- var sizeAnimation: Animatable<IntSize, AnimationVector2D>?
- by remember { mutableStateOf(null) }
+ val offsetAnimation: DeferredAnimation<IntOffset, AnimationVector2D> =
+ remember {
+ DeferredAnimation(IntOffset.VectorConverter)
+ }
+ val sizeAnimation: DeferredAnimation<IntSize, AnimationVector2D> =
+ remember { DeferredAnimation(IntSize.VectorConverter) }
var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) }
- var targetOffset: IntOffset? by remember {
- mutableStateOf(null)
- }
- var targetSize: IntSize? by remember {
- mutableStateOf(null)
- }
- LaunchedEffect(Unit) {
- launch {
- snapshotFlow {
- targetOffset
- }.collect { target ->
- if (target != null && target != offsetAnimation?.targetValue) {
- offsetAnimation?.run {
- launch { animateTo(target) }
- } ?: Animatable(target, IntOffset.VectorConverter).let {
- offsetAnimation = it
- }
- }
- }
- }
- launch {
- snapshotFlow {
- targetSize
- }.collect { target ->
- if (target != null && target != sizeAnimation?.targetValue) {
- sizeAnimation?.run {
- launch { animateTo(target) }
- } ?: Animatable(target, IntSize.VectorConverter).let {
- sizeAnimation = it
- }
- }
- }
- }
- }
this
- .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
- targetOffset =
- lookaheadScopeCoordinates.localLookaheadPositionOf(layoutCoordinates).round()
- placementOffset = lookaheadScopeCoordinates.localPositionOf(
- layoutCoordinates, Offset.Zero
- ).round()
- }
.drawBehind {
if (debugSharedElement) {
drawRect(
color = Color.Black,
style = Stroke(2f),
- topLeft = (targetOffset!! - placementOffset).toOffset(),
- size = targetSize!!.toSize()
+ topLeft = (offsetAnimation.target!! - placementOffset).toOffset(),
+ size = sizeAnimation.target!!.toSize()
)
}
}
- .intermediateLayout { measurable, _, lookaheadSize ->
- targetSize = lookaheadSize
- val (width, height) = sizeAnimation?.value ?: lookaheadSize
- val animatedConstraints = Constraints(
- minWidth = width,
- maxWidth = width,
- minHeight = height,
- maxHeight = height
+ .intermediateLayout { measurable, _ ->
+ val (width, height) = sizeAnimation.updateTarget(
+ lookaheadSize, spring(stiffness = Spring.StiffnessMediumLow)
)
+ val animatedConstraints = Constraints.fixed(width, height)
val placeable = measurable.measure(animatedConstraints)
layout(placeable.width, placeable.height) {
- val (x, y) = offsetAnimation?.run {
- value - placementOffset
- } ?: (targetOffset!! - placementOffset)
+ val (x, y) = offsetAnimation.updateTargetBasedOnCoordinates(
+ spring(stiffness = Spring.StiffnessMediumLow),
+ )
+ coordinates?.let {
+ placementOffset = lookaheadScopeCoordinates.localPositionOf(
+ it, Offset.Zero
+ ).round()
+ }
placeable.place(x, y)
}
}
}
+}
- fun Modifier.animateSizeAndSkipToFinalLayout() = composed {
- var sizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember {
- mutableStateOf(null)
- }
- var targetSize: IntSize? by remember { mutableStateOf(null) }
- LaunchedEffect(Unit) {
- snapshotFlow { targetSize }.collect { target ->
- if (target != null && target != sizeAnimation?.targetValue) {
- sizeAnimation?.run {
- launch { animateTo(target) }
- } ?: Animatable(target, IntSize.VectorConverter).let {
- sizeAnimation = it
- }
- }
- }
- }
- this
- .drawBehind {
- if (debugSharedElement) {
- drawRect(
- color = Color.Black,
- style = Stroke(2f),
- topLeft = Offset.Zero,
- size = targetSize!!.toSize()
- )
- }
- }
- .intermediateLayout { measurable, constraints, lookaheadSize ->
- targetSize = lookaheadSize
- val (width, height) = sizeAnimation?.value ?: lookaheadSize
- val placeable = measurable.measure(
- Constraints.fixed(lookaheadSize.width, lookaheadSize.height)
- )
- // Make sure the content is aligned to topStart
- val wrapperWidth = width.coerceIn(constraints.minWidth, constraints.maxWidth)
- val wrapperHeight = height.coerceIn(constraints.minHeight, constraints.maxHeight)
- layout(width, height) {
- placeable.place(-(wrapperWidth - width) / 2, -(wrapperHeight - height) / 2)
- }
- }
+fun Modifier.animateSizeAndSkipToFinalLayout() = composed {
+ var sizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember {
+ mutableStateOf(null)
}
+ var targetSize: IntSize? by remember { mutableStateOf(null) }
+ this
+ .drawBehind {
+ if (debugSharedElement) {
+ drawRect(
+ color = Color.Black,
+ style = Stroke(2f),
+ topLeft = Offset.Zero,
+ size = targetSize!!.toSize()
+ )
+ }
+ }
+ .intermediateLayout { measurable, constraints ->
+ targetSize = lookaheadSize
+ if (lookaheadSize != sizeAnimation?.targetValue) {
+ sizeAnimation?.run {
+ launch { animateTo(lookaheadSize) }
+ } ?: Animatable(lookaheadSize, IntSize.VectorConverter).let {
+ sizeAnimation = it
+ }
+ }
+ val (width, height) = sizeAnimation?.value ?: lookaheadSize
+ val placeable = measurable.measure(
+ Constraints.fixed(lookaheadSize.width, lookaheadSize.height)
+ )
+ // Make sure the content is aligned to topStart
+ val wrapperWidth = width.coerceIn(constraints.minWidth, constraints.maxWidth)
+ val wrapperHeight =
+ height.coerceIn(constraints.minHeight, constraints.maxHeight)
+ layout(width, height) {
+ placeable.place(-(wrapperWidth - width) / 2, -(wrapperHeight - height) / 2)
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt
index a62bf4d..86f48ef 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/ScreenSizeChangeDemo.kt
@@ -57,6 +57,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
@@ -142,6 +143,7 @@
}
}
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun Root(state: DisplayState) {
SceneHost {
@@ -357,6 +359,7 @@
}
}
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SceneScope.NavRail(state: DisplayState) {
Column(
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index 742fcbe..ba5bb31 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -943,7 +943,6 @@
private val DefaultAlpha = mutableStateOf(1f)
private val DefaultAlphaAndScaleSpring = spring<Float>(stiffness = Spring.StiffnessMediumLow)
-@Suppress("ModifierInspectorInfo")
private fun Modifier.slideInOut(
transition: Transition<EnterExitState>,
slideIn: State<Slide?>,
@@ -1025,7 +1024,6 @@
}
}
-@Suppress("ModifierInspectorInfo")
private fun Modifier.shrinkExpand(
transition: Transition<EnterExitState>,
expand: State<ChangeSize?>,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 3c99146..dd33cd1 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -450,12 +450,12 @@
traceEventStart(<>, %changed, -1, <>)
}
A(%composer, 0)
- val tmp0_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
A(%composer, 0)
if (condition) {
- %composer.endToMarker(tmp0_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
A(%composer, 0)
sourceInformationMarkerEnd(%composer)
@@ -504,6 +504,7 @@
if (isTraceInProgress()) {
traceEventEnd()
}
+ return
}
sourceInformationMarkerEnd(%composer)
}, %composer, 0)
@@ -570,11 +571,11 @@
M3({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C<A()>,<M1>,<A()>:Test.kt")
A(%composer, 0)
- val tmp0_marker = %composer.currentMarker
M1({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
if (condition) {
- %composer.endToMarker(tmp0_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
sourceInformationMarkerEnd(%composer)
}, %composer, 0)
@@ -627,14 +628,15 @@
traceEventStart(<>, %changed, -1, <>)
}
A(%composer, 0)
- val tmp0_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
+ val tmp0_marker = %composer.currentMarker
sourceInformationMarkerStart(%composer, <>, "C<A()>,<M1>,<A()>:Test.kt")
A(%composer, 0)
M1({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
if (condition) {
%composer.endToMarker(tmp0_marker)
+ return
}
sourceInformationMarkerEnd(%composer)
}, %composer, 0)
@@ -766,22 +768,22 @@
traceEventStart(<>, %changed, -1, <>)
}
A(%composer, 0)
- val tmp0_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
A(%composer, 0)
if (condition) {
- %composer.endToMarker(tmp0_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
A(%composer, 0)
sourceInformationMarkerEnd(%composer)
}, %composer, 0)
- val tmp1_marker = %composer.currentMarker
M3({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
A(%composer, 0)
if (condition) {
- %composer.endToMarker(tmp1_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
A(%composer, 0)
sourceInformationMarkerEnd(%composer)
@@ -902,11 +904,11 @@
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
}
- val tmp0_marker = %composer.currentMarker
FakeBox({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
if (condition) {
- %composer.endToMarker(tmp0_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
A(%composer, 0)
sourceInformationMarkerEnd(%composer)
@@ -1075,11 +1077,11 @@
if (isTraceInProgress()) {
traceEventStart(<>, %changed, -1, <>)
}
- val tmp0_marker = %composer.currentMarker
IW({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
if (condition) {
- %composer.endToMarker(tmp0_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
A(%composer, 0)
sourceInformationMarkerEnd(%composer)
@@ -1235,12 +1237,12 @@
traceEventStart(<>, %changed, -1, <>)
}
Text("Some text", %composer, 0b0110)
- val tmp0_marker = %composer.currentMarker
M1({ %composer: Composer?, %changed: Int ->
sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
Identity {
if (condition) {
- %composer.endToMarker(tmp0_marker)
+ sourceInformationMarkerEnd(%composer)
+ return
}
}
sourceInformationMarkerEnd(%composer)
@@ -6142,4 +6144,66 @@
inline fun InlineNonComposable(block: () -> Unit) {}
"""
)
+
+ @Test
+ fun testInlineLambda_nonLocalReturn() = verifyComposeIrTransform(
+ source = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ private fun Test(param: String?) {
+ Inline1 {
+ Inline2 {
+ if (true) return@Inline1
+ }
+ }
+ }
+ """,
+ expectedTransformed = """
+ @Composable
+ private fun Test(param: String?, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<Inline...>:Test.kt")
+ if (%changed and 0b0001 !== 0 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Inline1({ %composer: Composer?, %changed: Int ->
+ val tmp0_marker = %composer.currentMarker
+ sourceInformationMarkerStart(%composer, <>, "C<Inline...>:Test.kt")
+ Inline2({ %composer: Composer?, %changed: Int ->
+ sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+ if (true) {
+ %composer.endToMarker(tmp0_marker)
+ return
+ }
+ sourceInformationMarkerEnd(%composer)
+ }, %composer, 0)
+ sourceInformationMarkerEnd(%composer)
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(param, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """,
+ extra = """
+ import androidx.compose.runtime.*
+
+ @Composable
+ inline fun Inline1(block: @Composable () -> Unit) {
+ block()
+ }
+
+ @Composable
+ inline fun Inline2(block: @Composable () -> Unit) {
+ block()
+ }
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index d35ca47..05236bc 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -1040,10 +1040,17 @@
}
}
+ if (collectSourceInformation && isInlineLambda) {
+ scope.realizeEndCalls {
+ irSourceInformationMarkerEnd(body)
+ }
+ }
+
if (canSkipExecution) {
// We CANNOT skip if any of the following conditions are met
// 1. if any of the stable parameters have *differences* from last execution.
// 2. if the composer.skipping call returns false
+ // 3. function is inline
val shouldExecute = irOrOr(
dirtyForSkipping.irHasDifferences(scope.usedParams),
irNot(irIsSkipping())
@@ -1066,15 +1073,11 @@
body.startOffset,
body.endOffset,
listOfNotNull(
- if (scope.isInlinedLambda)
- irStartReplaceableGroup(body, scope, irFunctionSourceKey())
- else null,
*sourceInformationPreamble.statements.toTypedArray(),
*scope.markerPreamble.statements.toTypedArray(),
*skipPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
transformedBody,
- if (scope.isInlinedLambda) irEndReplaceableGroup() else null,
returnVar?.let { irReturn(declaration.symbol, irGet(it)) }
)
)
@@ -1084,8 +1087,8 @@
body.startOffset,
body.endOffset,
listOfNotNull(
- *sourceInformationPreamble.statements.toTypedArray(),
*scope.markerPreamble.statements.toTypedArray(),
+ *sourceInformationPreamble.statements.toTypedArray(),
*skipPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
transformed,
@@ -2516,13 +2519,7 @@
when (scope) {
is Scope.FunctionScope -> {
if (scope.function == symbol.owner) {
- if (
- !(
- leavingInlinedLambda ||
- (scope.isInlinedLambda && scope.inComposableCall)
- ) ||
- !rollbackGroupMarkerEnabled
- ) {
+ if (!leavingInlinedLambda || !rollbackGroupMarkerEnabled) {
blockScopeMarks.forEach {
it.markReturn(extraEndLocation)
}
@@ -3688,13 +3685,19 @@
fun allocateMarker(): IrVariable = marker ?: run {
val parent = parent
- return (if (isInlinedLambda && parent is Scope.CallScope) {
- parent.allocateMarker()
- } else transformer.irTemporary(
- transformer.irCurrentMarker(myComposer),
- getNameForTemporary("marker")
- ).also { markerPreamble.statements.add(it) }).also {
- marker = it
+ return when {
+ isInlinedLambda && !isComposable && parent is CallScope -> {
+ parent.allocateMarker()
+ }
+ else -> {
+ val newMarker = transformer.irTemporary(
+ transformer.irCurrentMarker(myComposer),
+ getNameForTemporary("marker")
+ )
+ markerPreamble.statements.add(newMarker)
+ marker = newMarker
+ newMarker
+ }
}
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index 9caf245..dd2a8ed 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -774,9 +774,18 @@
// only print the return statement directly if it is not a lambda
val returnTarget = expression.returnTargetSymbol.owner
if (returnTarget !is IrFunction ||
- returnTarget.name.asString() != "<anonymous>" &&
returnTarget.origin != IrDeclarationOrigin.ADAPTER_FOR_CALLABLE_REFERENCE) {
- print("return ")
+
+ val isLastStatementInLambda =
+ returnTarget is IrFunction &&
+ returnTarget.name.asString() == "<anonymous>" &&
+ returnTarget.body?.statements?.last().let {
+ it == expression || (it is IrBlock && it.statements.last() == expression)
+ }
+
+ if (!isLastStatementInLambda) {
+ print("return ")
+ }
}
if (expression.type.isUnit() || value.type.isUnit()) {
if (value is IrGetObjectValue) {
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
index 62833e4..9a9e20d 100644
--- a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
@@ -241,7 +241,7 @@
mandatorySystemGestures
}
-@Suppress("NOTHING_TO_INLINE", "ModifierInspectorInfo")
+@Suppress("NOTHING_TO_INLINE")
@Stable
private inline fun Modifier.windowInsetsPadding(
noinline inspectorInfo: InspectorInfo.() -> Unit,
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt
index 71c9444..a088418 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/AlignmentLine.kt
@@ -144,7 +144,6 @@
* @sample androidx.compose.foundation.layout.samples.PaddingFromBaselineSampleDp
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.paddingFromBaseline(top: Dp = Dp.Unspecified, bottom: Dp = Dp.Unspecified) = this
.then(
if (top != Dp.Unspecified) {
@@ -178,7 +177,6 @@
* @sample androidx.compose.foundation.layout.samples.PaddingFromBaselineSampleTextUnit
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.paddingFromBaseline(
top: TextUnit = TextUnit.Unspecified,
bottom: TextUnit = TextUnit.Unspecified
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index d14e5f6..0d625d7 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -312,12 +312,29 @@
/**
* Box [Constraints], but which abstract away width and height in favor of main axis and cross axis.
*/
-internal data class OrientationIndependentConstraints(
- val mainAxisMin: Int,
- val mainAxisMax: Int,
- val crossAxisMin: Int,
- val crossAxisMax: Int
+@JvmInline
+internal value class OrientationIndependentConstraints private constructor(
+ private val value: Constraints
) {
+ inline val mainAxisMin: Int get() = value.minWidth
+ inline val mainAxisMax: Int get() = value.maxWidth
+ inline val crossAxisMin: Int get() = value.minHeight
+ inline val crossAxisMax: Int get() = value.maxHeight
+
+ constructor(
+ mainAxisMin: Int,
+ mainAxisMax: Int,
+ crossAxisMin: Int,
+ crossAxisMax: Int
+ ) : this(
+ Constraints(
+ minWidth = mainAxisMin,
+ maxWidth = mainAxisMax,
+ minHeight = crossAxisMin,
+ maxHeight = crossAxisMax
+ )
+ )
+
constructor(c: Constraints, orientation: LayoutOrientation) : this(
if (orientation === LayoutOrientation.Horizontal) c.minWidth else c.minHeight,
if (orientation === LayoutOrientation.Horizontal) c.maxWidth else c.maxHeight,
@@ -356,6 +373,19 @@
} else {
mainAxisMax
}
+
+ fun copy(
+ mainAxisMin: Int = this.mainAxisMin,
+ mainAxisMax: Int = this.mainAxisMax,
+ crossAxisMin: Int = this.crossAxisMin,
+ crossAxisMax: Int = this.crossAxisMax
+ ): OrientationIndependentConstraints =
+ OrientationIndependentConstraints(
+ mainAxisMin,
+ mainAxisMax,
+ crossAxisMin,
+ crossAxisMax
+ )
}
internal val IntrinsicMeasurable.rowColumnParentData: RowColumnParentData?
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
index c56f33c..3fd65398 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
@@ -456,7 +456,6 @@
* @sample androidx.compose.foundation.layout.samples.FillHalfWidthModifier
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.fillMaxWidth(/*@FloatRange(from = 0.0, to = 1.0)*/ fraction: Float = 1f) =
this.then(if (fraction == 1f) FillWholeMaxWidth else FillElement.width(fraction))
@@ -477,7 +476,6 @@
* @sample androidx.compose.foundation.layout.samples.FillHalfHeightModifier
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.fillMaxHeight(/*@FloatRange(from = 0.0, to = 1.0)*/ fraction: Float = 1f) =
this.then(if (fraction == 1f) FillWholeMaxHeight else FillElement.height(fraction))
@@ -502,7 +500,6 @@
* @sample androidx.compose.foundation.layout.samples.FillHalfSizeModifier
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.fillMaxSize(/*@FloatRange(from = 0.0, to = 1.0)*/ fraction: Float = 1f) =
this.then(if (fraction == 1f) FillWholeMaxSize else FillElement.size(fraction))
@@ -521,7 +518,6 @@
* @sample androidx.compose.foundation.layout.samples.SimpleWrapContentHorizontallyAlignedModifier
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.wrapContentWidth(
align: Alignment.Horizontal = Alignment.CenterHorizontally,
unbounded: Boolean = false
@@ -551,7 +547,6 @@
* @sample androidx.compose.foundation.layout.samples.SimpleWrapContentVerticallyAlignedModifier
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.wrapContentHeight(
align: Alignment.Vertical = Alignment.CenterVertically,
unbounded: Boolean = false
@@ -581,7 +576,6 @@
* @sample androidx.compose.foundation.layout.samples.SimpleWrapContentAlignedModifier
*/
@Stable
-@Suppress("ModifierInspectorInfo")
fun Modifier.wrapContentSize(
align: Alignment = Alignment.Center,
unbounded: Boolean = false
@@ -608,7 +602,6 @@
* Example usage:
* @sample androidx.compose.foundation.layout.samples.DefaultMinSizeSample
*/
-@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.defaultMinSize(
minWidth: Dp = Dp.Unspecified,
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
index abf9a58..76af273 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
@@ -45,7 +45,6 @@
*
* @sample androidx.compose.foundation.layout.samples.insetsStartWidthSample
*/
-@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsStartWidth(insets: WindowInsets) = this.then(
DerivedWidthModifier(insets, debugInspectorInfo {
@@ -71,7 +70,6 @@
*
* @sample androidx.compose.foundation.layout.samples.insetsEndWidthSample
*/
-@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsEndWidth(insets: WindowInsets) = this.then(
DerivedWidthModifier(insets, debugInspectorInfo {
@@ -95,7 +93,6 @@
*
* @sample androidx.compose.foundation.layout.samples.insetsTopHeightSample
*/
-@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsTopHeight(insets: WindowInsets) = this.then(
DerivedHeightModifier(insets, debugInspectorInfo {
@@ -115,7 +112,6 @@
*
* @sample androidx.compose.foundation.layout.samples.insetsBottomHeightSample
*/
-@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsBottomHeight(insets: WindowInsets) = this.then(
DerivedHeightModifier(insets, debugInspectorInfo {
diff --git a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt
index ab50488..50a2e51 100644
--- a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt
@@ -102,7 +102,6 @@
)
}
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.drawSelectionHandle(
isStartHandle: Boolean,
direction: ResolvedTextDirection,
diff --git a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt
index 7a7e170..fa0d056 100644
--- a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation.newtext.text.copypasta.selection
-import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.MagnifierStyle
import androidx.compose.foundation.magnifier
@@ -36,7 +35,6 @@
// We use composed{} to read a local, but don't provide inspector info because the underlying
// magnifier modifier provides more meaningful inspector info.
-@SuppressLint("ModifierInspectorInfo")
@OptIn(ExperimentalFoundationApi::class)
internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier {
// Avoid tracking animation state on older Android versions that don't support magnifiers.
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt
index 2817f3e..1f7a00c 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt
@@ -60,7 +60,6 @@
* The text magnifier follows horizontal dragging exactly, but is vertically clamped to the current
* line, so when it changes lines we animate it.
*/
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.animatedSelectionMagnifier(
magnifierCenter: () -> Offset,
platformMagnifier: (animatedCenter: () -> Offset) -> Modifier
diff --git a/compose/foundation/foundation/samples/build.gradle b/compose/foundation/foundation/samples/build.gradle
index 343fecb..38d57ca 100644
--- a/compose/foundation/foundation/samples/build.gradle
+++ b/compose/foundation/foundation/samples/build.gradle
@@ -36,7 +36,7 @@
implementation("androidx.compose.runtime:runtime:1.2.1")
implementation("androidx.compose.ui:ui:1.2.1")
implementation("androidx.compose.ui:ui-text:1.2.1")
- implementation(project(":compose:ui:ui-tooling-preview"))
+ implementation("androidx.compose.ui:ui-tooling-preview:1.4.0-beta02")
debugImplementation(project(":compose:ui:ui-tooling"))
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt
new file mode 100644
index 0000000..edd29e1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/DragGestureDetectorTest.kt
@@ -0,0 +1,688 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gesture
+
+import androidx.compose.foundation.gestures.awaitDragOrCancellation
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.awaitHorizontalDragOrCancellation
+import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.awaitVerticalDragOrCancellation
+import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.testutils.TestViewConfiguration
+import androidx.compose.ui.AbsoluteAlignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpSize
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val TargetTag = "TargetLayout"
+
+@RunWith(Parameterized::class)
+class DragGestureDetectorTest(dragType: GestureType) {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ enum class GestureType {
+ VerticalDrag,
+ HorizontalDrag,
+ AwaitVerticalDragOrCancel,
+ AwaitHorizontalDragOrCancel,
+ AwaitDragOrCancel,
+ DragWithVertical,
+ DragWithHorizontal,
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun parameters() = GestureType.values()
+ }
+
+ private var dragDistance = 0f
+ private var dragged = false
+ private var gestureEnded = false
+ private var gestureStarted = false
+ private var gestureCanceled = false
+ private var consumePositiveOnly = false
+ private var sloppyDetector = false
+ private var startOrder = -1
+ private var endOrder = -1
+ private var cancelOrder = -1
+ private var dragOrder = -1
+
+ private val DragTouchSlopUtil = layoutWithGestureDetector {
+ var count = 0
+ detectDragGestures(
+ onDragStart = {
+ gestureStarted = true
+ startOrder = count++
+ },
+ onDragEnd = {
+ gestureEnded = true
+ endOrder = count++
+ },
+ onDragCancel = {
+ gestureCanceled = true
+ cancelOrder = count++
+ }
+ ) { change, dragAmount ->
+ val positionChange = change.positionChange()
+ dragOrder = count++
+ if (positionChange.x > 0f || positionChange.y > 0f || !consumePositiveOnly) {
+ change.consume()
+ dragged = true
+ dragDistance += dragAmount.getDistance()
+ }
+ }
+ }
+
+ private val VerticalTouchSlopUtil = layoutWithGestureDetector {
+ var count = 0
+ detectVerticalDragGestures(
+ onDragStart = {
+ gestureStarted = true
+ startOrder = count++
+ },
+ onDragEnd = {
+ gestureEnded = true
+ endOrder = count++
+ },
+ onDragCancel = {
+ gestureCanceled = true
+ cancelOrder = count++
+ }
+ ) { change, dragAmount ->
+ dragOrder = count++
+ if (change.positionChange().y > 0f || !consumePositiveOnly) {
+ dragged = true
+ dragDistance += dragAmount
+ }
+ }
+ }
+
+ private val HorizontalTouchSlopUtil = layoutWithGestureDetector {
+ var count = 0
+ detectHorizontalDragGestures(
+ onDragStart = {
+ gestureStarted = true
+ startOrder = count++
+ },
+ onDragEnd = {
+ gestureEnded = true
+ endOrder = count++
+ },
+ onDragCancel = {
+ gestureCanceled = true
+ cancelOrder = count++
+ }
+ ) { change, dragAmount ->
+ dragOrder = count++
+ if (change.positionChange().x > 0f || !consumePositiveOnly) {
+ dragged = true
+ dragDistance += dragAmount
+ }
+ }
+ }
+
+ private val AwaitVerticalDragUtil = layoutWithGestureDetector {
+ awaitEachGesture {
+ val down = awaitFirstDown()
+ val slopChange = awaitVerticalTouchSlopOrCancellation(down.id) { change, overSlop ->
+ if (change.positionChange().y > 0f || !consumePositiveOnly) {
+ dragged = true
+ dragDistance = overSlop
+ change.consume()
+ }
+ }
+ if (slopChange != null || sloppyDetector) {
+ gestureStarted = true
+ var pointer = if (sloppyDetector) down.id else slopChange!!.id
+ do {
+ val change = awaitVerticalDragOrCancellation(pointer)
+ if (change == null) {
+ gestureCanceled = true
+ } else {
+ dragDistance += change.positionChange().y
+ change.consume()
+ if (change.changedToUpIgnoreConsumed()) {
+ gestureEnded = true
+ }
+ pointer = change.id
+ }
+ } while (!gestureEnded && !gestureCanceled)
+ }
+ }
+ }
+
+ private val AwaitHorizontalDragUtil = layoutWithGestureDetector {
+ awaitEachGesture {
+ val down = awaitFirstDown()
+ val slopChange =
+ awaitHorizontalTouchSlopOrCancellation(down.id) { change, overSlop ->
+ if (change.positionChange().x > 0f || !consumePositiveOnly) {
+ dragged = true
+ dragDistance = overSlop
+ change.consume()
+ }
+ }
+ if (slopChange != null || sloppyDetector) {
+ gestureStarted = true
+ var pointer = if (sloppyDetector) down.id else slopChange!!.id
+ do {
+ val change = awaitHorizontalDragOrCancellation(pointer)
+ if (change == null) {
+ gestureCanceled = true
+ } else {
+ dragDistance += change.positionChange().x
+ change.consume()
+ if (change.changedToUpIgnoreConsumed()) {
+ gestureEnded = true
+ }
+ pointer = change.id
+ }
+ } while (!gestureEnded && !gestureCanceled)
+ }
+ }
+ }
+
+ private val AwaitDragUtil = layoutWithGestureDetector {
+ awaitEachGesture {
+ val down = awaitFirstDown()
+ val slopChange = awaitTouchSlopOrCancellation(down.id) { change, overSlop ->
+ val positionChange = change.positionChange()
+ if (positionChange.x > 0f || positionChange.y > 0f || !consumePositiveOnly) {
+ dragged = true
+ dragDistance = overSlop.getDistance()
+ change.consume()
+ }
+ }
+ if (slopChange != null || sloppyDetector) {
+ gestureStarted = true
+ var pointer = if (sloppyDetector) down.id else slopChange!!.id
+ do {
+ val change = awaitDragOrCancellation(pointer)
+ if (change == null) {
+ gestureCanceled = true
+ } else {
+ dragDistance += change.positionChange().getDistance()
+ change.consume()
+ if (change.changedToUpIgnoreConsumed()) {
+ gestureEnded = true
+ }
+ pointer = change.id
+ }
+ } while (!gestureEnded && !gestureCanceled)
+ }
+ }
+ }
+
+ private val content = when (dragType) {
+ GestureType.VerticalDrag -> VerticalTouchSlopUtil
+ GestureType.HorizontalDrag -> HorizontalTouchSlopUtil
+ GestureType.AwaitVerticalDragOrCancel -> AwaitVerticalDragUtil
+ GestureType.AwaitHorizontalDragOrCancel -> AwaitHorizontalDragUtil
+ GestureType.AwaitDragOrCancel -> AwaitDragUtil
+ GestureType.DragWithVertical -> DragTouchSlopUtil
+ GestureType.DragWithHorizontal -> DragTouchSlopUtil
+ }
+
+ private val dragMotion = when (dragType) {
+ GestureType.VerticalDrag,
+ GestureType.AwaitVerticalDragOrCancel,
+ GestureType.DragWithVertical -> Offset(0f, 18f)
+ else -> Offset(18f, 0f)
+ }
+
+ private val crossDragMotion = when (dragType) {
+ GestureType.VerticalDrag,
+ GestureType.AwaitVerticalDragOrCancel,
+ GestureType.DragWithVertical -> Offset(18f, 0f)
+ else -> Offset(0f, 18f)
+ }
+
+ private val twoAxisDrag = when (dragType) {
+ GestureType.DragWithVertical,
+ GestureType.DragWithHorizontal,
+ GestureType.AwaitDragOrCancel -> true
+ else -> false
+ }
+
+ private val supportsSloppyGesture = when (dragType) {
+ GestureType.AwaitVerticalDragOrCancel,
+ GestureType.AwaitHorizontalDragOrCancel,
+ GestureType.AwaitDragOrCancel -> true
+ else -> false
+ }
+
+ private val nothingHandler: PointerInputChange.() -> Unit = {}
+
+ private var initialPass: PointerInputChange.() -> Unit = nothingHandler
+ private var finalPass: PointerInputChange.() -> Unit = nothingHandler
+
+ @Before
+ fun setup() {
+ dragDistance = 0f
+ dragged = false
+ gestureEnded = false
+
+ rule.setContent(content)
+ }
+
+ private fun layoutWithGestureDetector(
+ gestureDetector: suspend PointerInputScope.() -> Unit,
+ ): @Composable () -> Unit = {
+ CompositionLocalProvider(
+ LocalDensity provides Density(1f),
+ LocalViewConfiguration provides TestViewConfiguration(
+ minimumTouchTargetSize = DpSize.Zero
+ )
+ ) {
+ with(LocalDensity.current) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .pointerInput(Unit) {
+ // Some tests execute a lambda before the initial and final passes
+ // so they are called here, higher up the chain, so that the
+ // calls happen prior to the gestureDetector below. The lambdas
+ // do things like consume events on the initial pass or validate
+ // consumption on the final pass.
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.changes.forEach {
+ initialPass(it)
+ }
+ awaitPointerEvent(PointerEventPass.Final)
+ event.changes.forEach {
+ finalPass(it)
+ }
+ }
+ }
+ }
+ .wrapContentSize(AbsoluteAlignment.TopLeft)
+ .size(100.toDp())
+ .pointerInput(key1 = gestureDetector, block = gestureDetector)
+ .testTag(TargetTag)
+ )
+ }
+ }
+ }
+
+ /**
+ * Executes [block] on the [TargetTag] layout. The optional [initialPass] is executed
+ * prior to the [PointerEventPass.Initial] and [finalPass] is executed before
+ * [PointerEventPass.Final] of the gesture detector.
+ */
+ private fun performTouch(
+ initialPass: PointerInputChange.() -> Unit = nothingHandler,
+ finalPass: PointerInputChange.() -> Unit = nothingHandler,
+ block: TouchInjectionScope.() -> Unit
+ ) {
+ this.initialPass = initialPass
+ this.finalPass = finalPass
+ rule.onNodeWithTag(TargetTag).performTouchInput(block)
+ rule.waitForIdle()
+ this.initialPass = nothingHandler
+ this.finalPass = nothingHandler
+ }
+
+ /**
+ * A normal drag, just to ensure that the drag worked.
+ */
+ @Test
+ fun normalDrag() {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(dragMotion)
+ }
+ assertTrue(gestureStarted)
+ assertTrue(dragged)
+ assertEquals(0f, dragDistance)
+ performTouch {
+ moveBy(dragMotion)
+ }
+ assertEquals(18f, dragDistance)
+ assertFalse(gestureEnded)
+ performTouch { up() }
+ assertTrue(gestureEnded)
+ }
+
+ /**
+ * A drag in the opposite direction doesn't cause a drag event.
+ */
+ @Test
+ fun crossDrag() {
+ if (!twoAxisDrag) {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(crossDragMotion)
+ up()
+ }
+ assertFalse(gestureStarted)
+ assertFalse(dragged)
+ assertFalse(gestureEnded)
+
+ // now try a normal drag to ensure that it is still working.
+ performTouch {
+ down(Offset.Zero)
+ moveBy(dragMotion)
+ up()
+ }
+ assertTrue(gestureStarted)
+ assertTrue(dragged)
+ assertEquals(0f, dragDistance)
+ assertTrue(gestureEnded)
+ }
+ }
+
+ /**
+ * Use two fingers and lift the finger before the touch slop is reached.
+ */
+ @Test
+ fun twoFingerDrag_upBeforeSlop() {
+ performTouch {
+ down(0, Offset.Zero)
+ down(1, Offset.Zero)
+ }
+
+ // second finger shouldn't cause a drag. It should follow finger1
+ performTouch {
+ moveBy(1, dragMotion)
+ }
+
+ assertFalse(gestureStarted)
+ assertFalse(dragged)
+
+ // now it should move to finger 2
+ performTouch {
+ up(0)
+ }
+
+ performTouch {
+ moveBy(1, dragMotion)
+ moveBy(1, dragMotion)
+ up(1)
+ }
+
+ assertTrue(dragged)
+ assertEquals(18f, dragDistance)
+ assertTrue(gestureEnded)
+ }
+
+ /**
+ * Use two fingers and lift the finger after the touch slop is reached.
+ */
+ @Test
+ fun twoFingerDrag_upAfterSlop() {
+ performTouch {
+ down(0, Offset.Zero)
+ down(1, Offset.Zero)
+ moveBy(0, dragMotion)
+ up(0)
+ }
+
+ assertTrue(gestureStarted)
+ assertTrue(dragged)
+ assertEquals(0f, dragDistance)
+ assertFalse(gestureEnded)
+
+ performTouch {
+ moveBy(1, dragMotion)
+ up(1)
+ }
+
+ assertEquals(18f, dragDistance)
+ assertTrue(gestureEnded)
+ }
+
+ /**
+ * Cancel drag during touch slop
+ */
+ @Test
+ fun cancelDragDuringSlop() {
+ performTouch {
+ down(Offset.Zero)
+ }
+ performTouch(initialPass = { consume() }) {
+ moveBy(dragMotion)
+ }
+ performTouch {
+ up()
+ }
+ assertFalse(gestureStarted)
+ assertFalse(dragged)
+ assertFalse(gestureEnded)
+ assertFalse(gestureCanceled) // only canceled if the touch slop was crossed first
+ }
+
+ /**
+ * Cancel drag after touch slop
+ */
+ @Test
+ fun cancelDragAfterSlop() {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(dragMotion)
+ }
+ performTouch(initialPass = { consume() }) {
+ moveBy(dragMotion)
+ }
+ performTouch {
+ up()
+ }
+ assertTrue(gestureStarted)
+ assertTrue(dragged)
+ assertFalse(gestureEnded)
+ assertTrue(gestureCanceled)
+ assertEquals(0f, dragDistance)
+ }
+
+ /**
+ * When this drag direction is more than the other drag direction, it should have priority
+ * in locking the orientation.
+ */
+ @Test
+ fun dragLockedWithPriority() {
+ if (!twoAxisDrag) {
+ performTouch {
+ down(Offset.Zero)
+ }
+ // This should have priority because it has moved more than the other direction.
+ performTouch(finalPass = { assertTrue(isConsumed) }) {
+ moveBy((dragMotion * 2f) + crossDragMotion)
+ }
+ performTouch {
+ up()
+ }
+ assertTrue(gestureStarted)
+ assertTrue(dragged)
+ assertTrue(gestureEnded)
+ assertFalse(gestureCanceled)
+ assertEquals(18f, dragDistance)
+ }
+ }
+
+ /**
+ * When a drag is not consumed, it should lead to the touch slop being reset. This is
+ * important when you drag your finger to
+ */
+ @Test
+ fun dragBackAndForth() {
+ if (supportsSloppyGesture) {
+ try {
+ consumePositiveOnly = true
+
+ performTouch {
+ down(Offset.Zero)
+ moveBy(-dragMotion)
+ }
+
+ assertFalse(gestureStarted)
+ assertFalse(dragged)
+ performTouch {
+ moveBy(dragMotion)
+ up()
+ }
+
+ assertTrue(gestureStarted)
+ assertTrue(dragged)
+ } finally {
+ consumePositiveOnly = false
+ }
+ }
+ }
+
+ /**
+ * When gesture detectors use the wrong pointer for the drag, it should just not
+ * detect the touch.
+ */
+ @Test
+ fun pointerUpTooQuickly() {
+ if (supportsSloppyGesture) {
+ try {
+ sloppyDetector = true
+
+ performTouch {
+ down(0, Offset.Zero)
+ down(1, Offset.Zero)
+ up(0)
+ moveBy(1, dragMotion)
+ up(1)
+ }
+
+ // The sloppy detector doesn't know to look at finger2
+ assertTrue(gestureCanceled)
+ } finally {
+ sloppyDetector = false
+ }
+ }
+ }
+
+ @Test
+ fun dragGestureCallbackOrder_normalFinish() {
+ if (!supportsSloppyGesture) {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(Offset(50f, 50f))
+ }
+ assertTrue(startOrder < dragOrder)
+ performTouch {
+ up()
+ }
+ assertTrue(startOrder < dragOrder)
+ assertTrue(dragOrder < endOrder)
+ assertTrue(cancelOrder == -1)
+ }
+ }
+
+ @Test
+ fun dragGestureCallbackOrder_cancel() {
+ if (!supportsSloppyGesture) {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(dragMotion)
+ }
+ performTouch(initialPass = { consume() }) {
+ moveBy(dragMotion)
+ }
+ assertTrue(startOrder < dragOrder)
+ assertTrue(dragOrder < cancelOrder)
+ assertTrue(endOrder == -1)
+ }
+ }
+
+ // An error in the pointer input stream should stop the gesture without error.
+ @Test
+ fun interruptedBeforeDrag() {
+ performTouch {
+ down(Offset.Zero)
+ cancel()
+ }
+ // The next stream doesn't have the existing pointer, so we lose the dragging
+ performTouch {
+ down(Offset.Zero)
+ up()
+ }
+ assertFalse(gestureStarted)
+ }
+
+ // An error in the pointer input stream should stop the gesture without error.
+ @Test
+ fun interruptedBeforeTouchSlop() {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(dragMotion / 2f)
+ cancel()
+ }
+ // The next stream doesn't have the existing pointer, so we lose the dragging
+ performTouch {
+ down(Offset.Zero)
+ up()
+ }
+ assertFalse(gestureStarted)
+ }
+
+ // An error in the pointer input stream should end in a drag cancellation.
+ @Test
+ fun interruptedAfterTouchSlop() {
+ performTouch {
+ down(Offset.Zero)
+ moveBy(dragMotion * 2f)
+ cancel()
+ }
+ // The next stream doesn't have the existing pointer, so we lose the dragging
+ performTouch {
+ down(Offset.Zero)
+ up()
+ }
+ assertTrue(gestureCanceled)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index 0da50f5..6da4790 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy.staggeredgrid
+import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
@@ -26,6 +27,7 @@
import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@@ -1848,4 +1850,42 @@
DpOffset(0.dp, itemSizeDp * 2), DpSize(itemSizeDp, itemSizeDp)
)
}
+
+ @Test
+ fun changeItemsAndScrollImmediately() {
+ val keys = mutableStateListOf<Int>().also { list ->
+ repeat(10) {
+ list.add(it)
+ }
+ }
+ rule.setContent {
+ state = rememberLazyStaggeredGridState()
+ LazyStaggeredGrid(
+ lanes = 2,
+ Modifier.mainAxisSize(itemSizeDp),
+ state
+ ) {
+ items(keys, key = { it }) {
+ Box(Modifier.size(itemSizeDp * 2))
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ state.scrollTo(8)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(8)
+
+ keys.add(0, -1)
+ keys.add(0, -2)
+
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollBy(10f)
+ state.scrollBy(-10f)
+ }
+
+ assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt
index e1375ef..925b531 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation
-import android.annotation.SuppressLint
import android.os.Build
import android.widget.Magnifier
import androidx.annotation.ChecksSdkIntAtLeast
@@ -261,7 +260,6 @@
@OptIn(ExperimentalFoundationApi::class)
// The InspectorInfo this modifier reports is for the above public overload, and intentionally
// doesn't include the platformMagnifierFactory parameter.
-@SuppressLint("ModifierInspectorInfo")
@RequiresApi(28)
internal fun Modifier.magnifier(
sourceCenter: Density.() -> Offset,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt
index 5d3a11a..9cd3f60 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt
@@ -59,7 +59,6 @@
Spacer(modifier.size(CursorHandleWidth, CursorHandleHeight).drawCursorHandle())
}
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.drawCursorHandle() = composed {
val handleColor = LocalTextSelectionColors.current.handleColor
this.then(
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 2e36f5e..70000af 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
@@ -105,7 +105,6 @@
)
}
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.drawSelectionHandle(
isStartHandle: Boolean,
direction: ResolvedTextDirection,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
index fb8c947..2227609 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation.text.selection
-import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.MagnifierStyle
import androidx.compose.foundation.magnifier
@@ -37,7 +36,6 @@
// We use composed{} to read a local, but don't provide inspector info because the underlying
// magnifier modifier provides more meaningful inspector info.
-@SuppressLint("ModifierInspectorInfo")
@OptIn(ExperimentalFoundationApi::class)
internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier {
// Avoid tracking animation state on older Android versions that don't support magnifiers.
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
index af9d356..e750b21 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation.text.selection
-import android.annotation.SuppressLint
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.MagnifierStyle
import androidx.compose.foundation.magnifier
@@ -35,7 +34,6 @@
// We use composed{} to read a local, but don't provide inspector info because the underlying
// magnifier modifier provides more meaningful inspector info.
-@SuppressLint("ModifierInspectorInfo")
@OptIn(ExperimentalFoundationApi::class)
internal actual fun Modifier.textFieldMagnifier(manager: TextFieldSelectionManager): Modifier {
// Avoid tracking animation state on older Android versions that don't support magnifiers.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
index bc2bf71..5d4cbcf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
@@ -25,12 +25,10 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
@@ -73,10 +71,7 @@
@ExperimentalFoundationApi
override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
- this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
- name = "animateItemPlacement"
- value = animationSpec
- }))
+ this then AnimateItemPlacementElement(animationSpec)
}
private class ParentSizeElement(
@@ -158,19 +153,33 @@
}
}
-private class AnimateItemPlacementModifier(
- val animationSpec: FiniteAnimationSpec<IntOffset>,
- inspectorInfo: InspectorInfo.() -> Unit,
-) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
- override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
+private class AnimateItemPlacementElement(
+ val animationSpec: FiniteAnimationSpec<IntOffset>
+) : ModifierNodeElement<AnimateItemPlacementNode>() {
+ override fun create(): AnimateItemPlacementNode = AnimateItemPlacementNode(animationSpec)
+
+ override fun update(node: AnimateItemPlacementNode): AnimateItemPlacementNode = node.also {
+ it.animationSpec = animationSpec
+ }
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is AnimateItemPlacementModifier) return false
+ if (other !is AnimateItemPlacementElement) return false
return animationSpec != other.animationSpec
}
override fun hashCode(): Int {
return animationSpec.hashCode()
}
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "animateItemPlacement"
+ value = animationSpec
+ }
+}
+
+private class AnimateItemPlacementNode(
+ var animationSpec: FiniteAnimationSpec<IntOffset>
+) : Modifier.Node(), ParentDataModifierNode {
+ override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
index 6851f68..79d7926 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt
@@ -19,10 +19,9 @@
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ParentDataModifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
@@ -30,25 +29,36 @@
internal object LazyGridItemScopeImpl : LazyGridItemScope {
@ExperimentalFoundationApi
override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
- this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
- name = "animateItemPlacement"
- value = animationSpec
- }))
+ this then AnimateItemPlacementElement(animationSpec)
}
-private class AnimateItemPlacementModifier(
- val animationSpec: FiniteAnimationSpec<IntOffset>,
- inspectorInfo: InspectorInfo.() -> Unit,
-) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
- override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
+private class AnimateItemPlacementElement(
+ val animationSpec: FiniteAnimationSpec<IntOffset>
+) : ModifierNodeElement<AnimateItemPlacementNode>() {
+ override fun create(): AnimateItemPlacementNode = AnimateItemPlacementNode(animationSpec)
+
+ override fun update(node: AnimateItemPlacementNode): AnimateItemPlacementNode = node.also {
+ it.animationSpec = animationSpec
+ }
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is AnimateItemPlacementModifier) return false
+ if (other !is AnimateItemPlacementElement) return false
return animationSpec != other.animationSpec
}
override fun hashCode(): Int {
return animationSpec.hashCode()
}
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "animateItemPlacement"
+ value = animationSpec
+ }
+}
+
+private class AnimateItemPlacementNode(
+ var animationSpec: FiniteAnimationSpec<IntOffset>
+) : Modifier.Node(), ParentDataModifierNode {
+ override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
index 1e56270..9e43e1c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -35,7 +35,7 @@
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
-@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Suppress("ComposableModifierFactory")
@Composable
internal fun Modifier.lazyLayoutSemantics(
itemProvider: LazyLayoutItemProvider,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
index edadce9..4fa39a1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
@@ -67,6 +67,9 @@
state.isVertical = isVertical
state.spanProvider = itemProvider.spanProvider
+ // ensure scroll position is up to date
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+
// setup measure
val beforeContentPadding = contentPadding.beforePadding(
orientation, reverseLayout, layoutDirection
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index 69c0ab3..703aa24 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -36,7 +36,6 @@
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.withContext
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.cursor(
state: TextFieldState,
value: TextFieldValue,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
index 10bdc6d..37db2fe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
@@ -232,7 +232,6 @@
}
}
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.textFieldKeyInput(
state: TextFieldState,
manager: TextFieldSelectionManager,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
index 24af865..dbb23d8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
@@ -33,7 +33,6 @@
/**
* Required for the press and tap [MutableInteractionSource] consistency for TextField.
*/
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.tapPressTextFieldModifier(
interactionSource: MutableInteractionSource?,
enabled: Boolean = true,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
index 62f03bc..124fdb7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
@@ -35,7 +35,6 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.textFieldMinSize(style: TextStyle) = composed {
val density = LocalDensity.current
val fontFamilyResolver = LocalFontFamilyResolver.current
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
index 531f018..604a5c3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
@@ -60,7 +60,6 @@
* The text magnifier follows horizontal dragging exactly, but is vertically clamped to the current
* line, so when it changes lines we animate it.
*/
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.animatedSelectionMagnifier(
magnifierCenter: () -> Offset,
platformMagnifier: (animatedCenter: () -> Offset) -> Modifier
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
deleted file mode 100644
index 9479a6c..0000000
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
+++ /dev/null
@@ -1,524 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.gestures
-
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.positionChange
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-class DragGestureDetectorTest(dragType: GestureType) {
- enum class GestureType {
- VerticalDrag,
- HorizontalDrag,
- AwaitVerticalDragOrCancel,
- AwaitHorizontalDragOrCancel,
- AwaitDragOrCancel,
- DragWithVertical,
- DragWithHorizontal,
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters
- fun parameters() = GestureType.values()
- }
-
- private var dragDistance = 0f
- private var dragged = false
- private var gestureEnded = false
- private var gestureStarted = false
- private var gestureCanceled = false
- private var consumePositiveOnly = false
- private var sloppyDetector = false
- private var startOrder = -1
- private var endOrder = -1
- private var cancelOrder = -1
- private var dragOrder = -1
-
- private val DragTouchSlopUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- var count = 0
- detectDragGestures(
- onDragStart = {
- gestureStarted = true
- startOrder = count++
- },
- onDragEnd = {
- gestureEnded = true
- endOrder = count++
- },
- onDragCancel = {
- gestureCanceled = true
- cancelOrder = count++
- }
- ) { change, dragAmount ->
- val positionChange = change.positionChange()
- dragOrder = count++
- if (positionChange.x > 0f || positionChange.y > 0f || !consumePositiveOnly) {
- change.consume()
- dragged = true
- dragDistance += dragAmount.getDistance()
- }
- }
- }
-
- private val VerticalTouchSlopUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- var count = 0
- detectVerticalDragGestures(
- onDragStart = {
- gestureStarted = true
- startOrder = count++
- },
- onDragEnd = {
- gestureEnded = true
- endOrder = count++
- },
- onDragCancel = {
- gestureCanceled = true
- cancelOrder = count++
- }
- ) { change, dragAmount ->
- dragOrder = count++
- if (change.positionChange().y > 0f || !consumePositiveOnly) {
- dragged = true
- dragDistance += dragAmount
- }
- }
- }
-
- private val HorizontalTouchSlopUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- var count = 0
- detectHorizontalDragGestures(
- onDragStart = {
- gestureStarted = true
- startOrder = count++
- },
- onDragEnd = {
- gestureEnded = true
- endOrder = count++
- },
- onDragCancel = {
- gestureCanceled = true
- cancelOrder = count++
- }
- ) { change, dragAmount ->
- dragOrder = count++
- if (change.positionChange().x > 0f || !consumePositiveOnly) {
- dragged = true
- dragDistance += dragAmount
- }
- }
- }
-
- private val AwaitVerticalDragUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- awaitEachGesture {
- val down = awaitFirstDown()
- val slopChange = awaitVerticalTouchSlopOrCancellation(down.id) { change, overSlop ->
- if (change.positionChange().y > 0f || !consumePositiveOnly) {
- dragged = true
- dragDistance = overSlop
- change.consume()
- }
- }
- if (slopChange != null || sloppyDetector) {
- gestureStarted = true
- var pointer = if (sloppyDetector) down.id else slopChange!!.id
- do {
- val change = awaitVerticalDragOrCancellation(pointer)
- if (change == null) {
- gestureCanceled = true
- } else {
- dragDistance += change.positionChange().y
- change.consume()
- if (change.changedToUpIgnoreConsumed()) {
- gestureEnded = true
- }
- pointer = change.id
- }
- } while (!gestureEnded && !gestureCanceled)
- }
- }
- }
-
- private val AwaitHorizontalDragUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- awaitEachGesture {
- val down = awaitFirstDown()
- val slopChange =
- awaitHorizontalTouchSlopOrCancellation(down.id) { change, overSlop ->
- if (change.positionChange().x > 0f || !consumePositiveOnly) {
- dragged = true
- dragDistance = overSlop
- change.consume()
- }
- }
- if (slopChange != null || sloppyDetector) {
- gestureStarted = true
- var pointer = if (sloppyDetector) down.id else slopChange!!.id
- do {
- val change = awaitHorizontalDragOrCancellation(pointer)
- if (change == null) {
- gestureCanceled = true
- } else {
- dragDistance += change.positionChange().x
- change.consume()
- if (change.changedToUpIgnoreConsumed()) {
- gestureEnded = true
- }
- pointer = change.id
- }
- } while (!gestureEnded && !gestureCanceled)
- }
- }
- }
-
- private val AwaitDragUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- awaitEachGesture {
- val down = awaitFirstDown()
- val slopChange = awaitTouchSlopOrCancellation(down.id) { change, overSlop ->
- val positionChange = change.positionChange()
- if (positionChange.x > 0f || positionChange.y > 0f || !consumePositiveOnly) {
- dragged = true
- dragDistance = overSlop.getDistance()
- change.consume()
- }
- }
- if (slopChange != null || sloppyDetector) {
- gestureStarted = true
- var pointer = if (sloppyDetector) down.id else slopChange!!.id
- do {
- val change = awaitDragOrCancellation(pointer)
- if (change == null) {
- gestureCanceled = true
- } else {
- dragDistance += change.positionChange().getDistance()
- change.consume()
- if (change.changedToUpIgnoreConsumed()) {
- gestureEnded = true
- }
- pointer = change.id
- }
- } while (!gestureEnded && !gestureCanceled)
- }
- }
- }
-
- private val util = when (dragType) {
- GestureType.VerticalDrag -> VerticalTouchSlopUtil
- GestureType.HorizontalDrag -> HorizontalTouchSlopUtil
- GestureType.AwaitVerticalDragOrCancel -> AwaitVerticalDragUtil
- GestureType.AwaitHorizontalDragOrCancel -> AwaitHorizontalDragUtil
- GestureType.AwaitDragOrCancel -> AwaitDragUtil
- GestureType.DragWithVertical -> DragTouchSlopUtil
- GestureType.DragWithHorizontal -> DragTouchSlopUtil
- }
-
- private val dragMotion = when (dragType) {
- GestureType.VerticalDrag,
- GestureType.AwaitVerticalDragOrCancel,
- GestureType.DragWithVertical -> Offset(0f, 18f)
- else -> Offset(18f, 0f)
- }
-
- private val crossDragMotion = when (dragType) {
- GestureType.VerticalDrag,
- GestureType.AwaitVerticalDragOrCancel,
- GestureType.DragWithVertical -> Offset(18f, 0f)
- else -> Offset(0f, 18f)
- }
-
- private val twoAxisDrag = when (dragType) {
- GestureType.DragWithVertical,
- GestureType.DragWithHorizontal,
- GestureType.AwaitDragOrCancel -> true
- else -> false
- }
-
- private val supportsSloppyGesture = when (dragType) {
- GestureType.AwaitVerticalDragOrCancel,
- GestureType.AwaitHorizontalDragOrCancel,
- GestureType.AwaitDragOrCancel -> true
- else -> false
- }
-
- @Before
- fun setup() {
- dragDistance = 0f
- dragged = false
- gestureEnded = false
- }
-
- /**
- * A normal drag, just to ensure that the drag worked.
- */
- @Test
- fun normalDrag() = util.executeInComposition {
- val move = down().moveBy(dragMotion)
- assertTrue(gestureStarted)
- assertTrue(dragged)
- assertEquals(0f, dragDistance)
- val move2 = move.moveBy(dragMotion)
- assertEquals(18f, dragDistance)
- assertFalse(gestureEnded)
- move2.up()
- assertTrue(gestureEnded)
- }
-
- /**
- * A drag in the opposite direction doesn't cause a drag event.
- */
- @Test
- fun crossDrag() = util.executeInComposition {
- if (!twoAxisDrag) {
- down().moveBy(crossDragMotion).up()
- assertFalse(gestureStarted)
- assertFalse(dragged)
- assertFalse(gestureEnded)
-
- // now try a normal drag to ensure that it is still working.
- down().moveBy(dragMotion).up()
- assertTrue(gestureStarted)
- assertTrue(dragged)
- assertEquals(0f, dragDistance)
- assertTrue(gestureEnded)
- }
- }
-
- /**
- * Use two fingers and lift the finger before the touch slop is reached.
- */
- @Test
- fun twoFingerDrag_upBeforeSlop() = util.executeInComposition {
- val finger1 = down()
- val finger2 = down()
-
- // second finger shouldn't cause a drag. It should follow finger1
- val moveFinger2 = finger2.moveBy(dragMotion)
-
- assertFalse(gestureStarted)
- assertFalse(dragged)
-
- // now it should move to finger 2
- finger1.up()
-
- moveFinger2.moveBy(dragMotion).moveBy(dragMotion).up()
-
- assertTrue(dragged)
- assertEquals(18f, dragDistance)
- assertTrue(gestureEnded)
- }
-
- /**
- * Use two fingers and lift the finger after the touch slop is reached.
- */
- @Test
- fun twoFingerDrag_upAfterSlop() = util.executeInComposition {
- val finger1 = down()
- val finger2 = down()
-
- finger1.moveBy(dragMotion).up()
-
- assertTrue(gestureStarted)
- assertTrue(dragged)
- assertEquals(0f, dragDistance)
- assertFalse(gestureEnded)
-
- finger2.moveBy(dragMotion).up()
-
- assertEquals(18f, dragDistance)
- assertTrue(gestureEnded)
- }
-
- /**
- * Cancel drag during touch slop
- */
- @Test
- fun cancelDragDuringSlop() = util.executeInComposition {
- down().moveBy(dragMotion) { consume() }.moveBy(dragMotion).up()
- assertFalse(gestureStarted)
- assertFalse(dragged)
- assertFalse(gestureEnded)
- assertFalse(gestureCanceled) // only canceled if the touch slop was crossed first
- }
-
- /**
- * Cancel drag after touch slop
- */
- @Test
- fun cancelDragAfterSlop() = util.executeInComposition {
- down().moveBy(dragMotion).moveBy(dragMotion) { consume() }.up()
- assertTrue(gestureStarted)
- assertTrue(dragged)
- assertFalse(gestureEnded)
- assertTrue(gestureCanceled)
- assertEquals(0f, dragDistance)
- }
-
- /**
- * When this drag direction is more than the other drag direction, it should have priority
- * in locking the orientation.
- */
- @Test
- fun dragLockedWithPriority() = util.executeInComposition {
- if (!twoAxisDrag) {
- down().moveBy(
- (dragMotion * 2f) + crossDragMotion,
- final = {
- // This should have priority because it has moved more than the other direction.
- assertTrue(isConsumed)
- }
- )
- .up()
- assertTrue(gestureStarted)
- assertTrue(dragged)
- assertTrue(gestureEnded)
- assertFalse(gestureCanceled)
- assertEquals(18f, dragDistance)
- }
- }
-
- /**
- * When a drag is not consumed, it should lead to the touch slop being reset. This is
- * important when you drag your finger to
- */
- @Test
- fun dragBackAndForth() = util.executeInComposition {
- if (supportsSloppyGesture) {
- try {
- consumePositiveOnly = true
-
- val back = down().moveBy(-dragMotion)
-
- assertFalse(gestureStarted)
- assertFalse(dragged)
- back.moveBy(dragMotion).up()
-
- assertTrue(gestureStarted)
- assertTrue(dragged)
- } finally {
- consumePositiveOnly = false
- }
- }
- }
-
- /**
- * When gesture detectors use the wrong pointer for the drag, it should just not
- * detect the touch.
- */
- @Test
- fun pointerUpTooQuickly() = util.executeInComposition {
- if (supportsSloppyGesture) {
- try {
- sloppyDetector = true
-
- val finger1 = down()
- val finger2 = down()
- finger1.up()
- finger2.moveBy(dragMotion).up()
-
- // The sloppy detector doesn't know to look at finger2
- assertTrue(gestureCanceled)
- } finally {
- sloppyDetector = false
- }
- }
- }
-
- @Test
- fun dragGestureCallbackOrder_normalFinish() = util.executeInComposition {
- if (!supportsSloppyGesture) {
- val progress = down().moveBy(Offset(50f, 50f))
- assertTrue(startOrder < dragOrder)
- progress.up()
- assertTrue(startOrder < dragOrder)
- assertTrue(dragOrder < endOrder)
- assertTrue(cancelOrder == -1)
- }
- }
-
- @Test
- fun dragGestureCallbackOrder_cancel() = util.executeInComposition {
- if (!supportsSloppyGesture) {
- down().moveBy(dragMotion).moveBy(dragMotion) { consume() }
- assertTrue(startOrder < dragOrder)
- assertTrue(dragOrder < cancelOrder)
- assertTrue(endOrder == -1)
- }
- }
-
- // An error in the pointer input stream should stop the gesture without error.
- @Test
- fun interruptedBeforeDrag() = util.executeInComposition {
- down()
- clearPointerStream()
- // The next stream doesn't have the existing pointer, so we lose the dragging
- down().up()
- assertFalse(gestureStarted)
- }
-
- // An error in the pointer input stream should stop the gesture without error.
- @Test
- fun interruptedBeforeTouchSlop() = util.executeInComposition {
- down().moveBy(dragMotion / 2f)
- clearPointerStream()
- // The next stream doesn't have the existing pointer, so we lose the dragging
- down().up()
- assertFalse(gestureStarted)
- }
-
- // An error in the pointer input stream should end in a drag cancellation.
- @Test
- fun interruptedAfterTouchSlop() = util.executeInComposition {
- down().moveBy(dragMotion * 2f)
- clearPointerStream()
- // The next stream doesn't have the existing pointer, so we lose the dragging
- down().up()
- assertTrue(gestureCanceled)
- }
-}
-
-@RunWith(JUnit4::class)
-class DragGestureOrderTest {
- var startCount = -1
- var stopCount = -1
- var dragCount = -1
- private val DragOrderUtil = SuspendingGestureTestUtil(width = 100, height = 100) {
- var counter = 0
- detectDragGestures(
- onDragStart = { startCount = counter++ },
- onDragEnd = { stopCount = counter++ }
- ) { _, _ ->
- dragCount = counter++
- }
- }
-
- @Test
- fun dragGestureCallbackOrder() = DragOrderUtil.executeInComposition {
- val progress = down().moveBy(Offset(50f, 50f))
- assertTrue(startCount < dragCount)
- progress.up()
- assertTrue(startCount < dragCount)
- assertTrue(dragCount < stopCount)
- }
-}
\ No newline at end of file
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 29098a8..34632df 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -29,7 +29,6 @@
override val issues get(): List<Issue> {
return listOf(
ListIteratorDetector.ISSUE,
- ModifierInspectorInfoDetector.ISSUE,
UnnecessaryLambdaCreationDetector.ISSUE,
)
}
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt
deleted file mode 100644
index 7cb8923..0000000
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ModifierInspectorInfoDetector.kt
+++ /dev/null
@@ -1,695 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("UnstableApiUsage")
-
-package androidx.compose.lint
-
-import com.android.tools.lint.client.api.UElementHandler
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.android.tools.lint.detector.api.TextFormat
-import com.intellij.psi.PsiField
-import com.intellij.psi.PsiJavaFile
-import com.intellij.psi.PsiMethod
-import com.intellij.psi.PsiType
-import com.intellij.psi.PsiWildcardType
-import com.intellij.psi.impl.source.PsiClassReferenceType
-import com.intellij.psi.util.InheritanceUtil
-import org.jetbrains.kotlin.asJava.classes.KtLightClass
-import org.jetbrains.kotlin.asJava.elements.KtLightField
-import org.jetbrains.kotlin.asJava.elements.KtLightParameter
-import org.jetbrains.kotlin.name.FqName
-import org.jetbrains.kotlin.psi.KtFile
-import org.jetbrains.kotlin.psi.KtParameter
-import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.VALUE_PARAMETER
-import org.jetbrains.uast.UBinaryExpression
-import org.jetbrains.uast.UBlockExpression
-import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.UElement
-import org.jetbrains.uast.UExpression
-import org.jetbrains.uast.UExpressionList
-import org.jetbrains.uast.UIfExpression
-import org.jetbrains.uast.ULambdaExpression
-import org.jetbrains.uast.UMethod
-import org.jetbrains.uast.UObjectLiteralExpression
-import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.UReturnExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
-import org.jetbrains.uast.USwitchClauseExpression
-import org.jetbrains.uast.USwitchClauseExpressionWithBody
-import org.jetbrains.uast.USwitchExpression
-import org.jetbrains.uast.UYieldExpression
-import org.jetbrains.uast.kotlin.KotlinStringTemplateUPolyadicExpression
-import org.jetbrains.uast.kotlin.KotlinStringULiteralExpression
-import org.jetbrains.uast.kotlin.KotlinUArrayAccessExpression
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
-import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression
-import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression
-import org.jetbrains.uast.visitor.AbstractUastVisitor
-
-private const val ModifierClass = "androidx.compose.ui.Modifier"
-private const val ModifierCompanionClass = "androidx.compose.ui.Modifier.Companion"
-private const val ModifierFile = "Modifier.kt"
-private const val ComposedModifierFile = "ComposedModifier.kt"
-private const val InspectableValueFile = "InspectableValue.kt"
-private const val LambdaFunction = "kotlin.jvm.functions.Function1"
-private const val InspectorInfoClass = "androidx.compose.ui.platform.InspectorInfo"
-private const val InspectorValueInfoClass = "androidx.compose.ui.platform.InspectorValueInfo"
-private const val UnitClass = "kotlin.Unit"
-private const val DebugInspectorInfoFunction = "debugInspectorInfo"
-private const val ThenMethodName = "then"
-private const val ComposedMethodName = "composed"
-private const val RememberMethodName = "remember"
-private const val InspectableMethodName = "inspectable"
-private const val ComposedMethodPackage = "androidx.compose.ui"
-private const val RememberMethodPackage = "androidx.compose.runtime"
-private val DemosPackageRegEx = "androidx\\.compose\\..+\\.demos\\..+".toRegex()
-private val UiPackage = FqName("androidx.compose.ui")
-private val PlatformPackage = FqName("androidx.compose.ui.platform")
-
-/**
- * Lint [Detector] to ensure that we are creating debug information for the layout inspector on
- * all modifiers. For example in:
- * ```
- * fun Modifier.width(width: Dp) = this.then(SizeModifier(width))
- * ```
- *
- * The layout inspector will not know that the name is `width` and the `width` member
- * may not be a field in SizeModifier that we can see via reflection.
- *
- * To supply debug information to the layout inspector include an InspectorInfo lambda like this:
- * ```
- * fun Modifier.width(width: Dp) = this.then(
- * SizeModifier(
- * width = width,
- * inspectorInfo = debugInspectorInfo {
- * name = "width",
- * value = width,
- * )
- * )
- * )
- * ```
- *
- * The `debugInspectorInfo' lambda will be stripped from release builds.
- *
- * If the modifier has multiple parameters use the `properties` specifier instead:
- * ```
- * fun Modifier.size(width: Dp, height: Dp) = this.then(
- * SizeModifier(
- * width = width,
- * inspectorInfo = debugInspectorInfo {
- * name = "width",
- * properties["width"] = width
- * properties["height"] = height
- * )
- * )
- * )
- * ```
- *
- * This inspector only applies to Modifiers implemented prior to Modifier.Node. For the
- * corresponding Modifier.Node inspection, see `ModifierNodeInspectablePropertiesDetector` in
- * `:compose:ui:ui-lint`.
- */
-class ModifierInspectorInfoDetector : Detector(), SourceCodeScanner {
- override fun createUastHandler(context: JavaContext): UElementHandler = ModifierHandler(context)
-
- override fun getApplicableUastTypes() = listOf(UMethod::class.java)
-
- /**
- * This handler visits every method and determines if this is a Modifier definition with a
- * `then` construct, and reports an issue for any of the following problems:
- *
- * 1. The modifier implementation is not a function call
- * 2. The modifier implementation does not have an InspectorInfo lambda as last parameter
- * 3. The lambda is not surrounded by a call to `debugInspectorInfo`
- * 4. The modifier name is missing from the lambda or specified incorrectly
- * 5. A modifier value is specified that doesn't match an actual modifier argument
- * 6. A modifier argument is specified that doesn't match an actual modifier argument
- * 7. An actual modifier argument is missing
- */
- private class ModifierHandler(private val context: JavaContext) : UElementHandler() {
- private val returnVisitor = ReturnVisitor()
- private val builderVisitor = ModifierBuilderVisitor()
- private val modifierVisitor = ModifierVisitor()
- private val debugInspectorVisitor = DebugInspectorVisitor()
- private val lambdaVisitor = InspectorLambdaVisitor()
- private var methodInfo: MethodInfo? = null
-
- override fun visitMethod(node: UMethod) {
- if (node.isInFile(ModifierFile, UiPackage) ||
- node.isInFile(ComposedModifierFile, UiPackage) ||
- node.isInFile(InspectableValueFile, PlatformPackage) ||
- firstParameterType(node) != ModifierClass ||
- node.returnType?.canonicalText != ModifierClass ||
- DemosPackageRegEx.matches(node.containingClass?.qualifiedName ?: "")
- ) {
- // Ignore the method if it isn't a method on Modifier returning a Modifier,
- // or if the method is defined in Modifier.kt or ComposedModifier.kt
- return
- }
- methodInfo = MethodInfo(node)
- node.uastBody?.accept(returnVisitor)
- methodInfo = null
- }
-
- private fun UMethod.isInFile(fileName: String, packageName: FqName): Boolean {
- val file = containingFile as? KtFile ?: return false
- return file.name == fileName && file.packageFqName == packageName
- }
-
- private fun firstParameterType(method: UMethod): String? =
- (method.parameters.firstOrNull() as? KtLightParameter)?.type?.canonicalText
-
- private fun isModifierType(type: PsiType?): Boolean =
- InheritanceUtil.isInheritor(type, ModifierClass)
-
- // Disregard the mangled part of a method with inline class parameters.
- // TODO: change this to an API call when a demangle function is available
- private fun name(method: PsiMethod): String =
- method.name.substringBefore('-')
-
- private fun wildcardType(type: PsiType?): String? =
- (type as? PsiWildcardType)?.bound?.canonicalText
-
- private fun isThenFunctionCall(node: UQualifiedReferenceExpression): Boolean {
- if (!isModifierType(node.receiver.getExpressionType())) return false
- val then = node.selector as? KotlinUFunctionCallExpression ?: return false
- return then.methodName == ThenMethodName &&
- then.valueArguments.size == 1 &&
- isModifierType(then.valueArguments.first().getExpressionType())
- }
-
- private fun isComposeFunctionCall(node: UCallExpression): Boolean =
- node.methodName == ComposedMethodName &&
- node.receiverType?.canonicalText == ModifierClass &&
- node.returnType?.canonicalText == ModifierClass &&
- methodPackageName(node) == ComposedMethodPackage
-
- private fun isRememberFunctionCall(node: UCallExpression): Boolean =
- node.methodName == RememberMethodName &&
- node.receiver == null &&
- isModifierType(node.returnType) &&
- methodPackageName(node) == RememberMethodPackage
-
- private fun isInspectableModifier(node: UCallExpression): Boolean =
- node.methodName == InspectableMethodName &&
- node.receiverType?.canonicalText in listOf(ModifierClass, ModifierCompanionClass) &&
- node.returnType?.canonicalText == ModifierClass &&
- methodPackageName(node) == PlatformPackage.asString()
-
- // Return true if this is a lambda expression of the type: "InspectorInfo.() -> Unit"
- private fun isInspectorInfoLambdaType(type: PsiType?): Boolean {
- val referenceType = type as? PsiClassReferenceType ?: return false
- return referenceType.rawType().canonicalText == LambdaFunction &&
- referenceType.parameterCount == 2 &&
- wildcardType(referenceType.parameters[0]) == InspectorInfoClass &&
- wildcardType(referenceType.parameters[1]) !== UnitClass
- }
-
- private fun isDebugInspectorInfoCall(node: UCallExpression): Boolean =
- node.methodName == DebugInspectorInfoFunction &&
- node.valueArgumentCount == 1 &&
- isInspectorInfoLambdaType(node.valueArguments.first().getExpressionType()) &&
- isInspectorInfoLambdaType(node.returnType)
-
- private fun methodPackageName(node: UCallExpression): String? =
- (node.resolve()?.containingFile as? PsiJavaFile)?.packageName
-
- private fun wrongLambda(element: UElement) =
- report(element, ISSUE.getBriefDescription(TextFormat.TEXT))
-
- private fun missingDebugInfoCall(element: UElement) =
- report(element, message = "Expected debugInspectorInfo call")
-
- private fun reportMissing(missingVariables: Set<String>, element: UElement) = report(
- element = element,
- message = "These lambda arguments are missing in the InspectorInfo: " +
- "`${missingVariables.sorted().joinToString("`, `")}`"
- )
-
- private fun wrongName(expectedName: String, unexpected: UElement) = report(
- element = unexpected,
- message = "Expected name of the modifier: `\"name\" = \"${expectedName}\"`"
- )
-
- private fun wrongArgument(args: Collection<String>, unexpected: UElement) {
- val message = when (args.size) {
- 0 -> "Unexpected, modifier has no arguments"
- 1 -> "Expected the variable: \"${args.first()}\""
- else -> "Expected one of the variables: ${args.joinToString(", ", "\"", "\"")}"
- }
- report(unexpected, message = message)
- }
-
- private fun wrongArgumentForIndex(indexName: String, value: UElement) =
- report(value, message = "The value should match the index name: `$indexName`")
-
- private fun unexpected(element: UElement) =
- report(element, message = "Unexpected element found")
-
- private fun report(element: UElement, message: String) {
- context.report(
- ISSUE,
- element,
- context.getNameLocation(element),
- message
- )
- methodInfo?.foundError = true
- }
-
- /**
- * Method related data for checking the inspector lambda against the modifier method.
- *
- * The inspector lambda normally holds a `properties["name"] = name` expression for each
- * of the parameters of the modifier. However if the modifier has exactly one parameter,
- * and that parameter is an instance of a data class, then the arguments of the data class
- * can be used in the lambda instead of the modifier parameters.
- *
- * See the test existingInspectorInfoWithDataClassMemberValues as an example.
- */
- private inner class MethodInfo(method: UMethod) {
- val name = name(method)
- val methodArguments = Arguments(findMethodArguments(method))
- val dataClassArguments = Arguments(findDataClassMembers(method))
- var foundName = false
- var foundError = false
-
- fun checkName(value: UExpression) {
- if (name == literal(value)) {
- foundName = true
- } else if (!foundError) {
- wrongName(name, value)
- }
- }
-
- fun checkValue(value: UExpression, inConditional: Boolean) {
- val (arguments, variable) = variable(value)
- if (variable != null && arguments.expected.contains(variable)) {
- arguments.found.add(variable)
- } else if (!foundError && !(inConditional && isArgumentReceiver(value))) {
- wrongArgument(arguments.expected, value)
- }
- }
-
- /**
- * Check an array from an Inspector lambda.
- *
- * A Inspector lambda may contain expressions of the form:
- * ` properties["name"] = value
- *
- * Check the name and value against the known parameters of the modifier.
- */
- fun checkArray(
- keyExpr: KotlinUArrayAccessExpression,
- value: UExpression,
- inConditional: Boolean
- ) {
- if (foundError) {
- return
- }
- val index = keyExpr.indices.singleOrNull()
- val key = literal(index)
- val (arguments, variable) = variable(value)
- when {
- key != null && arguments.expected.contains(key) && key == variable ->
- arguments.found.add(variable)
- inConditional && isArgumentReceiver(value) ->
- {} // ignore extra information
- key == null || !arguments.expected.contains(key) ->
- wrongArgument(arguments.expected, index ?: keyExpr)
- else ->
- wrongArgumentForIndex(key, value)
- }
- }
-
- fun checkComplete(lambda: UExpression) {
- if (foundError) {
- return
- } else if (!foundName) {
- wrongName(name, lambda)
- } else {
- val arguments =
- if (dataClassArguments.found.isNotEmpty()) dataClassArguments
- else methodArguments
- val absentVariables = arguments.expected.minus(arguments.found)
- if (absentVariables.isNotEmpty()) {
- reportMissing(absentVariables, lambda)
- }
- }
- }
-
- private fun literal(expr: UExpression?): String? = when (expr) {
- is KotlinStringULiteralExpression,
- is KotlinStringTemplateUPolyadicExpression -> expr.evaluate() as? String
- else -> null
- }
-
- private fun isArgumentReceiver(value: UExpression): Boolean {
- val reference = value as? KotlinUQualifiedReferenceExpression ?: return false
- val (arguments, variable) = variable(reference.receiver)
- return arguments.expected.contains(variable)
- }
-
- /**
- * Return the name of the variable that the [expr] represents.
- *
- * This can be a simple parameter variable from the modifier.\
- * Example: `width` from `fun Modifier.width(width: Int)`
- *
- * Or it can be a selector from a data class parameter variable from the modifier.\
- * Example: `values.start` from `fun Modifier.border(values: Borders)`
- *
- * where `Borders` is a data class with `start` as one of the value parameters.
- */
- private fun variable(expr: UExpression?): Pair<Arguments, String?> {
- return when (expr) {
- is KotlinUSimpleReferenceExpression ->
- Pair(methodArguments, expr.identifier)
- is KotlinUQualifiedReferenceExpression -> {
- val receiver = identifier(expr.receiver)
- if (methodArguments.expected.contains(receiver)) {
- Pair(dataClassArguments, identifier(expr.selector))
- } else {
- Pair(methodArguments, null)
- }
- }
- else -> Pair(methodArguments, null)
- }
- }
-
- private fun identifier(expr: UExpression?): String? =
- (expr as? KotlinUSimpleReferenceExpression)?.identifier
-
- private fun findMethodArguments(method: UMethod): Set<String> =
- method.parameters.asSequence().drop(1).mapNotNull { it.name }.toSet()
-
- /**
- * Return all the names of the value parameters of a data class.
- *
- * We don't actually know if a class is a data class, but we can see if all the
- * parameters of the main constructor are value parameter fields.
- */
- private fun findDataClassMembers(method: UMethod): Set<String> {
- val singleParameter = method.parameters.asSequence().drop(1).singleOrNull()
- val type = (singleParameter?.type as? PsiClassReferenceType)?.reference?.resolve()
- val klass = type as? KtLightClass ?: return emptySet()
- val mainConstructor = klass.constructors.firstOrNull() ?: return emptySet()
- val valueParameters = klass.fields.asSequence()
- .filter { isValueParameter(it) }.map { it.name }.toSet()
- val constructorParameters = mainConstructor.parameters.asSequence()
- .map { it.name }.toSet()
- if (constructorParameters != valueParameters) {
- return emptySet()
- }
- return valueParameters
- }
-
- private fun isValueParameter(field: PsiField): Boolean {
- val kotlinField = field as? KtLightField
- val kotlinParameter =
- kotlinField?.lightMemberOrigin?.originalElement as? KtParameter
- return kotlinParameter?.elementType == VALUE_PARAMETER
- }
- }
-
- private class Arguments(val expected: Set<String>) {
- val found = mutableSetOf<String>()
- }
-
- /**
- * Finds all return expressions, ignores all other elements.
- */
- private inner class ReturnVisitor : AbstractUastVisitor() {
- override fun visitReturnExpression(node: UReturnExpression): Boolean {
- node.returnExpression?.accept(builderVisitor)
- return true
- }
-
- // Ignore returns from a lambda expression
- override fun visitLambdaExpression(node: ULambdaExpression): Boolean = true
- }
-
- /**
- * Find and check known Modifier builder expressions.
- *
- * The expression is known to be the return expression from a return statement.
- *
- * Currently the only check the following expressions:
- * - Modifier.then(Modifier)
- * - Modifier.composed(InspectorInfoLambda,factory)
- * - everything else is ignored
- */
- private inner class ModifierBuilderVisitor : AbstractUastVisitor() {
- override fun visitQualifiedReferenceExpression(
- node: UQualifiedReferenceExpression
- ): Boolean {
- if (isThenFunctionCall(node)) {
- node.receiver.accept(this)
- val then = node.selector as KotlinUFunctionCallExpression
- then.valueArguments.first().accept(modifierVisitor)
- return true
- }
- return super.visitQualifiedReferenceExpression(node)
- }
-
- override fun visitCallExpression(node: UCallExpression): Boolean {
- if (isComposeFunctionCall(node)) {
- val inspectorInfo = node.valueArguments
- .find { isInspectorInfoLambdaType(it.getExpressionType()) }
- if (inspectorInfo == null) {
- wrongLambda(node)
- } else {
- inspectorInfo.accept(debugInspectorVisitor)
- }
- return true
- }
- return super.visitCallExpression(node)
- }
- }
-
- /**
- * Find and check the Modifier constructor.
- *
- * The expression is known to be inside one of the Modifier builder expressions accepted
- * by [ModifierBuilderVisitor].
- *
- * Currently the only accepted expressions are of the form:
- * - SomeClassExtendingModifier(p1,p2,p3,p4,inspectorInfoLambda)
- * - SomeClass.InnerModifier(p1,p2,p3,p4,inspectorInfoLambda)
- * - object : Modifier, InspectorValueInfoClass(inspectorInfoLambda)
- * - remember { }
- * - Modifier
- * - if-then-else (with a modifier constructor in both then and else)
- * - when (with a modifier constructor in each of the when clauses)
- * All other expressions are considered errors.
- */
- private inner class ModifierVisitor : UnexpectedVisitor({ wrongLambda(it) }) {
- override fun visitCallExpression(node: UCallExpression): Boolean {
- val info: UExpression? = node.valueArguments.firstOrNull {
- isInspectorInfoLambdaType(it.getExpressionType())
- }
- if (info != null) {
- info.accept(debugInspectorVisitor)
- return true
- }
- if (isRememberFunctionCall(node)) {
- val lambda = node.valueArguments.singleOrNull() as? ULambdaExpression
- val body = lambda?.body as? UBlockExpression
- val ret = body?.expressions?.firstOrNull() as? UReturnExpression
- val definition = ret?.returnExpression ?: return super.visitCallExpression(node)
- definition.accept(this)
- return true
- }
- if (isModifierType(node.receiverType) && isModifierType(node.returnType)) {
- // For now accept all other calls. Assume that the method being called
- // will add inspector information.
- return true
- }
- return super.visitCallExpression(node)
- }
-
- override fun visitQualifiedReferenceExpression(
- node: UQualifiedReferenceExpression
- ): Boolean {
- node.selector.accept(this)
- return true
- }
-
- override fun visitObjectLiteralExpression(node: UObjectLiteralExpression): Boolean {
- if (node.valueArgumentCount == 1 &&
- node.declaration.uastSuperTypes.any {
- it.getQualifiedName() == InspectorValueInfoClass
- }
- ) {
- node.valueArguments.first().accept(debugInspectorVisitor)
- return true
- }
- return super.visitObjectLiteralExpression(node)
- }
-
- override fun visitSimpleNameReferenceExpression(
- node: USimpleNameReferenceExpression
- ): Boolean {
- // Accept any variable including Modifier
- return true
- }
-
- override fun visitIfExpression(node: UIfExpression): Boolean {
- node.thenExpression?.accept(this)
- node.elseExpression?.accept(this)
- return true
- }
-
- override fun visitSwitchExpression(node: USwitchExpression): Boolean {
- node.body.accept(this)
- return true
- }
-
- override fun visitSwitchClauseExpression(node: USwitchClauseExpression): Boolean {
- (node as? USwitchClauseExpressionWithBody)?.let {
- it.body.expressions.last().accept(this)
- return true
- }
- return super.visitSwitchClauseExpression(node)
- }
-
- override fun visitYieldExpression(node: UYieldExpression): Boolean {
- return false
- }
-
- override fun visitExpressionList(node: UExpressionList): Boolean {
- return false
- }
-
- override fun visitBlockExpression(node: UBlockExpression): Boolean {
- node.expressions.lastOrNull()?.accept(this)
- return true
- }
- }
-
- /**
- * Expect debugInspectorInfo factory method:
- * - debugInspectorInfo { inspectorInfoLambda }
- * For all other expressions: complain about the missing debugInspectorInfo call.
- */
- private inner class DebugInspectorVisitor :
- UnexpectedVisitor({ missingDebugInfoCall(it) }) {
-
- override fun visitCallExpression(node: UCallExpression): Boolean {
- if (isDebugInspectorInfoCall(node)) {
- node.valueArguments.single().accept(lambdaVisitor)
- return true
- }
- return super.visitCallExpression(node)
- }
- }
-
- /**
- * Find and check the InspectorInfo lambda.
- *
- * The expression is known to be a InspectorInfo lambda found inside a debugInspectorInfo
- * call from one of the modifier constructors accepted by [ModifierVisitor].
- *
- * Check the name, value, and properties against the original modifier definition expressed
- * by [methodInfo] found by [ModifierHandler] above. After the lambda expression check that
- * all the elements of [methodInfo] was found.
- */
- private inner class InspectorLambdaVisitor : UnexpectedVisitor({ unexpected(it) }) {
- // We allow alternate values inside conditionals.
- // Example see the test: existingInspectorInfoWffithConditionals.
- var inConditional = false
-
- override fun visitBinaryExpression(node: UBinaryExpression): Boolean {
- val left = node.leftOperand
- val right = node.rightOperand
- when {
- left is KotlinUSimpleReferenceExpression && left.identifier == "name" ->
- methodInfo?.checkName(right)
- left is KotlinUSimpleReferenceExpression && left.identifier == "value" ->
- methodInfo?.checkValue(right, inConditional)
- left is KotlinUArrayAccessExpression ->
- methodInfo?.checkArray(left, right, inConditional)
- else ->
- unexpected(left)
- }
- return true
- }
-
- override fun visitLambdaExpression(node: ULambdaExpression): Boolean {
- // accept, and recurse
- return false
- }
-
- override fun visitIfExpression(node: UIfExpression): Boolean {
- inConditional = true
- try {
- node.thenExpression?.accept(this)
- node.elseExpression?.accept(this)
- } finally {
- inConditional = false
- }
- return true
- }
-
- override fun visitBlockExpression(node: UBlockExpression): Boolean {
- // accept, and recurse
- return false
- }
-
- override fun afterVisitLambdaExpression(node: ULambdaExpression) {
- methodInfo?.checkComplete(node)
- }
- }
-
- private abstract inner class UnexpectedVisitor(
- private val error: (node: UElement) -> Unit
- ) : AbstractUastVisitor() {
-
- override fun visitElement(node: UElement): Boolean {
- error(node)
- return true
- }
- }
- }
-
- companion object {
- val ISSUE = Issue.create(
- id = "ModifierInspectorInfo",
- briefDescription = "Modifier missing inspectorInfo",
- explanation =
- """
- The Layout Inspector will see an instance of the usually private modifier class \
- where the modifier name is gone, and the fields may not reflect directly on what \
- was specified for the modifier. Instead specify the `inspectorInfo` directly on \
- the modifier. See example here:
- `androidx.compose.ui.samples.InspectorInfoInComposedModifierSample`.""",
- category = Category.PRODUCTIVITY,
- priority = 5,
- severity = Severity.ERROR,
- implementation = Implementation(
- ModifierInspectorInfoDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
- )
- }
-}
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
deleted file mode 100644
index 25fa03f..0000000
--- a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
+++ /dev/null
@@ -1,1440 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("UnstableApiUsage")
-
-package androidx.compose.lint
-
-import androidx.compose.lint.test.Stubs
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestMode
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-/* ktlint-disable max-line-length */
-@RunWith(JUnit4::class)
-class ModifierInspectorInfoDetectorTest : LintDetectorTest() {
- override fun getDetector(): Detector = ModifierInspectorInfoDetector()
-
- override fun getIssues(): List<Issue> = listOf(ModifierInspectorInfoDetector.ISSUE)
-
- private val inspectableInfoStub = kotlin(
- """
- package androidx.compose.ui.platform
-
- import androidx.compose.ui.Modifier
-
- val NoInspectorInfo: InspectorInfo.() -> Unit = {}
- val DebugInspectorInfo = false
-
- interface InspectableValue {
- val inspectableElements: Sequence<ValueElement>
- get() = emptySequence()
-
- val nameFallback: String?
- get() = null
-
- val valueOverride: Any?
- get() = null
- }
-
- data class ValueElement(val name: String, val value: Any?)
-
- class InspectorInfo {
- var name: String? = null
- var value: Any? = null
- val properties = ValueElementSequence()
- }
-
- class ValueElementSequence : Sequence<ValueElement> {
- private val elements = mutableListOf<ValueElement>()
-
- override fun iterator(): Iterator<ValueElement> = elements.iterator()
-
- operator fun set(name: String, value: Any?) {
- elements.add(ValueElement(name, value))
- }
- }
-
- abstract class InspectorValueInfo(
- private val info: InspectorInfo.() -> Unit
- ) : InspectableValue {
- private var _values: InspectorInfo? = null
-
- private val values: InspectorInfo
- get() {
- val valueInfo = _values ?: InspectorInfo().apply { info() }
- _values = valueInfo
- return valueInfo
- }
-
- override val nameFallback: String?
- get() = values.name
-
- override val valueOverride: Any?
- get() = values.value
-
- override val inspectableElements: Sequence<ValueElement>
- get() = values.properties
- }
-
- inline fun debugInspectorInfo(
- crossinline definitions: InspectorInfo.() -> Unit
- ): InspectorInfo.() -> Unit =
- if (DebugInspectorInfo) ({ definitions() }) else NoInspectorInfo
-
- fun Modifier.inspectable(
- inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
- wrapped: Modifier
- ): Modifier = this.then(InspectableModifierImpl(inspectorInfo, wrapped))
-
- /**
- * Interface for a [Modifier] wrapped for inspector purposes.
- */
- interface InspectableModifier {
- val wrapped: Modifier
- }
-
- private class InspectableModifierImpl(
- inspectorInfo: InspectorInfo.() -> Unit,
- override val wrapped: Modifier
- ) : Modifier.Element, InspectableModifier, InspectorValueInfo(inspectorInfo) {
- override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
- wrapped.foldIn(operation(initial, this), operation)
-
- override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
- operation(this, wrapped.foldOut(initial, operation))
-
- override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
- wrapped.any(predicate)
-
- override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
- wrapped.all(predicate)
- }
- """
- ).indented()
-
- private val composedStub = kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
-
- fun Modifier.composed(
- inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
- factory: Modifier.() -> Modifier
- ): Modifier = this.then(ComposedModifier(inspectorInfo, factory))
-
- private class ComposedModifier(
- inspectorInfo: InspectorInfo.() -> Unit,
- val factory: Modifier.() -> Modifier
- ) : Modifier.Element, InspectorValueInfo(inspectorInfo)
- """
- ).indented()
-
- @Test
- fun existingInspectorInfo() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- inline class Dp(val value: Float)
-
- fun Modifier.width1(width: Dp) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["width"] = width
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo)
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithStatementsBeforeDefinition() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- inline class Dp(val value: Float)
-
- inline fun require(value: Boolean, lazyMessage: () -> String) {
- if (!value) {
- val message = lazyMessage()
- throw IllegalArgumentException(message)
- }
- }
-
- fun Modifier.width1(width: Dp): Modifier {
- require(width.value > 0.0f) { return "sds" }
-
- val x = width.value.toInt() * 2
- for (i in 0..4) {
- println("x = " + x)
- }
-
- return this.then(SizeModifier1(x, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["width"] = width
- }))
- }
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo)
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithValue() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width2(width: Int) =
- this.then(SizeModifier2(width, inspectorInfo = debugInspectorInfo {
- name = "width2"
- value = width
- }))
-
- private class SizeModifier2(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoViaSynonym() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- inline class Dp(val value: Float)
-
- fun Modifier.width1(width: Dp) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["width"] = width
- }))
-
- fun Modifier.width2(width: Dp) = width1(width)
-
- fun Modifier.width20() = width1(Dp(20.0f))
-
- fun Modifier.preferredIconWidth(x: Int) = this.then(
- if (x == 7) DefaultIconSizeModifier else Modifier
- )
-
- private val DefaultIconSizeModifier = Modifier.width1(Dp(24.0f))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo)
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithAnonymousClass() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.drawBehind() = this.then(
- object : Modifier,
- InspectorValueInfo(debugInspectorInfo { name = "drawBehind" }) {}
- )
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithDataClassMemberValues() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.border(values: Borders) =
- this.then(BorderModifier(values, inspectorInfo = debugInspectorInfo {
- name = "border"
- properties["start"] = values.start
- properties["top"] = values.top
- properties["end"] = values.end
- properties["bottom"] = values.bottom
- }))
-
- fun Modifier.border2(start: Int, top: Int, end: Int, bottom: Int) =
- this.then(
- BorderModifier2(
- start, top, end, bottom, inspectorInfo = debugInspectorInfo {
- name = "border2"
- properties["start"] = start
- properties["top"] = top
- properties["end"] = end
- properties["bottom"] = bottom
- }))
-
- fun Modifier.border2(values: Borders) =
- border2(values.start, values.top, values.end, values.bottom)
-
- fun Modifier.border3(corner1: Location, corner2: Location) =
- border2(corner1.x, corner1.y, corner2.x, corner2.y)
-
- private class BorderModifier(
- val values: Borders,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- private class BorderModifier2(
- val start: Int,
- val top: Int,
- val end: Int,
- val bottom: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- data class Borders(val start: Int, val top: Int, val end: Int, val bottom: Int) {
- constructor(all: Int) : this(all, all, all, all)
- }
-
- data class Location(val x: Int, val y: Int)
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithConditional() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.padding(size: Int) =
- this.then(
- if (size >= 10) {
- PaddingModifier(
- paddingSize = size,
- inspectorInfo = debugInspectorInfo {
- name = "padding"
- properties["size"] = size
- }
- )
- } else {
- Modifier
- }
- )
-
- fun Modifier.paddingFromBaseline(top: Int, bottom: Int) = this
- .then(if (bottom > 0) padding(bottom) else Modifier)
- .then(if (top > 0) padding(top) else Modifier)
-
- fun Modifier.paddingFromBaseline2(top: Int, bottom: Int) =
- this.padding(bottom).padding(top)
-
- private class PaddingModifier(
- paddingSize: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithWhen() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.border(painter: Painter) =
- this.then(
- when (painter.size) {
- 0 -> Modifier
- 1 -> BorderModifier(inspectorInfo = debugInspectorInfo {
- name = "border"
- properties["painter"] = painter
- })
- else -> Modifier
- }
- )
-
- private class BorderModifier(
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- class Painter(val size: Int)
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun existingInspectorInfoWithConditionals() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- class Brush
-
- class SolidColor(val color: Int): Brush()
-
- fun Modifier.border(width: Int, brush: Brush, shape: Shape): Modifier = composed(
- factory = { BorderModifier(shape, width, brush) },
- inspectorInfo = debugInspectorInfo {
- name = "border"
- properties["width"] = width
- if (brush is SolidColor) {
- properties["color"] = brush.value
- value = brush.value
- } else {
- properties["brush"] = brush
- }
- properties["shape"] = shape
- }
- )
-
- fun Modifier.border2(width: Int, color: Int, shape: Shape): Modifier =
- if (width > 0) {
- composed(
- inspectorInfo = debugInspectorInfo {
- name = "border2"
- properties["width"] = width
- properties["color"] = color
- properties["shape"] = shape
- }
- ) {
- border(width, SolidColor(color), shape)
- }
- } else {
- this
- }
-
- private class BorderModifier(shape: Shape, width: Int, brush: Brush)
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun composedModifierWithInspectorInfo() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.border(width: Int): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "border"
- properties["width"] = width
- },
- factory = { this.then(BorderModifier(width)) }
- )
-
- fun Modifier.border2(width: Int): Modifier = composed(
- factory = { this.then(BorderModifier(width)) },
- inspectorInfo = debugInspectorInfo {
- name = "border2"
- properties["width"] = width
- }
- )
-
- private class BorderModifier(private val width: Int): Modifier.Element {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun rememberModifierInfo() {
- lint().files(
- Stubs.Composable,
- Stubs.Modifier,
- composedStub,
- Stubs.Remember,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.runtime.*
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int) = this.then(
- remember {
- SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["width"] = width
- })
- }
- )
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo)
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun emptyModifier() {
- lint().files(
- Stubs.Composable,
- Stubs.Modifier,
- Stubs.Remember,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.runtime.*
- import androidx.compose.ui.Modifier
-
- internal actual fun Modifier.width1(width: Int): Modifier = this
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun acceptMissingInspectorInfoInSamples() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui.demos.whatever
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width2(width: Int) = this.then(SizeModifier2(width))
-
- private data class SizeModifier2(
- val width: Int,
- ): Modifier.Element
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun missingInspectorInfo() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.preferredWidth2(width: Int) = this.then(SizeModifier2(width))
-
- private data class SizeModifier2(
- val width: Int,
- ): Modifier.Element
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier2.kt:8: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
- fun Modifier.preferredWidth2(width: Int) = this.then(SizeModifier2(width))
- ~~~~~~~~~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun composedModifierWithMissingInspectorInfo() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.border(width: Int): Modifier =
- composed { this.then(BorderModifier(width)) }
-
- private class BorderModifier(private val width: Int): Modifier.Element {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/BorderModifier.kt:9: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
- composed { this.then(BorderModifier(width)) }
- ~~~~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun missingInspectorInfoFromInnerClassImplementation() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- /**
- * Documentation
- */
- fun Modifier.size(width: Int) =
- this.then(SizeModifier.WithOption(width))
-
- internal sealed class SizeModifier : Modifier.Element {
- internal data class WithOption(val width: Int) : SizeModifier() {
- }
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier.kt:12: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
- this.then(SizeModifier.WithOption(width))
- ~~~~~~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithWrongName() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "otherName"
- value = width
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:10: Error: Expected name of the modifier: "name" = "width1" [ModifierInspectorInfo]
- name = "otherName"
- ~~~~~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithWrongValue() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- value = 3.4
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:11: Error: Expected the variable: "width" [ModifierInspectorInfo]
- value = 3.4
- ~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithWrongValueWhenMultipleAreAvailable() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int, height: Int) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- value = "oldWidth"
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:11: Error: Expected one of the variables: "width, height" [ModifierInspectorInfo]
- value = "oldWidth"
- ~~~~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithWrongParameterNameInProperties() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int, height: Int) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["width"] = width
- properties["other"] = height
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:12: Error: Expected one of the variables: "width, height" [ModifierInspectorInfo]
- properties["other"] = height
- ~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithMismatchInProperties() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int, height: Int) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["height"] = width
- properties["width"] = height
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:11: Error: The value should match the index name: height [ModifierInspectorInfo]
- properties["height"] = width
- ~~~~~
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithMissingDebugSelector() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
-
- fun Modifier.width1(width: Int, height: Int) =
- this.then(SizeModifier1(width, height, inspectorInfo = {
- name = "width1"
- properties["width"] = width
- properties["height"] = height
- }))
-
- private class SizeModifier1(
- val width: Int,
- val height: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:8: Error: Expected debugInspectorInfo call [ModifierInspectorInfo]
- this.then(SizeModifier1(width, height, inspectorInfo = {
- ^
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithMissingName() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.width1(width: Int) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- value = width
- }))
-
- private class SizeModifier1(
- val width: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/SizeModifier1.kt:9: Error: Expected name of the modifier: "name" = "width1" [ModifierInspectorInfo]
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- ^
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithMissingVariables() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.border(start: Int, top: Int, end: Int, bottom: Int) =
- this.then(
- BorderModifier(
- start, top, end, bottom, debugInspectorInfo {
- name = "border"
- properties["start"] = start
- }
- ))
-
- private class BorderModifier(
- val start: Int,
- val top: Int,
- val end: Int,
- bottom: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/BorderModifier.kt:11: Error: These lambda arguments are missing in the InspectorInfo: bottom, end, top [ModifierInspectorInfo]
- start, top, end, bottom, debugInspectorInfo {
- ^
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun inspectorInfoWithMissingDataClassMemberValues() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.border(values: Borders) =
- this.then(BorderModifier(values, inspectorInfo = debugInspectorInfo {
- name = "border"
- value = values.start
- properties["top"] = values.top
- }))
-
- private class BorderModifier(
- val values: Borders,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier.Element, InspectorValueInfo(inspectorInfo) {
- }
-
- data class Borders(val start: Int, val top: Int, val end: Int, val bottom: Int) {
- constructor(all: Int) : this(all, all, all, all)
- }
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/BorderModifier.kt:9: Error: These lambda arguments are missing in the InspectorInfo: bottom, end [ModifierInspectorInfo]
- this.then(BorderModifier(values, inspectorInfo = debugInspectorInfo {
- ^
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun missingInfoInConditionals() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- class Brush
-
- class SolidColor(val color: Int): Brush()
-
- fun Modifier.border(width: Int): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "border"
- value = width
- }
- ) {
- BorderModifier(shape, width, brush)
- }
-
- fun Modifier.border2(width: Int): Modifier =
- if (width > 0) {
- border(width)
- } else {
- composed { BorderModifier(shape, width, brush) }
- }
-
- fun Modifier.border3(width: Int): Modifier =
- when {
- width < 0 -> this
- width < 2 -> border(width)
- width < 3 -> composed { BorderModifier(shape, width, brush) }
- else -> this
- }
-
- fun Modifier.border4(width: Int): Modifier =
- when {
- width < 0 -> this
- width < 2 -> border(width)
- else -> this.then(BorderModifier(shape, width, brush))
- }
-
- private class BorderModifier(shape: Shape, width: Int, brush: Brush): Modifier
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/androidx/compose/ui/Brush.kt:25: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
- composed { BorderModifier(shape, width, brush) }
- ~~~~~~~~
- src/androidx/compose/ui/Brush.kt:32: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
- width < 3 -> composed { BorderModifier(shape, width, brush) }
- ~~~~~~~~
- src/androidx/compose/ui/Brush.kt:40: Error: Modifier missing inspectorInfo [ModifierInspectorInfo]
- else -> this.then(BorderModifier(shape, width, brush))
- ~~~~~~~~~~~~~~
- 3 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun testInspectable() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package mypackage
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.inspectable
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.background(color: Int): Modifier = this.then(
- Background(color, inspectorInfo = debugInspectorInfo {
- name = "background"
- value = color
- })
- )
-
- fun Modifier.border(width: Int, color: Int): Modifier = this.then(
- BorderModifier(width, color, inspectorInfo = debugInspectorInfo {
- name = "border"
- properties["width"] = width
- properties["color"] = color
- })
- )
-
- fun Modifier.frame(color: Int) = this.then(
- Modifier.inspectable(
- inspectorInfo = debugInspectorInfo {
- name = "frame"
- value = color
- },
- wrapped = Modifier.background(color).border(width = 5, color = color)
- )
- )
-
- private class BackgroundModifier(
- color: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier
-
- private class BorderModifier(
- width: Int,
- color: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-
- @Test
- fun testInspectableWithMissingParameter() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package mypackage
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.inspectable
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- fun Modifier.background(color: Int): Modifier = this.then(
- Background(color, inspectorInfo = debugInspectorInfo {
- name = "background"
- value = color
- })
- )
-
- fun Modifier.border(width: Int, color: Int): Modifier = this.then(
- BorderModifier(width, color, inspectorInfo = debugInspectorInfo {
- name = "border"
- properties["width"] = width
- properties["color"] = color
- })
- )
-
- fun Modifier.frame(color: Int) = this.then(
- Modifier.inspectable(
- inspectorInfo = debugInspectorInfo {
- name = "frame"
- },
- wrapped = Modifier.background(color).border(width = 5, color = color)
- )
- )
-
- private class BackgroundModifier(
- color: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier
-
- private class BorderModifier(
- width: Int,
- color: Int,
- inspectorInfo: InspectorInfo.() -> Unit
- ): Modifier
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expect(
- """
- src/mypackage/BackgroundModifier.kt:26: Error: These lambda arguments are missing in the InspectorInfo: color [ModifierInspectorInfo]
- inspectorInfo = debugInspectorInfo {
- ^
- 1 errors, 0 warnings
- """
- )
- }
-
- @Test
- fun passInspectorInfoAtSecondLastParameter() {
- lint().files(
- Stubs.Modifier,
- composedStub,
- inspectableInfoStub,
- kotlin(
- """
- package androidx.compose.ui
-
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.platform.InspectorInfo
- import androidx.compose.ui.platform.InspectorValueInfo
- import androidx.compose.ui.platform.debugInspectorInfo
-
- inline class Dp(val value: Float)
-
- fun Modifier.width1(width: Dp, height: Dp) =
- this.then(SizeModifier1(width, inspectorInfo = debugInspectorInfo {
- name = "width1"
- properties["width"] = width
- properties["height"] = height
- }, height))
-
- private class SizeModifier1(
- val width: Dp,
- inspectorInfo: InspectorInfo.() -> Unit,
- val height: Dp
- ): Modifier.Element, InspectorValueInfo(inspectorInfo)
-
- """
- ).indented()
- )
- .testModes(TestMode.DEFAULT)
- .run()
- .expectClean()
- }
-}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index c5cbba9..86c4206 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -1207,4 +1207,40 @@
rule.waitForIdle()
assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.HalfExpanded)
}
+
+ // TODO: Migrate to manual clock mode once b/269613287 is fixed
+ @Test
+ fun modalBottomSheet_anchorChangeHandler_missingAnchor_immediatelySnapsForInitialization() {
+ val stateRestorationTester = StateRestorationTester(rule)
+
+ // Not backed by state as we don't want changes to cause recompositions
+ var sheetState = ModalBottomSheetState(ModalBottomSheetValue.HalfExpanded)
+ var tallSheetContent = true
+
+ stateRestorationTester.setContent {
+ ModalBottomSheetLayout(
+ sheetState = sheetState,
+ sheetContent = {
+ Box(Modifier.fillMaxHeight(if (tallSheetContent) 1f else 0.4f))
+ },
+ content = { Box(Modifier.fillMaxSize()) }
+ )
+ }
+
+ assertThat(sheetState.hasHalfExpandedState).isTrue()
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.HalfExpanded)
+
+ tallSheetContent = false
+ // Recreate the sheet state so it doesn't have anchors or an offset yet
+ sheetState = ModalBottomSheetState(ModalBottomSheetValue.HalfExpanded)
+
+ assertThat(sheetState.swipeableState.anchors).isEmpty()
+ assertThat(sheetState.swipeableState.offset).isNull()
+
+ stateRestorationTester.emulateSavedInstanceStateRestore()
+ rule.waitForIdle()
+
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+ assertThat(sheetState.hasHalfExpandedState).isFalse()
+ }
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
index 0e04ef0..5fcdcc9 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
@@ -32,6 +32,7 @@
import androidx.compose.material.swipeable.TestState.B
import androidx.compose.material.swipeable.TestState.C
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -52,6 +53,8 @@
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
@@ -441,4 +444,38 @@
shouldInvokeChangeHandler = state.updateAnchors(mapOf(A to 100f, B to 500f, C to 700f))
assertThat(shouldInvokeChangeHandler).isTrue()
}
+
+ @Test
+ fun swipeable_updateAnchors_ongoingOffsetMutation_shouldNotUpdate() = runBlocking {
+ val clock = HandPumpTestFrameClock()
+ val animationScope = CoroutineScope(clock)
+ val animationDuration = 2000
+ val state = SwipeableV2State(initialValue = A, animationSpec = tween(animationDuration))
+ val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+ state.updateAnchors(anchors)
+ animationScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ state.animateTo(B)
+ }
+ clock.advanceByFrame()
+
+ assertThat(state.isAnimationRunning).isTrue()
+
+ val offsetBeforeAnchorUpdate = state.offset
+ val shouldInvokeChangeHandler = state.updateAnchors(mapOf(A to 100f, B to 500f, C to 700f))
+ assertThat(offsetBeforeAnchorUpdate).isEqualTo(state.offset)
+ assertThat(shouldInvokeChangeHandler).isTrue()
+ }
+
+ private class HandPumpTestFrameClock : MonotonicFrameClock {
+ private val frameCh = Channel<Long>(1)
+
+ suspend fun advanceByFrame() {
+ frameCh.send(16_000_000L)
+ }
+
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
+ return onFrame(frameCh.receive())
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt
index 220666b..87ad9ab 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt
@@ -44,7 +44,6 @@
* sure there is adequate space for touch target expansion.
*/
@OptIn(ExperimentalMaterialApi::class)
-@Suppress("ModifierInspectorInfo")
fun Modifier.minimumInteractiveComponentSize(): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "minimumInteractiveComponentSize"
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InternalMutatorMutex.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InternalMutatorMutex.kt
new file mode 100644
index 0000000..326c77e
--- /dev/null
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InternalMutatorMutex.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material
+
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.runtime.Stable
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/*** This is an internal copy of androidx.compose.foundation.MutatorMutex with an additional
+ * tryMutate method. Do not modify, except for tryMutate. ***/
+
+expect class InternalAtomicReference<V>(value: V) {
+ fun get(): V
+ fun set(value: V)
+ fun getAndSet(value: V): V
+ fun compareAndSet(expect: V, newValue: V): Boolean
+}
+
+/**
+ * Mutual exclusion for UI state mutation over time.
+ *
+ * [mutate] permits interruptible state mutation over time using a standard [MutatePriority].
+ * A [InternalMutatorMutex] enforces that only a single writer can be active at a time for a particular
+ * state resource. Instead of queueing callers that would acquire the lock like a traditional
+ * [Mutex], new attempts to [mutate] the guarded state will either cancel the current mutator or
+ * if the current mutator has a higher priority, the new caller will throw [CancellationException].
+ *
+ * [InternalMutatorMutex] should be used for implementing hoisted state objects that many mutators may
+ * want to manipulate over time such that those mutators can coordinate with one another. The
+ * [InternalMutatorMutex] instance should be hidden as an implementation detail. For example:
+ *
+ */
+@Stable
+internal class InternalMutatorMutex {
+ private class Mutator(val priority: MutatePriority, val job: Job) {
+ fun canInterrupt(other: Mutator) = priority >= other.priority
+
+ fun cancel() = job.cancel()
+ }
+
+ private val currentMutator = InternalAtomicReference<Mutator?>(null)
+ private val mutex = Mutex()
+
+ private fun tryMutateOrCancel(mutator: Mutator) {
+ while (true) {
+ val oldMutator = currentMutator.get()
+ if (oldMutator == null || mutator.canInterrupt(oldMutator)) {
+ if (currentMutator.compareAndSet(oldMutator, mutator)) {
+ oldMutator?.cancel()
+ break
+ }
+ } else throw CancellationException("Current mutation had a higher priority")
+ }
+ }
+
+ /**
+ * Enforce that only a single caller may be active at a time.
+ *
+ * If [mutate] is called while another call to [mutate] or [mutateWith] is in progress, their
+ * [priority] values are compared. If the new caller has a [priority] equal to or higher than
+ * the call in progress, the call in progress will be cancelled, throwing
+ * [CancellationException] and the new caller's [block] will be invoked. If the call in
+ * progress had a higher [priority] than the new caller, the new caller will throw
+ * [CancellationException] without invoking [block].
+ *
+ * @param priority the priority of this mutation; [MutatePriority.Default] by default.
+ * Higher priority mutations will interrupt lower priority mutations.
+ * @param block mutation code to run mutually exclusive with any other call to [mutate],
+ * [mutateWith] or [tryMutate].
+ */
+ suspend fun <R> mutate(
+ priority: MutatePriority = MutatePriority.Default,
+ block: suspend () -> R
+ ) = coroutineScope {
+ val mutator = Mutator(priority, coroutineContext[Job]!!)
+
+ tryMutateOrCancel(mutator)
+
+ mutex.withLock {
+ try {
+ block()
+ } finally {
+ currentMutator.compareAndSet(mutator, null)
+ }
+ }
+ }
+
+ /**
+ * Enforce that only a single caller may be active at a time.
+ *
+ * If [mutateWith] is called while another call to [mutate] or [mutateWith] is in progress,
+ * their [priority] values are compared. If the new caller has a [priority] equal to or
+ * higher than the call in progress, the call in progress will be cancelled, throwing
+ * [CancellationException] and the new caller's [block] will be invoked. If the call in
+ * progress had a higher [priority] than the new caller, the new caller will throw
+ * [CancellationException] without invoking [block].
+ *
+ * This variant of [mutate] calls its [block] with a [receiver], removing the need to create
+ * an additional capturing lambda to invoke it with a receiver object. This can be used to
+ * expose a mutable scope to the provided [block] while leaving the rest of the state object
+ * read-only. For example:
+ *
+ * @param receiver the receiver `this` that [block] will be called with
+ * @param priority the priority of this mutation; [MutatePriority.Default] by default.
+ * Higher priority mutations will interrupt lower priority mutations.
+ * @param block mutation code to run mutually exclusive with any other call to [mutate],
+ * [mutateWith] or [tryMutate].
+ */
+ suspend fun <T, R> mutateWith(
+ receiver: T,
+ priority: MutatePriority = MutatePriority.Default,
+ block: suspend T.() -> R
+ ) = coroutineScope {
+ val mutator = Mutator(priority, coroutineContext[Job]!!)
+
+ tryMutateOrCancel(mutator)
+
+ mutex.withLock {
+ try {
+ receiver.block()
+ } finally {
+ currentMutator.compareAndSet(mutator, null)
+ }
+ }
+ }
+
+ /**
+ * Attempt to mutate synchronously if there is no other active caller.
+ * If there is no other active caller, the [block] will be executed in a lock. If there is
+ * another active caller, this method will return false, indicating that the active caller
+ * needs to be cancelled through a [mutate] or [mutateWith] call with an equal or higher
+ * mutation priority.
+ *
+ * Calls to [mutate] and [mutateWith] will suspend until execution of the [block] has finished.
+ *
+ * @param block mutation code to run mutually exclusive with any other call to [mutate],
+ * [mutateWith] or [tryMutate].
+ * @return true if the [block] was executed, false if there was another active caller and the
+ * [block] was not executed.
+ */
+ fun tryMutate(block: () -> Unit): Boolean {
+ val didLock = mutex.tryLock()
+ if (didLock) {
+ try {
+ block()
+ } finally {
+ mutex.unlock()
+ }
+ }
+ return didLock
+ }
+}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 5ea9086..5bd062c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -245,6 +245,10 @@
internal suspend fun snapTo(target: ModalBottomSheetValue) = swipeableState.snapTo(target)
+ internal fun trySnapTo(target: ModalBottomSheetValue): Boolean {
+ return swipeableState.trySnapTo(target)
+ }
+
internal fun requireOffset() = swipeableState.requireOffset()
internal val lastVelocity: Float get() = swipeableState.lastVelocity
@@ -448,7 +452,10 @@
animateTo = { target, velocity ->
scope.launch { sheetState.animateTo(target, velocity = velocity) }
},
- snapTo = { target -> scope.launch { sheetState.snapTo(target) } }
+ snapTo = { target ->
+ val didSnapSynchronously = sheetState.trySnapTo(target)
+ if (!didSnapSynchronously) scope.launch { sheetState.snapTo(target) }
+ }
)
}
BoxWithConstraints(modifier) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index ec40501..d5d27ae 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -19,6 +19,8 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.DragScope
import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
@@ -48,6 +50,7 @@
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
@@ -77,7 +80,7 @@
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null
) = draggable(
- state = state.draggableState,
+ state = state.swipeDraggableState,
orientation = orientation,
enabled = enabled,
interactionSource = interactionSource,
@@ -120,7 +123,11 @@
val previousTarget = state.targetValue
val stateRequiresCleanup = state.updateAnchors(newAnchors)
if (stateRequiresCleanup) {
- anchorChangeHandler?.onAnchorsChanged(previousTarget, previousAnchors, newAnchors)
+ anchorChangeHandler?.onAnchorsChanged(
+ previousTarget,
+ previousAnchors,
+ newAnchors
+ )
}
}
},
@@ -163,6 +170,27 @@
internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
) {
+ private val swipeMutex = InternalMutatorMutex()
+
+ internal val swipeDraggableState = object : DraggableState {
+ private val dragScope = object : DragScope {
+ override fun dragBy(pixels: Float) {
+ this@SwipeableV2State.dispatchRawDelta(pixels)
+ }
+ }
+
+ override suspend fun drag(
+ dragPriority: MutatePriority,
+ block: suspend DragScope.() -> Unit
+ ) {
+ swipe(dragPriority) { dragScope.block() }
+ }
+
+ override fun dispatchRawDelta(delta: Float) {
+ this@SwipeableV2State.dispatchRawDelta(delta)
+ }
+ }
+
/**
* The current value of the [SwipeableV2State].
*/
@@ -253,9 +281,6 @@
val maxOffset by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
private var animationTarget: T? by mutableStateOf(null)
- internal val draggableState = DraggableState {
- offset = ((offset ?: 0f) + it).coerceIn(minOffset, maxOffset)
- }
internal var anchors by mutableStateOf(emptyMap<T, Float>())
@@ -274,9 +299,10 @@
val previousAnchorsEmpty = anchors.isEmpty()
anchors = newAnchors
val initialValueHasAnchor = if (previousAnchorsEmpty) {
- val initialValueAnchor = anchors[currentValue]
+ val initialValue = currentValue
+ val initialValueAnchor = anchors[initialValue]
val initialValueHasAnchor = initialValueAnchor != null
- if (initialValueHasAnchor) offset = initialValueAnchor
+ if (initialValueHasAnchor) trySnapTo(initialValue)
initialValueHasAnchor
} else true
return !initialValueHasAnchor || !previousAnchorsEmpty
@@ -298,20 +324,7 @@
* @param targetValue The target value of the animation
*/
suspend fun snapTo(targetValue: T) {
- val targetOffset = anchors[targetValue]
- if (targetOffset != null) {
- try {
- draggableState.drag {
- animationTarget = targetValue
- dragBy(targetOffset - requireOffset())
- }
- this.currentValue = targetValue
- } finally {
- animationTarget = null
- }
- } else {
- currentValue = targetValue
- }
+ swipe { snap(targetValue) }
}
/**
@@ -332,7 +345,7 @@
val targetOffset = anchors[targetValue]
if (targetOffset != null) {
try {
- draggableState.drag {
+ swipe {
animationTarget = targetValue
var prev = offset ?: 0f
animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
@@ -379,17 +392,17 @@
}
/**
- * Swipe by the [delta], coerce it in the bounds and dispatch it to the [draggableState].
+ * Swipe by the [delta], coerce it in the bounds and dispatch it to the [SwipeableV2State].
*
- * @return The delta the [draggableState] will consume
+ * @return The delta the consumed by the [SwipeableV2State]
*/
fun dispatchRawDelta(delta: Float): Float {
val currentDragPosition = offset ?: 0f
val potentiallyConsumed = currentDragPosition + delta
val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
val deltaToConsume = clamped - currentDragPosition
- if (abs(deltaToConsume) > 0) {
- draggableState.dispatchRawDelta(deltaToConsume)
+ if (abs(deltaToConsume) >= 0) {
+ offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
}
return deltaToConsume
}
@@ -441,6 +454,31 @@
"this=$this SwipeableState?"
}
+ private suspend fun swipe(
+ swipePriority: MutatePriority = MutatePriority.Default,
+ action: suspend () -> Unit
+ ): Unit = coroutineScope { swipeMutex.mutate(swipePriority, action) }
+
+ /**
+ * Attempt to snap synchronously. Snapping can happen synchronously when there is no other swipe
+ * transaction like a drag or an animation is progress. If there is another interaction in
+ * progress, the suspending [snapTo] overload needs to be used.
+ *
+ * @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
+ */
+ internal fun trySnapTo(targetValue: T): Boolean = swipeMutex.tryMutate { snap(targetValue) }
+
+ private fun snap(targetValue: T) {
+ val targetOffset = anchors[targetValue]
+ if (targetOffset != null) {
+ dispatchRawDelta(targetOffset - (offset ?: 0f))
+ currentValue = targetValue
+ animationTarget = null
+ } else {
+ currentValue = targetValue
+ }
+ }
+
companion object {
/**
* The default [Saver] implementation for [SwipeableV2State].
@@ -649,6 +687,3 @@
private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
- "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
-}
diff --git a/compose/material/material/src/jvmMain/kotlin/androidx/compose/material/ActualJvm.kt b/compose/material/material/src/jvmMain/kotlin/androidx/compose/material/ActualJvm.kt
new file mode 100644
index 0000000..307fd82
--- /dev/null
+++ b/compose/material/material/src/jvmMain/kotlin/androidx/compose/material/ActualJvm.kt
@@ -0,0 +1,22 @@
+// ktlint-disable filename
+
+/*
+ * 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.material
+
+internal actual typealias InternalAtomicReference<V> =
+ java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt b/compose/material/material/src/test/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt
new file mode 100644
index 0000000..b603846
--- /dev/null
+++ b/compose/material/material/src/test/kotlin/androidx/compose/material/InternalMutatorMutexTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material
+
+import androidx.compose.foundation.MutatePriority
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("RemoveExplicitTypeArguments")
+@RunWith(JUnit4::class)
+internal class InternalMutatorMutexTest {
+ interface MutateCaller {
+ suspend fun <R> mutate(
+ priority: MutatePriority = MutatePriority.Default,
+ block: suspend () -> R
+ ): R
+ }
+
+ class MutateWithoutReceiverCaller(private val mutex: InternalMutatorMutex) : MutateCaller {
+ override suspend fun <R> mutate(
+ priority: MutatePriority,
+ block: suspend () -> R
+ ): R = mutex.mutate(priority, block)
+ }
+
+ class MutateWithReceiverCaller(private val mutex: InternalMutatorMutex) : MutateCaller {
+ override suspend fun <R> mutate(
+ priority: MutatePriority,
+ block: suspend () -> R
+ ): R {
+ val receiver = Any()
+ return mutex.mutateWith(receiver, priority) {
+ assertSame("mutateWith receiver", receiver, this)
+ block()
+ }
+ }
+ }
+
+ @Test
+ fun newMutatorCancelsOld() = runBlocking<Unit> {
+ val mutex = InternalMutatorMutex()
+ runNewMutatorCancelsOld(MutateWithoutReceiverCaller(mutex))
+ runNewMutatorCancelsOld(MutateWithReceiverCaller(mutex))
+ }
+
+ private suspend fun runNewMutatorCancelsOld(mutex: MutateCaller) = coroutineScope<Unit> {
+ val firstMutatorJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ mutex.mutate {
+ // Suspend forever
+ suspendCancellableCoroutine<Unit> { }
+ }
+ fail("mutator should have thrown CancellationException")
+ }
+
+ // Cancel firstMutatorJob
+ mutex.mutate { }
+ assertTrue("first mutator was cancelled", firstMutatorJob.isCancelled)
+ }
+
+ @Test
+ fun mutatorsCancelByPriority() = runBlocking<Unit> {
+ val mutex = InternalMutatorMutex()
+ runMutatorsCancelByPriority(MutateWithoutReceiverCaller(mutex))
+ runMutatorsCancelByPriority(MutateWithReceiverCaller(mutex))
+ }
+
+ @Test
+ fun tryMutateBlockingSuspendsSubsequentMutate() = runBlocking<Unit> {
+ val mutex = InternalMutatorMutex()
+ val tryMutateJob = launch(start = CoroutineStart.LAZY) {
+ mutex.tryMutate {
+ while (true) { /* Block forever */ }
+ }
+ }
+ val mutateJob = launch(start = CoroutineStart.LAZY) {
+ mutex.mutate {
+ if (tryMutateJob.isActive) fail("Attempted to mutate before tryMutate finished")
+ }
+ }
+ tryMutateJob.start()
+ mutateJob.start()
+
+ tryMutateJob.cancelAndJoin()
+ mutateJob.cancelAndJoin()
+ }
+
+ @Test
+ fun tryMutateDoesNotOverrideActiveCaller() = runBlocking<Unit> {
+ val mutex = InternalMutatorMutex()
+ val mutateJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ mutex.mutate {
+ suspendCancellableCoroutine { } // Suspend forever
+ }
+ }
+ val tryMutateSuccessful = mutex.tryMutate { }
+ Assert.assertFalse(
+ "tryMutate should not run if there is an ongoing mutation",
+ tryMutateSuccessful
+ )
+ mutateJob.cancelAndJoin()
+ }
+
+ @Test
+ fun tryMutateBlockingTryMutateLocks() = runBlocking<Unit> {
+ val mutex = InternalMutatorMutex()
+ mutex.tryMutate {
+ val tryMutateSuccessful = mutex.tryMutate { }
+ Assert.assertFalse(
+ "tryMutate should not run if there is an ongoing mutation",
+ tryMutateSuccessful
+ )
+ }
+ }
+
+ private suspend fun runMutatorsCancelByPriority(mutex: MutateCaller) = coroutineScope<Unit> {
+ for (firstPriority in MutatePriority.values()) {
+ for (secondPriority in MutatePriority.values()) {
+ val firstMutatorJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ mutex.mutate(firstPriority) {
+ // Suspend forever
+ suspendCancellableCoroutine<Unit> { }
+ }
+ fail("mutator should have thrown CancellationException")
+ }
+
+ // Attempt mutation and (maybe) cause cancellation
+ try {
+ mutex.mutate(secondPriority) { }
+ } catch (ce: CancellationException) {
+ assertTrue(
+ "attempted second mutation was cancelled with lower priority",
+ secondPriority < firstPriority
+ )
+ }
+ assertEquals(
+ "first mutator of priority $firstPriority cancelled by second " +
+ "mutator of priority $secondPriority",
+ secondPriority >= firstPriority,
+ firstMutatorJob.isCancelled
+ )
+
+ // Cleanup regardless of results
+ firstMutatorJob.cancel()
+ firstMutatorJob.join()
+ }
+ }
+ }
+}
diff --git a/compose/material3/material3/samples/build.gradle b/compose/material3/material3/samples/build.gradle
index 64bc8f5..9cf5a69 100644
--- a/compose/material3/material3/samples/build.gradle
+++ b/compose/material3/material3/samples/build.gradle
@@ -41,9 +41,9 @@
implementation("androidx.compose.ui:ui:1.2.1")
implementation("androidx.compose.ui:ui-text:1.2.1")
implementation("androidx.savedstate:savedstate-ktx:1.2.0")
- implementation(project(":compose:ui:ui-tooling-preview"))
+ implementation("androidx.compose.ui:ui-tooling-preview:1.4.0-beta02")
- debugImplementation(project(":compose:ui:ui-tooling"))
+ debugImplementation("androidx.compose.ui:ui-tooling:1.4.0-beta02")
}
androidx {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
index 47dd16b..6094e85 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
@@ -65,6 +65,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -851,23 +852,19 @@
rule
.setMaterialContentForSizeAssertions {
CompositionLocalProvider(
- LocalDensity provides
- Density(
- density = LocalDensity.current.density,
- fontScale = 2.0f
- )
+ LocalDensity provides Density(density = 1f, fontScale = 10f)
) {
Surface {
Tab(
selected = true,
onClick = {},
- text = { Text("Text") },
+ text = { Text(text = "Text", fontSize = 10.sp) },
icon = { Icon(icon, null) }
)
}
}
}
- .assertHeightIsAtLeast(100.dp)
+ .assertHeightIsAtLeast(90.dp)
}
@Test
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index 1f8a7ed..65f9107 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -156,6 +156,15 @@
Strings.BottomSheetDragHandleDescription -> resources.getString(
androidx.compose.material3.R.string.bottom_sheet_drag_handle_description
)
+ Strings.BottomSheetCollapseDescription -> resources.getString(
+ androidx.compose.material3.R.string.bottom_sheet_collapse_description
+ )
+ Strings.BottomSheetDismissDescription -> resources.getString(
+ androidx.compose.material3.R.string.bottom_sheet_dismiss_description
+ )
+ Strings.BottomSheetExpandDescription -> resources.getString(
+ androidx.compose.material3.R.string.bottom_sheet_expand_description
+ )
Strings.TooltipLongPressLabel -> resources.getString(
androidx.compose.material3.R.string.tooltip_long_press_label
)
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index 9480205..ac0c807 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -74,6 +74,12 @@
<string name="date_range_input_invalid_range_input">Invalid date range input</string>
<!-- Names the drag handle visual for bottom sheet. -->
<string name="bottom_sheet_drag_handle_description">Drag handle</string>
+ <!-- Describes the collapse action for bottom sheet. -->
+ <string name="bottom_sheet_collapse_description">Collapse bottom sheet</string>
+ <!-- Describes the dismiss action for bottom sheet. -->
+ <string name="bottom_sheet_dismiss_description">Dismiss bottom sheet</string>
+ <!-- Describes the expand action visual for bottom sheet. -->
+ <string name="bottom_sheet_expand_description">Expand bottom sheet</string>
<!-- Spoken description of a tooltip -->
<string name="tooltip_pane_description">Tooltip</string>
<string name="tooltip_long_press_label">Show tooltip</string>
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
index a5de613..da9bf4d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
@@ -261,6 +261,8 @@
) {
Column(Modifier.fillMaxWidth()) {
if (dragHandle != null) {
+ val collapseActionLabel = getString(Strings.BottomSheetCollapseDescription)
+ val expandActionLabel = getString(Strings.BottomSheetExpandDescription)
Box(Modifier
.align(CenterHorizontally)
.semantics {
@@ -270,11 +272,15 @@
if (swipeableState.anchors.size > 1 && sheetSwipeEnabled) {
if (currentValue == PartiallyExpanded) {
if (swipeableState.confirmValueChange(Expanded)) {
- expand { scope.launch { expand() }; true }
+ expand(expandActionLabel) {
+ scope.launch { expand() }; true
+ }
}
} else {
if (swipeableState.confirmValueChange(PartiallyExpanded)) {
- collapse { scope.launch { partialExpand() }; true }
+ collapse(collapseActionLabel) {
+ scope.launch { partialExpand() }; true
+ }
}
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt
index 4597f577..31e307b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt
@@ -44,7 +44,6 @@
* sure there is adequate space for touch target expansion.
*/
@OptIn(ExperimentalMaterial3Api::class)
-@Suppress("ModifierInspectorInfo")
fun Modifier.minimumInteractiveComponentSize(): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "minimumInteractiveComponentSize"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
index 6e816cd..31776bd 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -183,25 +183,28 @@
) {
Column(Modifier.fillMaxWidth()) {
if (dragHandle != null) {
+ val collapseActionLabel = getString(Strings.BottomSheetCollapseDescription)
+ val dismissActionLabel = getString(Strings.BottomSheetDismissDescription)
+ val expandActionLabel = getString(Strings.BottomSheetExpandDescription)
Box(Modifier
.align(Alignment.CenterHorizontally)
.semantics {
// Provides semantics to interact with the bottomsheet based on its
// current value.
with(sheetState) {
- dismiss {
+ dismiss(dismissActionLabel) {
animateToDismiss()
true
}
if (currentValue == PartiallyExpanded) {
- expand {
+ expand(expandActionLabel) {
if (swipeableState.confirmValueChange(Expanded)) {
scope.launch { sheetState.expand() }
}
true
}
} else if (hasPartiallyExpandedState) {
- collapse {
+ collapse(collapseActionLabel) {
if (
swipeableState.confirmValueChange(PartiallyExpanded)
) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index 2d12676..54a96ae 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -74,6 +74,9 @@
val DateRangeInputTitle = Strings()
val DateRangeInputInvalidRangeInput = Strings()
val BottomSheetDragHandleDescription = Strings()
+ val BottomSheetCollapseDescription = Strings()
+ val BottomSheetDismissDescription = Strings()
+ val BottomSheetExpandDescription = Strings()
val TooltipLongPressLabel = Strings()
val TimePickerAM = Strings()
val TimePickerPM = Strings()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
index 6a50f07..f58f0d5 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
@@ -70,6 +70,9 @@
Strings.DateRangeInputTitle -> "Enter dates"
Strings.DateRangeInputInvalidRangeInput -> "Invalid date range input"
Strings.BottomSheetDragHandleDescription -> "Drag Handle"
+ Strings.BottomSheetCollapseDescription -> "Collapse bottom sheet"
+ Strings.BottomSheetDismissDescription -> "Dismiss bottom sheet"
+ Strings.BottomSheetExpandDescription -> "Expand bottom sheet"
Strings.TooltipLongPressLabel -> "Show tooltip"
Strings.TimePickerAM -> "AM"
Strings.TimePickerPM -> "PM"
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotMutableState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotMutableState.kt
index 001c0a0..d14b04d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotMutableState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotMutableState.kt
@@ -29,6 +29,9 @@
* [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
* will be scheduled. Writes to it are transacted as part of the [Snapshot] system.
*
+ * In general for correctness: Anything that is mutable, that is read during composition or written
+ * to during composition, should be a [SnapshotMutableState].
+ *
* @see [State]
* @see [MutableState]
* @see [mutableStateOf]
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 79176a9..2916991 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -3314,6 +3314,103 @@
revalidate()
}
+ @Test
+ fun test_returnConditionally_fromInlineLambda() = compositionTest {
+ var condition by mutableStateOf(true)
+
+ compose {
+ InlineWrapper {
+ if (condition) return@InlineWrapper
+ Text("Test")
+ }
+ }
+
+ validate {
+ if (!condition) {
+ Text("Test")
+ }
+ }
+
+ condition = false
+
+ expectChanges()
+
+ revalidate()
+ }
+
+ @Test
+ fun test_returnConditionally_fromInlineLambda_nonLocal() = compositionTest {
+ var condition by mutableStateOf(true)
+
+ compose {
+ InlineWrapper {
+ M1 {
+ if (condition) return@InlineWrapper
+ Text("Test")
+ }
+ }
+ }
+
+ validate {
+ if (!condition) {
+ Text("Test")
+ }
+ }
+
+ condition = false
+
+ expectChanges()
+
+ revalidate()
+ }
+
+ @Test
+ fun test_returnConditionally_fromLambda_nonLocal() = compositionTest {
+ var condition by mutableStateOf(true)
+
+ compose {
+ Wrap {
+ M1 {
+ if (condition) return@Wrap
+ Text("Test")
+ }
+ }
+ }
+
+ validate {
+ if (!condition) {
+ Text("Test")
+ }
+ }
+
+ condition = false
+
+ expectChanges()
+
+ revalidate()
+ }
+
+ @Test
+ fun test_returnConditionally_fromFunction_nonLocal() = compositionTest {
+ val text = mutableStateOf<String?>(null)
+
+ compose {
+ TextWithNonLocalReturn(text.value)
+ }
+
+ validate {
+ if (text.value != null) {
+ Text(text.value!!)
+ }
+ }
+
+ text.value = "Test"
+
+ expectChanges()
+
+ revalidate()
+ }
+
@Test // regression test for 267586102
fun test_remember_in_a_loop() = compositionTest {
var i1 = 0
@@ -3592,7 +3689,9 @@
private inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
@Composable
-private inline fun M2(content: @Composable () -> Unit) = M1 { content() }
-
-@Composable
-private inline fun M3(content: @Composable () -> Unit) = M2 { content() }
+private fun TextWithNonLocalReturn(text: String?) {
+ InlineWrapper {
+ if (text == null) return
+ Text(text)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
index 97ecc6d..b9546e2 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathNode.kt
@@ -143,159 +143,225 @@
}
/**
- * Return the corresponding [PathNode] for the given character key if it exists.
+ * Adds the corresponding [PathNode] for the given character key, if it exists, to [nodes].
* If the key is unknown then [IllegalArgumentException] is thrown
- * @return [PathNode] that matches the key
* @throws IllegalArgumentException
*/
-internal fun Char.toPathNodes(args: FloatArray): List<PathNode> = when (this) {
- RelativeCloseKey, CloseKey -> listOf(PathNode.Close)
- RelativeMoveToKey -> pathNodesFromArgs(args, NUM_MOVE_TO_ARGS) { array ->
- PathNode.RelativeMoveTo(dx = array[0], dy = array[1])
- }
+internal fun Char.addPathNodes(nodes: MutableList<PathNode>, args: FloatArray, count: Int) {
+ when (this) {
+ RelativeCloseKey, CloseKey -> nodes.add(PathNode.Close)
- MoveToKey -> pathNodesFromArgs(args, NUM_MOVE_TO_ARGS) { array ->
- PathNode.MoveTo(x = array[0], y = array[1])
- }
+ RelativeMoveToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_MOVE_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeMoveTo(dx = array[start], dy = array[start + 1])
+ }
- RelativeLineToKey -> pathNodesFromArgs(args, NUM_LINE_TO_ARGS) { array ->
- PathNode.RelativeLineTo(dx = array[0], dy = array[1])
- }
+ MoveToKey -> pathNodesFromArgs(nodes, args, count, NUM_MOVE_TO_ARGS) { array, start ->
+ PathNode.MoveTo(x = array[start], y = array[start + 1])
+ }
- LineToKey -> pathNodesFromArgs(args, NUM_LINE_TO_ARGS) { array ->
- PathNode.LineTo(x = array[0], y = array[1])
- }
+ RelativeLineToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_LINE_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeLineTo(dx = array[start], dy = array[start + 1])
+ }
- RelativeHorizontalToKey -> pathNodesFromArgs(args, NUM_HORIZONTAL_TO_ARGS) { array ->
- PathNode.RelativeHorizontalTo(dx = array[0])
- }
+ LineToKey -> pathNodesFromArgs(nodes, args, count, NUM_LINE_TO_ARGS) { array, start ->
+ PathNode.LineTo(x = array[start], y = array[start + 1])
+ }
- HorizontalToKey -> pathNodesFromArgs(args, NUM_HORIZONTAL_TO_ARGS) { array ->
- PathNode.HorizontalTo(x = array[0])
- }
+ RelativeHorizontalToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_HORIZONTAL_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeHorizontalTo(dx = array[start])
+ }
- RelativeVerticalToKey -> pathNodesFromArgs(args, NUM_VERTICAL_TO_ARGS) { array ->
- PathNode.RelativeVerticalTo(dy = array[0])
- }
+ HorizontalToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_HORIZONTAL_TO_ARGS
+ ) { array, start ->
+ PathNode.HorizontalTo(x = array[start])
+ }
- VerticalToKey -> pathNodesFromArgs(args, NUM_VERTICAL_TO_ARGS) { array ->
- PathNode.VerticalTo(y = array[0])
- }
+ RelativeVerticalToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_VERTICAL_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeVerticalTo(dy = array[start])
+ }
- RelativeCurveToKey -> pathNodesFromArgs(args, NUM_CURVE_TO_ARGS) { array ->
- PathNode.RelativeCurveTo(
- dx1 = array[0],
- dy1 = array[1],
- dx2 = array[2],
- dy2 = array[3],
- dx3 = array[4],
- dy3 = array[5]
- )
- }
+ VerticalToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_VERTICAL_TO_ARGS
+ ) { array, start ->
+ PathNode.VerticalTo(y = array[start])
+ }
- CurveToKey -> pathNodesFromArgs(args, NUM_CURVE_TO_ARGS) { array ->
- PathNode.CurveTo(
- x1 = array[0],
- y1 = array[1],
- x2 = array[2],
- y2 = array[3],
- x3 = array[4],
- y3 = array[5]
- )
- }
+ RelativeCurveToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_CURVE_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeCurveTo(
+ dx1 = array[start],
+ dy1 = array[start + 1],
+ dx2 = array[start + 2],
+ dy2 = array[start + 3],
+ dx3 = array[start + 4],
+ dy3 = array[start + 5]
+ )
+ }
- RelativeReflectiveCurveToKey -> pathNodesFromArgs(args, NUM_REFLECTIVE_CURVE_TO_ARGS) { array ->
- PathNode.RelativeReflectiveCurveTo(
- dx1 = array[0],
- dy1 = array[1],
- dx2 = array[2],
- dy2 = array[3]
- )
- }
+ CurveToKey -> pathNodesFromArgs(nodes, args, count, NUM_CURVE_TO_ARGS) { array, start ->
+ PathNode.CurveTo(
+ x1 = array[start],
+ y1 = array[start + 1],
+ x2 = array[start + 2],
+ y2 = array[start + 3],
+ x3 = array[start + 4],
+ y3 = array[start + 5]
+ )
+ }
- ReflectiveCurveToKey -> pathNodesFromArgs(args, NUM_REFLECTIVE_CURVE_TO_ARGS) { array ->
- PathNode.ReflectiveCurveTo(
- x1 = array[0],
- y1 = array[1],
- x2 = array[2],
- y2 = array[3]
- )
- }
+ RelativeReflectiveCurveToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_REFLECTIVE_CURVE_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeReflectiveCurveTo(
+ dx1 = array[start],
+ dy1 = array[start + 1],
+ dx2 = array[start + 2],
+ dy2 = array[start + 3]
+ )
+ }
- RelativeQuadToKey -> pathNodesFromArgs(args, NUM_QUAD_TO_ARGS) { array ->
- PathNode.RelativeQuadTo(
- dx1 = array[0],
- dy1 = array[1],
- dx2 = array[2],
- dy2 = array[3]
- )
- }
+ ReflectiveCurveToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_REFLECTIVE_CURVE_TO_ARGS
+ ) { array, start ->
+ PathNode.ReflectiveCurveTo(
+ x1 = array[start],
+ y1 = array[start + 1],
+ x2 = array[start + 2],
+ y2 = array[start + 3]
+ )
+ }
- QuadToKey -> pathNodesFromArgs(args, NUM_QUAD_TO_ARGS) { array ->
- PathNode.QuadTo(
- x1 = array[0],
- y1 = array[1],
- x2 = array[2],
- y2 = array[3]
- )
- }
+ RelativeQuadToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_QUAD_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeQuadTo(
+ dx1 = array[start],
+ dy1 = array[start + 1],
+ dx2 = array[start + 2],
+ dy2 = array[start + 3]
+ )
+ }
- RelativeReflectiveQuadToKey -> pathNodesFromArgs(args, NUM_REFLECTIVE_QUAD_TO_ARGS) { array ->
- PathNode.RelativeReflectiveQuadTo(dx = array[0], dy = array[1])
- }
+ QuadToKey -> pathNodesFromArgs(nodes, args, count, NUM_QUAD_TO_ARGS) { array, start ->
+ PathNode.QuadTo(
+ x1 = array[start],
+ y1 = array[start + 1],
+ x2 = array[start + 2],
+ y2 = array[start + 3]
+ )
+ }
- ReflectiveQuadToKey -> pathNodesFromArgs(args, NUM_REFLECTIVE_QUAD_TO_ARGS) { array ->
- PathNode.ReflectiveQuadTo(x = array[0], y = array[1])
- }
+ RelativeReflectiveQuadToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_REFLECTIVE_QUAD_TO_ARGS
+ ) { array, start ->
+ PathNode.RelativeReflectiveQuadTo(dx = array[start], dy = array[start + 1])
+ }
- RelativeArcToKey -> pathNodesFromArgs(args, NUM_ARC_TO_ARGS) { array ->
- PathNode.RelativeArcTo(
- horizontalEllipseRadius = array[0],
- verticalEllipseRadius = array[1],
- theta = array[2],
- isMoreThanHalf = array[3].compareTo(0.0f) != 0,
- isPositiveArc = array[4].compareTo(0.0f) != 0,
- arcStartDx = array[5],
- arcStartDy = array[6]
- )
- }
+ ReflectiveQuadToKey -> pathNodesFromArgs(
+ nodes,
+ args,
+ count,
+ NUM_REFLECTIVE_QUAD_TO_ARGS
+ ) { array, start ->
+ PathNode.ReflectiveQuadTo(x = array[start], y = array[start + 1])
+ }
- ArcToKey -> pathNodesFromArgs(args, NUM_ARC_TO_ARGS) { array ->
- PathNode.ArcTo(
- horizontalEllipseRadius = array[0],
- verticalEllipseRadius = array[1],
- theta = array[2],
- isMoreThanHalf = array[3].compareTo(0.0f) != 0,
- isPositiveArc = array[4].compareTo(0.0f) != 0,
- arcStartX = array[5],
- arcStartY = array[6]
- )
- }
+ RelativeArcToKey -> pathNodesFromArgs(nodes, args, count, NUM_ARC_TO_ARGS) { array, start ->
+ PathNode.RelativeArcTo(
+ horizontalEllipseRadius = array[start],
+ verticalEllipseRadius = array[start + 1],
+ theta = array[start + 2],
+ isMoreThanHalf = array[start + 3].compareTo(0.0f) != 0,
+ isPositiveArc = array[start + 4].compareTo(0.0f) != 0,
+ arcStartDx = array[start + 5],
+ arcStartDy = array[start + 6]
+ )
+ }
- else -> throw IllegalArgumentException("Unknown command for: $this")
+ ArcToKey -> pathNodesFromArgs(nodes, args, count, NUM_ARC_TO_ARGS) { array, start ->
+ PathNode.ArcTo(
+ horizontalEllipseRadius = array[start],
+ verticalEllipseRadius = array[start + 1],
+ theta = array[start + 2],
+ isMoreThanHalf = array[start + 3].compareTo(0.0f) != 0,
+ isPositiveArc = array[start + 4].compareTo(0.0f) != 0,
+ arcStartX = array[start + 5],
+ arcStartY = array[start + 6]
+ )
+ }
+
+ else -> throw IllegalArgumentException("Unknown command for: $this")
+ }
}
private inline fun pathNodesFromArgs(
+ nodes: MutableList<PathNode>,
args: FloatArray,
+ count: Int,
numArgs: Int,
- nodeFor: (subArray: FloatArray) -> PathNode
-): List<PathNode> {
- return (0..args.size - numArgs step numArgs).map { index ->
- val subArray = args.copyOfRange(index, index + numArgs)
- val node = nodeFor(subArray)
- when {
+ crossinline nodeFor: (subArray: FloatArray, start: Int) -> PathNode
+) {
+ val end = count - numArgs
+ var index = 0
+ while (index <= end) {
+ val node = nodeFor(args, index)
+ nodes.add(when {
// According to the spec, if a MoveTo is followed by multiple pairs of coordinates,
// the subsequent pairs are treated as implicit corresponding LineTo commands.
- node is PathNode.MoveTo && index > 0 -> PathNode.LineTo(subArray[0], subArray[1])
+ node is PathNode.MoveTo && index > 0 -> PathNode.LineTo(args[index], args[index + 1])
node is PathNode.RelativeMoveTo && index > 0 ->
- PathNode.RelativeLineTo(subArray[0], subArray[1])
+ PathNode.RelativeLineTo(args[index], args[index + 1])
else -> node
- }
+ })
+ index += numArgs
}
}
/**
- * Constants used by [Char.toPathNodes] for creating [PathNode]s from parsed paths.
+ * Constants used by [Char.addPathNodes] for creating [PathNode]s from parsed paths.
*/
private const val RelativeCloseKey = 'z'
private const val CloseKey = 'Z'
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
index 7f634e8..5bc0f69 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
@@ -46,6 +46,8 @@
import kotlin.math.sqrt
import kotlin.math.tan
+internal val EmptyArray = FloatArray(0)
+
class PathParser {
private data class PathPoint(var x: Float = 0.0f, var y: Float = 0.0f) {
@@ -66,6 +68,12 @@
private val segmentPoint = PathPoint()
private val reflectiveCtrlPoint = PathPoint()
+ // We need to return the position of the next separator and whether the
+ // next float starts with a '-' or a '.'.
+ private var resultIndexAdvance = 0
+
+ private var results = FloatArray(64)
+
/**
* Parses the path string to create a collection of PathNode instances with their corresponding
* arguments
@@ -78,17 +86,22 @@
var end = 1
while (end < pathData.length) {
end = nextStart(pathData, end)
- val s = pathData.substring(start, end).trim { it <= ' ' }
- if (s.isNotEmpty()) {
- val args = getFloats(s)
- addNode(s[0], args)
+
+ // Trim leading and trailing spaces
+ var trimEnd = end
+ while (start < end && pathData[start] <= ' ') start++
+ while (trimEnd > start && pathData[trimEnd - 1] <= ' ') trimEnd--
+
+ if (start != trimEnd) {
+ val count = getFloats(pathData, start, trimEnd)
+ addNodes(pathData[start], results, count)
}
start = end
end++
}
if (end - start == 1 && start < pathData.length) {
- addNode(pathData[start], FloatArray(0))
+ addNodes(pathData[start], EmptyArray, 0)
}
return this
@@ -521,16 +534,15 @@
}
}
- private fun addNode(cmd: Char, args: FloatArray) {
- nodes.addAll(cmd.toPathNodes(args))
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun addNodes(cmd: Char, args: FloatArray, count: Int) {
+ cmd.addPathNodes(nodes, args, count)
}
private fun nextStart(s: String, end: Int): Int {
var index = end
- var c: Char
-
while (index < s.length) {
- c = s[index]
+ val c = s[index]
// Note that 'e' or 'E' are not valid path commands, but could be
// used for floating point numbers' scientific notation.
// Therefore, when searching for next command, we should ignore 'e'
@@ -545,101 +557,73 @@
return index
}
- private fun getFloats(s: String): FloatArray {
- if (s[0] == 'z' || s[0] == 'Z') {
- return FloatArray(0)
+ private fun getFloats(s: String, start: Int, end: Int): Int {
+ if (s[start] == 'z' || s[start] == 'Z') {
+ return 0
}
- val results = FloatArray(s.length)
- var count = 0
- var startPosition = 1
- var endPosition: Int
- val result = ExtractFloatResult()
- val totalLength = s.length
+ var count = 0
+ var startPosition = start + 1
// The startPosition should always be the first character of the
// current number, and endPosition is the character after the current
// number.
- while (startPosition < totalLength) {
- extract(s, startPosition, result)
- endPosition = result.endPosition
+ while (startPosition < end) {
+ val endPosition = extract(s, startPosition, end)
if (startPosition < endPosition) {
- results[count++] =
- s.substring(startPosition, endPosition).toFloat()
+ results[count++] = s.substring(startPosition, endPosition).toFloat()
+ resizeResultsIfNeeded(count)
}
- if (result.endWithNegativeOrDot) {
- // Keep the '-' or '.' sign with next number.
- startPosition = endPosition
- } else {
- startPosition = endPosition + 1
- }
+ startPosition = endPosition + resultIndexAdvance
}
- return copyOfRange(results, 0, count)
+
+ return count
}
- private fun copyOfRange(original: FloatArray, start: Int, end: Int): FloatArray {
- if (start > end) {
- throw IllegalArgumentException()
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun resizeResultsIfNeeded(count: Int) {
+ if (count >= results.size) {
+ val previous = results
+ results = FloatArray(count * 2)
+ previous.copyInto(results, 0, 0, previous.size)
}
- val originalLength = original.size
- if (start < 0 || start > originalLength) {
- throw IndexOutOfBoundsException()
- }
- val resultLength = end - start
- val copyLength = minOf(resultLength, originalLength - start)
- val result = FloatArray(resultLength)
- original.copyInto(result, 0, start, start + copyLength)
- return result
}
- private fun extract(s: String, start: Int, result: ExtractFloatResult) {
+ private fun extract(s: String, start: Int, end: Int): Int {
// Now looking for ' ', ',', '.' or '-' from the start.
var currentIndex = start
- var foundSeparator = false
- result.endWithNegativeOrDot = false
- var secondDot = false
- var isExponential = false
- while (currentIndex < s.length) {
- val isPrevExponential = isExponential
- isExponential = false
- val currentChar = s[currentIndex]
- when (currentChar) {
- ' ', ',' -> foundSeparator = true
+
+ resultIndexAdvance = 1
+
+ while (currentIndex < end) {
+ when (s[currentIndex]) {
+ ' ', ',' -> return currentIndex
'-' ->
// The negative sign following a 'e' or 'E' is not a separator.
- if (currentIndex != start && !isPrevExponential) {
- foundSeparator = true
- result.endWithNegativeOrDot = true
+ if (currentIndex != start &&
+ s[currentIndex - 1] != 'e' &&
+ s[currentIndex - 1] != 'E'
+ ) {
+ resultIndexAdvance = 0
+ return currentIndex
}
+
'.' ->
- if (!secondDot) {
- secondDot = true
- } else {
+ if (currentIndex != start && s[currentIndex - 1] == '.') {
// This is the second dot, and it is considered as a separator.
- foundSeparator = true
- result.endWithNegativeOrDot = true
+ resultIndexAdvance = 0
+ return currentIndex
}
- 'e', 'E' -> isExponential = true
- }
- if (foundSeparator) {
- break
}
currentIndex++
}
+
// When there is nothing found, then we put the end position to the end
// of the string.
- result.endPosition = currentIndex
+ return currentIndex
}
- private data class ExtractFloatResult(
- // We need to return the position of the next separator and whether the
- // next float starts with a '-' or a '.'.
- var endPosition: Int = 0,
- var endWithNegativeOrDot: Boolean = false
- )
-
- private fun Float.toRadians(): Float = this / 180f * PI.toFloat()
private fun Double.toRadians(): Double = this / 180 * PI
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
index 9a310b1..1c3db34 100644
--- a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
+++ b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
@@ -26,6 +26,40 @@
import kotlin.test.Test
class PathParserTest {
+ @Test
+ fun negativeExponent() {
+ val linePath = object : TestPath() {
+ var lineToPoints = ArrayList<Offset>()
+
+ override fun lineTo(x: Float, y: Float) {
+ lineToPoints.add(Offset(x, y))
+ }
+ }
+
+ val parser = PathParser()
+ parser.parsePathString("H1e-5").toPath(linePath)
+
+ assertEquals(1, linePath.lineToPoints.size)
+ assertEquals(1e-5f, linePath.lineToPoints[0].x)
+ }
+
+ @Test
+ fun dotDot() {
+ val linePath = object : TestPath() {
+ var lineToPoints = ArrayList<Offset>()
+
+ override fun relativeLineTo(dx: Float, dy: Float) {
+ lineToPoints.add(Offset(dx, dy))
+ }
+ }
+
+ val parser = PathParser()
+ parser.parsePathString("m0 0l2..5").toPath(linePath)
+
+ assertEquals(1, linePath.lineToPoints.size)
+ assertEquals(2.0f, linePath.lineToPoints[0].x)
+ assertEquals(0.5f, linePath.lineToPoints[0].y)
+ }
@Test
fun relativeQuadToTest() {
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index ce009b4..dfe8a5e 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -121,7 +121,7 @@
@Deprecated public final class GestureScope {
ctor @Deprecated public GestureScope(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.test.TestContext testContext);
method @Deprecated public long getVisibleSize();
- property public final long visibleSize;
+ property @Deprecated public final long visibleSize;
}
public final class GestureScopeKt {
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index 14f4cb4..f937e6e 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -127,7 +127,7 @@
@Deprecated public final class GestureScope {
ctor @Deprecated public GestureScope(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.test.TestContext testContext);
method @Deprecated public long getVisibleSize();
- property public final long visibleSize;
+ property @Deprecated public final long visibleSize;
}
public final class GestureScopeKt {
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 659da21..28d5fca 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -121,7 +121,7 @@
@Deprecated public final class GestureScope {
ctor @Deprecated public GestureScope(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.test.TestContext testContext);
method @Deprecated public long getVisibleSize();
- property public final long visibleSize;
+ property @Deprecated public final long visibleSize;
field @Deprecated @kotlin.PublishedApi internal final androidx.compose.ui.test.MultiModalInjectionScope delegateScope;
}
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 422f06f..e218a96 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -444,6 +444,12 @@
property public final long size;
}
+ @androidx.compose.runtime.Immutable public final class TextMeasurer {
+ ctor public TextMeasurer(androidx.compose.ui.text.font.FontFamily.Resolver fallbackFontFamilyResolver, androidx.compose.ui.unit.Density fallbackDensity, androidx.compose.ui.unit.LayoutDirection fallbackLayoutDirection, optional int cacheSize);
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(String text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
+ }
+
public final class TextPainter {
method public void paint(androidx.compose.ui.graphics.Canvas canvas, androidx.compose.ui.text.TextLayoutResult textLayoutResult);
field public static final androidx.compose.ui.text.TextPainter INSTANCE;
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 54a842a..da4f33e 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -475,7 +475,7 @@
property public final long size;
}
- @androidx.compose.runtime.Immutable @androidx.compose.ui.text.ExperimentalTextApi public final class TextMeasurer {
+ @androidx.compose.runtime.Immutable public final class TextMeasurer {
ctor public TextMeasurer(androidx.compose.ui.text.font.FontFamily.Resolver fallbackFontFamilyResolver, androidx.compose.ui.unit.Density fallbackDensity, androidx.compose.ui.unit.LayoutDirection fallbackLayoutDirection, optional int cacheSize);
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(String text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 422f06f..e218a96 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -444,6 +444,12 @@
property public final long size;
}
+ @androidx.compose.runtime.Immutable public final class TextMeasurer {
+ ctor public TextMeasurer(androidx.compose.ui.text.font.FontFamily.Resolver fallbackFontFamilyResolver, androidx.compose.ui.unit.Density fallbackDensity, androidx.compose.ui.unit.LayoutDirection fallbackLayoutDirection, optional int cacheSize);
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(String text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
+ }
+
public final class TextPainter {
method public void paint(androidx.compose.ui.graphics.Canvas canvas, androidx.compose.ui.text.TextLayoutResult textLayoutResult);
field public static final androidx.compose.ui.text.TextPainter INSTANCE;
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index 1dba872..1220a5b 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -459,6 +459,78 @@
}
@Test
+ fun getLineForVerticalPosition_ltr_lineTopCenterBottom_paddingFalse() {
+ val text = "ab\ncde\n\nfg"
+ // default density for the pixel 2 XL where test fails.
+ val density = Density(3.5f, 1.0f)
+ // font size where test fails
+ val fontSize = 14.sp
+
+ @Suppress("DEPRECATION") val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontSize = fontSize,
+ platformStyle = PlatformTextStyle(includeFontPadding = false)
+ ),
+ density = density
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(4)
+
+ for (index in 0 until paragraph.lineCount) {
+ assertThat(
+ paragraph.getLineForVerticalPosition(paragraph.getLineTop(index))
+ ).isEqualTo(index)
+
+ assertThat(
+ paragraph.getLineForVerticalPosition(
+ (paragraph.getLineTop(index) + paragraph.getLineBottom(index)) / 2f
+ )
+ ).isEqualTo(index)
+
+ assertThat(
+ paragraph.getLineForVerticalPosition(paragraph.getLineBottom(index) - 1f)
+ ).isEqualTo(index)
+ }
+ }
+
+ @Test
+ fun getLineForVerticalPosition_ltr_lineTopCenterBottom_paddingTrue() {
+ val text = "ab\ncde\n\nfg"
+ // default density for the pixel 2 XL where test fails.
+ val density = Density(3.5f, 1.0f)
+ // font size where test fails
+ val fontSize = 14.sp
+
+ @Suppress("DEPRECATION") val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontSize = fontSize,
+ platformStyle = PlatformTextStyle(includeFontPadding = true)
+ ),
+ density = density
+ )
+
+ assertThat(paragraph.lineCount).isEqualTo(4)
+
+ for (index in 0 until paragraph.lineCount) {
+ assertThat(
+ paragraph.getLineForVerticalPosition(paragraph.getLineTop(index))
+ ).isEqualTo(index)
+
+ assertThat(
+ paragraph.getLineForVerticalPosition(
+ (paragraph.getLineTop(index) + paragraph.getLineBottom(index)) / 2f
+ )
+ ).isEqualTo(index)
+
+ assertThat(
+ paragraph.getLineForVerticalPosition(paragraph.getLineBottom(index) - 1f)
+ ).isEqualTo(index)
+ }
+ }
+
+ @Test
fun getBoundingBox_ltr_singleLine() {
with(defaultDensity) {
val text = "abc"
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index 9ca36a4..53effe0 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -79,7 +79,6 @@
* other layout affecting input, cache can be skipped because most repeated measure calls would miss
* the cache.
*/
-@ExperimentalTextApi
@Immutable
class TextMeasurer constructor(
private val fallbackFontFamilyResolver: FontFamily.Resolver,
diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
index 64b4ea9..b772c2f 100644
--- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
+++ b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
@@ -77,6 +77,7 @@
}
@Test
+ @Ignore("b/271123970 Fails in AOSP. Will be fixed after upstreaming Compose for Desktop")
fun getBoundingBox_multicodepoints() {
assumeTrue(isLinux)
with(defaultDensity) {
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 6403d53..60324d1 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -40,7 +40,7 @@
api(project(":compose:ui:ui-tooling-data"))
implementation("androidx.savedstate:savedstate-ktx:1.2.0")
implementation("androidx.compose.material:material:1.0.0")
- implementation("androidx.activity:activity-compose:1.7.0-rc01")
+ implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.lifecycle:lifecycle-common:2.6.0")
// kotlin-reflect and animation-tooling-internal are provided by Studio at runtime
@@ -87,7 +87,7 @@
implementation(project(":compose:animation:animation"))
implementation("androidx.savedstate:savedstate-ktx:1.2.0")
implementation(project(":compose:material:material"))
- implementation("androidx.activity:activity-compose:1.7.0-rc01")
+ implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.lifecycle:lifecycle-common:2.6.0")
// kotlin-reflect and tooling-animation-internal are provided by Studio at runtime
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index b791bd7..3573195 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -327,14 +327,14 @@
method @Deprecated public void setRight(androidx.compose.ui.focus.FocusRequester);
method @Deprecated public void setStart(androidx.compose.ui.focus.FocusRequester);
method @Deprecated public void setUp(androidx.compose.ui.focus.FocusRequester);
- property public final androidx.compose.ui.focus.FocusRequester down;
- property public final androidx.compose.ui.focus.FocusRequester end;
- property public final androidx.compose.ui.focus.FocusRequester left;
- property public final androidx.compose.ui.focus.FocusRequester next;
- property public final androidx.compose.ui.focus.FocusRequester previous;
- property public final androidx.compose.ui.focus.FocusRequester right;
- property public final androidx.compose.ui.focus.FocusRequester start;
- property public final androidx.compose.ui.focus.FocusRequester up;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester down;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester end;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester left;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester next;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester previous;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester right;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester start;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester up;
}
@Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusOrderModifier extends androidx.compose.ui.Modifier.Element {
@@ -400,7 +400,7 @@
@Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
method @Deprecated public androidx.compose.ui.focus.FocusRequester getFocusRequester();
- property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
+ property @Deprecated public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
@@ -2231,6 +2231,13 @@
package androidx.compose.ui.node {
+ public interface CompositionLocalConsumerModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ }
+
+ public final class CompositionLocalConsumerModifierNodeKt {
+ method public static <T> T! currentValueOf(androidx.compose.ui.node.CompositionLocalConsumerModifierNode, androidx.compose.runtime.CompositionLocal<T> local);
+ }
+
public interface DelegatableNode {
method public androidx.compose.ui.Modifier.Node getNode();
property public abstract androidx.compose.ui.Modifier.Node node;
@@ -2260,12 +2267,6 @@
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
- public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
- method public long getTargetSize();
- method public void setTargetSize(long);
- property public abstract long targetSize;
- }
-
public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public default void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
method public default void onRemeasured(long size);
@@ -2282,8 +2283,8 @@
public final class LayoutModifierNodeKt {
method public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
- method public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
method public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidatePlacement(androidx.compose.ui.node.LayoutModifierNode);
}
public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
@@ -3091,6 +3092,14 @@
}
+package androidx.compose.ui.text {
+
+ public final class TextMeasurerHelperKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.TextMeasurer rememberTextMeasurer(optional int cacheSize);
+ }
+
+}
+
package androidx.compose.ui.viewinterop {
public final class AndroidView_androidKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index a73682d..43ad917 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -416,14 +416,14 @@
method @Deprecated public void setRight(androidx.compose.ui.focus.FocusRequester);
method @Deprecated public void setStart(androidx.compose.ui.focus.FocusRequester);
method @Deprecated public void setUp(androidx.compose.ui.focus.FocusRequester);
- property public final androidx.compose.ui.focus.FocusRequester down;
- property public final androidx.compose.ui.focus.FocusRequester end;
- property public final androidx.compose.ui.focus.FocusRequester left;
- property public final androidx.compose.ui.focus.FocusRequester next;
- property public final androidx.compose.ui.focus.FocusRequester previous;
- property public final androidx.compose.ui.focus.FocusRequester right;
- property public final androidx.compose.ui.focus.FocusRequester start;
- property public final androidx.compose.ui.focus.FocusRequester up;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester down;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester end;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester left;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester next;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester previous;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester right;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester start;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester up;
}
@Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusOrderModifier extends androidx.compose.ui.Modifier.Element {
@@ -518,7 +518,7 @@
@Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
method @Deprecated public androidx.compose.ui.focus.FocusRequester getFocusRequester();
- property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
+ property @Deprecated public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
@@ -2018,6 +2018,11 @@
ctor public HorizontalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface IntermediateMeasureScope extends androidx.compose.ui.layout.LookaheadScope kotlinx.coroutines.CoroutineScope androidx.compose.ui.layout.MeasureScope {
+ method public long getLookaheadSize();
+ property public abstract long lookaheadSize;
+ }
+
public interface IntrinsicMeasurable {
method public Object? getParentData();
method public int maxIntrinsicHeight(int width);
@@ -2115,17 +2120,25 @@
method public static androidx.compose.ui.Modifier layout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measure);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface LookaheadLayoutCoordinates extends androidx.compose.ui.layout.LayoutCoordinates {
- method public long localLookaheadPositionOf(androidx.compose.ui.layout.LookaheadLayoutCoordinates sourceCoordinates, optional long relativeToSource);
+ @Deprecated @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface LookaheadLayoutCoordinates extends androidx.compose.ui.layout.LayoutCoordinates {
}
public final class LookaheadLayoutKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.UiComposable public static void LookaheadLayout(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LookaheadLayoutScope,kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.MeasurePolicy measurePolicy);
+ method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.UiComposable public static void LookaheadLayout(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LookaheadScope,kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.MeasurePolicy measurePolicy);
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.UiComposable public static void LookaheadScope(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LookaheadScope,kotlin.Unit> content);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier intermediateLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntermediateMeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measure);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface LookaheadLayoutScope {
- method public androidx.compose.ui.Modifier intermediateLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function4<? super androidx.compose.ui.layout.MeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? super androidx.compose.ui.unit.IntSize,? extends androidx.compose.ui.layout.MeasureResult> measure);
- method public androidx.compose.ui.Modifier onPlaced(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.LookaheadLayoutCoordinates,? super androidx.compose.ui.layout.LookaheadLayoutCoordinates,kotlin.Unit> onPlaced);
+ @Deprecated @androidx.compose.ui.ExperimentalComposeUiApi public interface LookaheadLayoutScope {
+ method @Deprecated public androidx.compose.ui.Modifier onPlaced(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.LookaheadLayoutCoordinates,? super androidx.compose.ui.layout.LookaheadLayoutCoordinates,kotlin.Unit> onPlaced);
+ }
+
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface LookaheadScope {
+ method public androidx.compose.ui.layout.LayoutCoordinates getLookaheadScopeCoordinates(androidx.compose.ui.layout.Placeable.PlacementScope);
+ method @Deprecated public default androidx.compose.ui.Modifier intermediateLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function4<? super androidx.compose.ui.layout.MeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? super androidx.compose.ui.unit.IntSize,? extends androidx.compose.ui.layout.MeasureResult> measure);
+ method public default long localLookaheadPositionOf(androidx.compose.ui.layout.LayoutCoordinates, androidx.compose.ui.layout.LayoutCoordinates coordinates);
+ method @Deprecated public androidx.compose.ui.Modifier onPlaced(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.LookaheadLayoutCoordinates,? super androidx.compose.ui.layout.LookaheadLayoutCoordinates,kotlin.Unit> onPlaced);
+ method public androidx.compose.ui.layout.LayoutCoordinates toLookaheadCoordinates(androidx.compose.ui.layout.LayoutCoordinates);
}
public interface Measurable extends androidx.compose.ui.layout.IntrinsicMeasurable {
@@ -2423,11 +2436,11 @@
package androidx.compose.ui.node {
- @androidx.compose.ui.ExperimentalComposeUiApi public interface CompositionLocalConsumerModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface CompositionLocalConsumerModifierNode extends androidx.compose.ui.node.DelegatableNode {
}
public final class CompositionLocalConsumerModifierNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static <T> T! currentValueOf(androidx.compose.ui.node.CompositionLocalConsumerModifierNode, androidx.compose.runtime.CompositionLocal<T> local);
+ method public static <T> T! currentValueOf(androidx.compose.ui.node.CompositionLocalConsumerModifierNode, androidx.compose.runtime.CompositionLocal<T> local);
}
public interface DelegatableNode {
@@ -2459,12 +2472,6 @@
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
- public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
- method public long getTargetSize();
- method public void setTargetSize(long);
- property public abstract long targetSize;
- }
-
@kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalCoreApi {
}
@@ -2473,7 +2480,6 @@
}
public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
- method @androidx.compose.ui.ExperimentalComposeUiApi public default void onLookaheadPlaced(androidx.compose.ui.layout.LookaheadLayoutCoordinates coordinates);
method public default void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
method public default void onRemeasured(long size);
}
@@ -2489,8 +2495,8 @@
public final class LayoutModifierNodeKt {
method public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
- method public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
method public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidatePlacement(androidx.compose.ui.node.LayoutModifierNode);
}
public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
@@ -3353,7 +3359,7 @@
package androidx.compose.ui.text {
public final class TextMeasurerHelperKt {
- method @androidx.compose.runtime.Composable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.TextMeasurer rememberTextMeasurer(optional int cacheSize);
+ method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.TextMeasurer rememberTextMeasurer(optional int cacheSize);
}
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 3e61d76..9b3c278 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -327,14 +327,14 @@
method @Deprecated public void setRight(androidx.compose.ui.focus.FocusRequester);
method @Deprecated public void setStart(androidx.compose.ui.focus.FocusRequester);
method @Deprecated public void setUp(androidx.compose.ui.focus.FocusRequester);
- property public final androidx.compose.ui.focus.FocusRequester down;
- property public final androidx.compose.ui.focus.FocusRequester end;
- property public final androidx.compose.ui.focus.FocusRequester left;
- property public final androidx.compose.ui.focus.FocusRequester next;
- property public final androidx.compose.ui.focus.FocusRequester previous;
- property public final androidx.compose.ui.focus.FocusRequester right;
- property public final androidx.compose.ui.focus.FocusRequester start;
- property public final androidx.compose.ui.focus.FocusRequester up;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester down;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester end;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester left;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester next;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester previous;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester right;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester start;
+ property @Deprecated public final androidx.compose.ui.focus.FocusRequester up;
}
@Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusOrderModifier extends androidx.compose.ui.Modifier.Element {
@@ -400,7 +400,7 @@
@Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
method @Deprecated public androidx.compose.ui.focus.FocusRequester getFocusRequester();
- property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
+ property @Deprecated public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
@@ -2278,6 +2278,13 @@
property public final kotlin.jvm.functions.Function0<androidx.compose.ui.node.ComposeUiNode> VirtualConstructor;
}
+ public interface CompositionLocalConsumerModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ }
+
+ public final class CompositionLocalConsumerModifierNodeKt {
+ method public static <T> T! currentValueOf(androidx.compose.ui.node.CompositionLocalConsumerModifierNode, androidx.compose.runtime.CompositionLocal<T> local);
+ }
+
public interface DelegatableNode {
method public androidx.compose.ui.Modifier.Node getNode();
property public abstract androidx.compose.ui.Modifier.Node node;
@@ -2307,12 +2314,6 @@
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
- public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
- method public long getTargetSize();
- method public void setTargetSize(long);
- property public abstract long targetSize;
- }
-
public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public default void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
method public default void onRemeasured(long size);
@@ -2329,8 +2330,8 @@
public final class LayoutModifierNodeKt {
method public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
- method public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
method public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidatePlacement(androidx.compose.ui.node.LayoutModifierNode);
}
public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
@@ -3139,6 +3140,14 @@
}
+package androidx.compose.ui.text {
+
+ public final class TextMeasurerHelperKt {
+ method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.TextMeasurer rememberTextMeasurer(optional int cacheSize);
+ }
+
+}
+
package androidx.compose.ui.viewinterop {
public final class AndroidView_androidKt {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 4820bf0..1131691 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -76,7 +76,7 @@
compileOnly(libs.kotlinReflect)
testImplementation(libs.kotlinReflect)
- implementation("androidx.activity:activity-ktx:1.7.0-rc01")
+ implementation("androidx.activity:activity-ktx:1.7.0")
implementation("androidx.core:core:1.9.0")
implementation('androidx.collection:collection:1.0.0')
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
@@ -124,7 +124,7 @@
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.0")
androidTestImplementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
androidTestImplementation("androidx.core:core-ktx:1.9.0")
- androidTestImplementation("androidx.activity:activity-compose:1.7.0-rc01")
+ androidTestImplementation("androidx.activity:activity-compose:1.7.0")
androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
androidTestImplementation("androidx.fragment:fragment:1.3.0")
@@ -173,7 +173,7 @@
implementation("androidx.autofill:autofill:1.0.0")
implementation(libs.kotlinCoroutinesAndroid)
- implementation("androidx.activity:activity-ktx:1.7.0-rc01")
+ implementation("androidx.activity:activity-ktx:1.7.0")
implementation("androidx.core:core:1.9.0")
implementation('androidx.collection:collection:1.0.0')
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
@@ -251,7 +251,7 @@
implementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.0")
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
implementation("androidx.core:core-ktx:1.2.0")
- implementation("androidx.activity:activity-compose:1.7.0-rc01")
+ implementation("androidx.activity:activity-compose:1.7.0")
}
desktopTest.dependencies {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CancelFocusDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CancelFocusDemo.kt
index ec9a575..3829477 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CancelFocusDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CancelFocusDemo.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.demos.focus
-import android.annotation.SuppressLint
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement.SpaceEvenly
@@ -76,7 +75,6 @@
}
}
-@SuppressLint("ModifierInspectorInfo")
private fun Modifier.focusableWithBorder() = composed {
var color by remember { mutableStateOf(Black) }
Modifier
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
index 706d782..343c89d 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
@@ -36,20 +36,18 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.movableContentWithReceiverOf
+import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.LookaheadLayout
-import androidx.compose.ui.layout.LookaheadLayoutScope
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -59,89 +57,63 @@
@Sampled
@Composable
-fun LookaheadLayoutSample() {
- // Creates a custom modifier that animates the constraints and measure child/children with them.
- // It is built on top of `Modifier.intermediateLayout`, which allows access to the target size
- // of the layout. A resize animation will be created to animate to the target size. Fixed
- // constraints created based on the animation value will be used to measure child/children, so
- // that all the children gradually change their size to fit in the animated constraints.
- fun Modifier.animateConstraints(lookaheadScope: LookaheadLayoutScope) = composed {
+fun IntermediateLayoutSample() {
+ // Creates a custom modifier that animates the constraints and measures child with the
+ // animated constraints. This modifier is built on top of `Modifier.intermediateLayout`, which
+ // allows access to the lookahead size of the layout. A resize animation will be kicked off
+ // whenever the lookahead size changes, to animate children from current size to lookahead size.
+ // Fixed constraints created based on the animation value will be used to measure
+ // child, so the child layout gradually changes its size and potentially its child's placement
+ // to fit within the animated constraints.
+ fun Modifier.animateConstraints() = composed {
+ // Creates a size animation
var sizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember {
mutableStateOf(null)
}
- var targetSize: IntSize? by remember { mutableStateOf(null) }
- // Create a `LaunchEffect` to handle target size change. This avoids creating side effects
- // from measure/layout phase.
- LaunchedEffect(Unit) {
- snapshotFlow { targetSize }.collect { target ->
- if (target != null && target != sizeAnimation?.targetValue) {
- sizeAnimation?.run {
- launch { animateTo(target) }
- } ?: Animatable(target, IntSize.VectorConverter).let {
- sizeAnimation = it
- }
+
+ this.intermediateLayout { measurable, _ ->
+ // When layout changes, the lookahead pass will calculate a new final size for the
+ // child layout. This lookahead size can be used to animate the size
+ // change, such that the animation starts from the current size and gradually
+ // change towards `lookaheadSize`.
+ if (lookaheadSize != sizeAnimation?.targetValue) {
+ sizeAnimation?.run {
+ launch { animateTo(lookaheadSize) }
+ } ?: Animatable(lookaheadSize, IntSize.VectorConverter).let {
+ sizeAnimation = it
}
}
- }
- with(lookaheadScope) {
- // The measure logic in `intermediateLayout` is skipped in the lookahead pass, as
- // intermediateLayout is expected to produce intermediate stages of a layout transform.
- // When the measure block is invoked after lookahead pass, the lookahead size of the
- // child will be accessible as a parameter to the measure block.
- this@composed.intermediateLayout { measurable, _, lookaheadSize ->
- // When layout changes, the lookahead pass will calculate a new final size for the
- // child modifier. This lookahead size can be used to animate the size
- // change, such that the animation starts from the current size and gradually
- // change towards `lookaheadSize`.
- targetSize = lookaheadSize
- // Reads the animation size if the animation is set up. Otherwise (i.e. first
- // frame), use the lookahead size without animation.
- val (width, height) = sizeAnimation?.value ?: lookaheadSize
- // Creates a fixed set of constraints using the animated size
- val animatedConstraints = Constraints.fixed(width, height)
- // Measure child/children with animated constraints.
- val placeable = measurable.measure(animatedConstraints)
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
- }
+ val (width, height) = sizeAnimation!!.value
+ // Creates a fixed set of constraints using the animated size
+ val animatedConstraints = Constraints.fixed(width, height)
+ // Measure child with animated constraints.
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
}
}
}
- LookaheadLayout(
- content = {
- var fullWidth by remember { mutableStateOf(false) }
- Row(
- (if (fullWidth) Modifier.fillMaxWidth() else Modifier.width(100.dp))
- .height(200.dp)
- // Use the custom modifier created above to animate the constraints passed
- // to the child, and therefore resize children in an animation.
- .animateConstraints(this@LookaheadLayout)
- .clickable { fullWidth = !fullWidth }) {
- Box(
- Modifier
- .weight(1f)
- .fillMaxHeight()
- .background(Color.Red)
- )
- Box(
- Modifier
- .weight(2f)
- .fillMaxHeight()
- .background(Color.Yellow)
- )
- }
- }
- ) { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
- }
- }
+ var fullWidth by remember { mutableStateOf(false) }
+ Row(
+ (if (fullWidth) Modifier.fillMaxWidth() else Modifier.width(100.dp))
+ .height(200.dp)
+ // Use the custom modifier created above to animate the constraints passed
+ // to the child, and therefore resize children in an animation.
+ .animateConstraints()
+ .clickable { fullWidth = !fullWidth }) {
+ Box(
+ Modifier
+ .weight(1f)
+ .fillMaxHeight()
+ .background(Color.Red)
+ )
+ Box(
+ Modifier
+ .weight(2f)
+ .fillMaxHeight()
+ .background(Color.Yellow)
+ )
}
}
@@ -149,94 +121,80 @@
@Composable
fun LookaheadLayoutCoordinatesSample() {
// Creates a custom modifier to animate the local position of the layout within the
- // LookaheadLayout, whenever there's a change in the layout.
- fun Modifier.animatePlacementInScope(lookaheadScope: LookaheadLayoutScope) = composed {
- var offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by remember {
- mutableStateOf(
- null
- )
- }
+ // given LookaheadScope, whenever the relative position changes.
+ fun Modifier.animatePlacementInScope(lookaheadScope: LookaheadScope) = composed {
+ // Creates an offset animation
+ var offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by mutableStateOf(
+ null
+ )
+ var targetOffset: IntOffset? by mutableStateOf(null)
- var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) }
- var targetOffset: IntOffset? by remember {
- mutableStateOf(null)
- }
- // Create a `LaunchEffect` to handle target size change. This avoids creating side effects
- // from measure/layout phase.
- LaunchedEffect(Unit) {
- snapshotFlow {
- targetOffset
- }.collect { target ->
- if (target != null && target != offsetAnimation?.targetValue) {
- offsetAnimation?.run {
- launch { animateTo(target) }
- } ?: Animatable(target, IntOffset.VectorConverter).let {
- offsetAnimation = it
+ this.intermediateLayout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ // Converts coordinates of the current layout to LookaheadCoordinates
+ val coordinates = coordinates
+ if (coordinates != null) {
+ // Calculates the target offset within the lookaheadScope
+ val target = with(lookaheadScope) {
+ lookaheadScopeCoordinates
+ .localLookaheadPositionOf(coordinates)
+ .round().also { targetOffset = it }
}
+
+ // Uses the target offset to start an offset animation
+ if (target != offsetAnimation?.targetValue) {
+ offsetAnimation?.run {
+ launch { animateTo(target) }
+ } ?: Animatable(target, IntOffset.VectorConverter).let {
+ offsetAnimation = it
+ }
+ }
+ // Calculates the *current* offset within the given LookaheadScope
+ val placementOffset =
+ lookaheadScopeCoordinates.localPositionOf(
+ coordinates,
+ Offset.Zero
+ ).round()
+ // Calculates the delta between animated position in scope and current
+ // position in scope, and places the child at the delta offset. This puts
+ // the child layout at the animated position.
+ val (x, y) = requireNotNull(offsetAnimation).run { value - placementOffset }
+ placeable.place(x, y)
+ } else {
+ placeable.place(0, 0)
}
}
}
- with(lookaheadScope) {
- this@composed
- .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
- // This block of code has the LookaheadCoordinates of the LookaheadLayout
- // as the first parameter, and the coordinates of this modifier as the second
- // parameter.
-
- // localLookaheadPositionOf returns the *target* position of this
- // modifier in the LookaheadLayout's local coordinates.
- targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(
- layoutCoordinates
- ).round()
- // localPositionOf returns the *current* position of this
- // modifier in the LookaheadLayout's local coordinates.
- placementOffset = lookaheadScopeCoordinates.localPositionOf(
- layoutCoordinates, Offset.Zero
- ).round()
- }
- // The measure logic in `intermediateLayout` is skipped in the lookahead pass, as
- // intermediateLayout is expected to produce intermediate stages of a layout
- // transform. When the measure block is invoked after lookahead pass, the lookahead
- // size of the child will be accessible as a parameter to the measure block.
- .intermediateLayout { measurable, constraints, _ ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- // offsetAnimation will animate the target position whenever it changes.
- // In order to place the child at the animated position, we need to offset
- // the child based on the target and current position in LookaheadLayout.
- val (x, y) = offsetAnimation?.run { value - placementOffset }
- // If offsetAnimation has not been set up yet (i.e. in the first frame),
- // skip the animation
- ?: (targetOffset!! - placementOffset)
- placeable.place(x, y)
- }
- }
- }
}
val colors = listOf(
Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84)
)
- // Creates movable content containing 4 boxes. They will be put either in a [Row] or in a
- // [Column] depending on the state
- val items = remember {
- movableContentWithReceiverOf<LookaheadLayoutScope> {
- colors.forEach { color ->
- Box(
- Modifier
- .padding(15.dp)
- .size(100.dp, 80.dp)
- .animatePlacementInScope(this)
- .background(color, RoundedCornerShape(20))
- )
+ var isInColumn by remember { mutableStateOf(true) }
+ LookaheadScope {
+ // Creates movable content containing 4 boxes. They will be put either in a [Row] or in a
+ // [Column] depending on the state
+ val items = remember {
+ movableContentOf {
+ colors.forEach { color ->
+ Box(
+ Modifier
+ .padding(15.dp)
+ .size(100.dp, 80.dp)
+ .animatePlacementInScope(this)
+ .background(color, RoundedCornerShape(20))
+ )
+ }
}
}
- }
- var isInColumn by remember { mutableStateOf(true) }
- LookaheadLayout(
- content = {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clickable { isInColumn = !isInColumn }
+ ) {
// As the items get moved between Column and Row, their positions in LookaheadLayout
// will change. The `animatePlacementInScope` modifier created above will
// observe that final position change via `localLookaheadPositionOf`, and create
@@ -248,19 +206,6 @@
} else {
Row { items() }
}
- },
- modifier = Modifier
- .fillMaxSize()
- .clickable { isInColumn = !isInColumn }
- ) { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
- }
}
}
}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt
index b98dee2..df7fd7a 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt
@@ -21,7 +21,6 @@
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -31,7 +30,6 @@
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.InspectorInfo
-@OptIn(ExperimentalComposeUiApi::class)
@Sampled
@Composable
fun CompositionLocalConsumingModifierSample() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
index 7df3c84..47f9c79 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/ParentDataModifierTest.kt
@@ -21,9 +21,17 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutIdParentData
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.node.Ref
import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import org.junit.Assert.assertEquals
@@ -152,6 +160,24 @@
}
}
+ @Test
+ fun implementingBothParentDataAndLayoutModifier() {
+ val parentData = "data"
+ runOnUiThread {
+ activity.setContent {
+ Layout({
+ Layout(
+ modifier = ParentDataAndLayoutElement(parentData),
+ content = {}
+ ) { _, _ -> layout(0, 0) {} }
+ }) { measurables, _ ->
+ assertEquals("data", measurables[0].parentData)
+ layout(0, 0) { }
+ }
+ }
+ }
+ }
+
// We only need this because IR compiler doesn't like converting lambdas to Runnables
private fun runOnUiThread(block: () -> Unit) {
val runnable: Runnable = object : Runnable {
@@ -173,3 +199,24 @@
}
) {}
}
+
+private data class ParentDataAndLayoutElement(val data: String) :
+ ModifierNodeElement<ParentDataAndLayoutNode>() {
+ override fun create() = ParentDataAndLayoutNode(data)
+ override fun update(node: ParentDataAndLayoutNode) = node.also { it.data = data }
+}
+
+class ParentDataAndLayoutNode(var data: String) : Modifier.Node(), LayoutModifierNode,
+ ParentDataModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun Density.modifyParentData(parentData: Any?) = data
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
index 93902e5..7641c8a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
@@ -53,6 +53,7 @@
import androidx.test.filters.SdkSuppress
import org.junit.Assert
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -262,6 +263,7 @@
}
}
+ @Ignore("Test disabled due to flakiness, see b/256950653")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun concaveClip() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
index 905dfcd..3d6683e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
@@ -35,6 +35,9 @@
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.AlignTopLeft
import androidx.compose.ui.Alignment
@@ -242,14 +245,13 @@
rule.setContent {
AtLeastSize(
size = containerWidth.roundToInt() * 2,
- modifier = Modifier.background(Color.White).paint(
- TestPainter(
- containerWidth,
- containerHeight
- ),
- alignment = Alignment.BottomEnd,
- contentScale = ContentScale.Inside
- )
+ modifier = Modifier
+ .background(Color.White)
+ .paint(
+ TestPainter(
+ containerWidth, containerHeight
+ ), alignment = Alignment.BottomEnd, contentScale = ContentScale.Inside
+ )
) {
// Intentionally empty
}
@@ -379,7 +381,9 @@
val containerSize = containerWidth.roundToInt() / 2
rule.setContent {
NoIntrinsicSizeContainer(
- Modifier.background(Color.White).then(FixedSizeModifier(containerSize))
+ Modifier
+ .background(Color.White)
+ .then(FixedSizeModifier(containerSize))
) {
NoIntrinsicSizeContainer(
FixedSizeModifier(containerSize).paint(
@@ -600,9 +604,9 @@
}
}
Box(
- modifier =
- Modifier.then(modifier)
- .paint(painter, contentScale = contentScale)
+ modifier = Modifier
+ .then(modifier)
+ .paint(painter, contentScale = contentScale)
)
}
@@ -802,6 +806,56 @@
}
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun testUpdatingPainterWithTheSameIntrinsicsSize() {
+ var painter by mutableStateOf(TestPainter(10f, 10f).apply {
+ color = Color.Red
+ })
+ val tag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(tag).paint(painter))
+ }
+
+ rule.runOnIdle {
+ painter = TestPainter(10f, 10f).apply {
+ color = Color.Blue
+ }
+ }
+
+ rule.onNodeWithTag(tag).captureToImage().apply {
+ assertEquals(10, width)
+ assertEquals(10, height)
+ assertPixels { Color.Blue }
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun testUpdatingPainterWithTheDifferentIntrinsicsSize() {
+ var painter by mutableStateOf(TestPainter(10f, 10f).apply {
+ color = Color.Red
+ })
+ val tag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(tag).paint(painter))
+ }
+
+ rule.runOnIdle {
+ painter = TestPainter(5f, 5f).apply {
+ color = Color.Blue
+ }
+ }
+
+ rule.onNodeWithTag(tag).captureToImage().apply {
+ assertEquals(5, width)
+ assertEquals(5, height)
+ assertPixels { Color.Blue }
+ }
+ }
+
@Composable
private fun TestPainter(
alpha: Float = DefaultAlpha,
@@ -812,7 +866,8 @@
val layoutDirection = if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
AtLeastSize(
- modifier = Modifier.background(Color.White)
+ modifier = Modifier
+ .background(Color.White)
.paint(p, alpha = alpha, colorFilter = colorFilter),
size = containerWidth.roundToInt()
) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
index 7827030..6c15e46 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
@@ -1114,7 +1114,6 @@
val res =
childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
-
emitNewParent.value = false
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index b22ea31..2c87d41 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -39,7 +39,6 @@
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.modifier.ModifierLocalManager
import androidx.compose.ui.node.InternalCoreApi
@@ -3563,10 +3562,12 @@
) : NodeCoordinator(LayoutNode()) {
var additionalOffset = Offset.Zero
- override fun createLookaheadDelegate(scope: LookaheadScope): LookaheadDelegate {
+ override fun ensureLookaheadDelegateCreated() {
TODO("Not yet implemented")
}
+ override var lookaheadDelegate: LookaheadDelegate? = null
+
override val providedAlignmentLines: Set<AlignmentLine>
get() = TODO("not implemented")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt
index a34f741..417ace5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputDensityTest.kt
@@ -16,13 +16,21 @@
package androidx.compose.ui.input.pointer
+import android.view.MotionEvent
+import android.view.View
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.background
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Density
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -39,6 +47,134 @@
@get:Rule
val rule = createComposeRule()
+ val tag = "Tagged Layout"
+
+ @Test
+ fun sendNotANumberDensityInPointerEvents() {
+ lateinit var view: View
+
+ val motionEventsToTrigger = generateMultipleMotionEvents(
+ lastPointerX = Float.NaN,
+ lastPointerY = Float.NaN
+ )
+ val recordedEvents = mutableListOf<PointerEventType>()
+
+ rule.setContent {
+ view = LocalView.current
+
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .testTag(tag)
+ .pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event: PointerEvent = awaitPointerEvent()
+ recordedEvents += event.type
+ }
+ }
+ }
+ ) { }
+ }
+
+ rule.waitForIdle()
+
+ rule.runOnUiThread {
+ motionEventsToTrigger.forEach {
+ view.dispatchTouchEvent(it)
+ }
+ }
+ rule.waitForIdle()
+
+ assertThat(recordedEvents).hasSize(2)
+ assertThat(recordedEvents[0]).isEqualTo(PointerEventType.Press)
+ assertThat(recordedEvents[1]).isEqualTo(PointerEventType.Press)
+ }
+
+ @Test
+ fun sendPositiveInfinityDensityInPointerEvents() {
+ lateinit var view: View
+
+ val motionEventsToTrigger = generateMultipleMotionEvents(
+ lastPointerX = Float.POSITIVE_INFINITY,
+ lastPointerY = Float.POSITIVE_INFINITY
+ )
+ val recordedEvents = mutableListOf<PointerEventType>()
+
+ rule.setContent {
+ view = LocalView.current
+
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Red)
+ .testTag(tag)
+ .pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event: PointerEvent = awaitPointerEvent()
+ recordedEvents += event.type
+ }
+ }
+ }
+ ) { }
+ }
+
+ rule.waitForIdle()
+
+ rule.runOnUiThread {
+ motionEventsToTrigger.forEach {
+ view.dispatchTouchEvent(it)
+ }
+ }
+ rule.waitForIdle()
+
+ assertThat(recordedEvents).hasSize(2)
+ assertThat(recordedEvents[0]).isEqualTo(PointerEventType.Press)
+ assertThat(recordedEvents[1]).isEqualTo(PointerEventType.Press)
+ }
+
+ @Test
+ fun sendNegativeInfinityDensityInPointerEvents() {
+ lateinit var view: View
+
+ val motionEventsToTrigger = generateMultipleMotionEvents(
+ lastPointerX = Float.NEGATIVE_INFINITY,
+ lastPointerY = Float.NEGATIVE_INFINITY
+ )
+ val recordedEvents = mutableListOf<PointerEventType>()
+
+ rule.setContent {
+ view = LocalView.current
+
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Cyan)
+ .testTag(tag)
+ .pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event: PointerEvent = awaitPointerEvent()
+ recordedEvents += event.type
+ }
+ }
+ }
+ ) { }
+ }
+
+ rule.waitForIdle()
+
+ rule.runOnUiThread {
+ motionEventsToTrigger.forEach {
+ view.dispatchTouchEvent(it)
+ }
+ }
+ rule.waitForIdle()
+
+ assertThat(recordedEvents).hasSize(2)
+ assertThat(recordedEvents[0]).isEqualTo(PointerEventType.Press)
+ assertThat(recordedEvents[1]).isEqualTo(PointerEventType.Press)
+ }
+
@Test
fun compositionLocalDensityChangeRestartsPointerInputOverload1() {
compositionLocalDensityChangeRestartsPointerInput {
@@ -90,4 +226,104 @@
assertThat(pointerInputDensities.last()).isEqualTo(9f)
}
}
+
+ private fun generateMultipleMotionEvents(
+ lastPointerX: Float,
+ lastPointerY: Float
+ ): List<MotionEvent> {
+ val firstPointerOffset = Offset(0f, 0f)
+ val secondPointerOffset = Offset(10f, 0f)
+
+ val firstFingerPointerPropertiesId = 0
+ val secondFingerPointerPropertiesId = 1
+ val thirdFingerPointerPropertiesId = 2
+
+ val firstPointerProperties =
+ PointerProperties(firstFingerPointerPropertiesId).also {
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ val secondPointerProperties =
+ PointerProperties(secondFingerPointerPropertiesId).also {
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+
+ val thirdPointerProperties =
+ PointerProperties(thirdFingerPointerPropertiesId).also {
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+
+ val eventDownTime = 1L
+ var eventStartTime = 0L
+
+ val firstPointerEvent = MotionEvent.obtain(
+ eventDownTime,
+ eventStartTime,
+ MotionEvent.ACTION_DOWN,
+ 1,
+ arrayOf(firstPointerProperties),
+ arrayOf(PointerCoords(firstPointerOffset.x, firstPointerOffset.y)),
+ 0,
+ 0,
+ 0f,
+ 0f,
+ 0,
+ 0,
+ 0,
+ 0
+ )
+
+ eventStartTime += 500
+
+ val secondPointerEvent = MotionEvent.obtain(
+ eventDownTime,
+ eventStartTime,
+ MotionEvent.ACTION_POINTER_DOWN,
+ 2,
+ arrayOf(
+ firstPointerProperties,
+ secondPointerProperties
+ ),
+ arrayOf(
+ PointerCoords(firstPointerOffset.x, firstPointerOffset.y),
+ PointerCoords(secondPointerOffset.x, secondPointerOffset.y)
+ ),
+ 0,
+ 0,
+ 0f,
+ 0f,
+ 0,
+ 0,
+ 0,
+ 0
+ )
+
+ eventStartTime += 500
+
+ val thirdPointerEvent = MotionEvent.obtain(
+ eventDownTime,
+ eventStartTime,
+ MotionEvent.ACTION_POINTER_DOWN,
+ 3,
+ arrayOf(
+ firstPointerProperties,
+ secondPointerProperties,
+ thirdPointerProperties
+ ),
+ arrayOf(
+ PointerCoords(firstPointerOffset.x, firstPointerOffset.y),
+ PointerCoords(secondPointerOffset.x, secondPointerOffset.y),
+ PointerCoords(lastPointerX, lastPointerY)
+ ),
+ 0,
+ 0,
+ 0f,
+ 0f,
+ 0,
+ 0,
+ 0,
+ 0
+ )
+
+ return listOf(firstPointerEvent, secondPointerEvent, thirdPointerEvent)
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
index fd6b69e..ba5d463 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
@@ -39,6 +39,7 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
@@ -51,9 +52,9 @@
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -62,13 +63,16 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -99,39 +103,26 @@
var size2 = IntSize.Zero
rule.setContent {
CompositionLocalProvider(LocalDensity provides Density(1f)) {
- LookaheadLayout(
- measurePolicy = { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
- }
- }
- },
- content = {
- Row(if (isLarge) Modifier.size(200.dp) else Modifier.size(50.dp, 100.dp)) {
- Box(
- Modifier
- .fillMaxHeight()
- .weight(2f)
- .onSizeChanged {
- size1 = it
- }
- .animateSize(this@LookaheadLayout))
- Box(
- Modifier
- .fillMaxHeight()
- .weight(3f)
- .onSizeChanged {
- size2 = it
- }
- .animateSize(this@LookaheadLayout))
- }
+ LookaheadScope {
+ Row(if (isLarge) Modifier.size(200.dp) else Modifier.size(50.dp, 100.dp)) {
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .weight(2f)
+ .onSizeChanged {
+ size1 = it
+ }
+ .animateSize())
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .weight(3f)
+ .onSizeChanged {
+ size2 = it
+ }
+ .animateSize())
}
- )
+ }
}
}
// Check that:
@@ -154,22 +145,21 @@
}
}
- private fun Modifier.animateSize(scope: LookaheadLayoutScope): Modifier = composed {
- val cScope = rememberCoroutineScope()
+ private fun Modifier.animateSize(): Modifier = composed {
var anim: Animatable<IntSize, AnimationVector2D>? by remember { mutableStateOf(null) }
- with(scope) {
- this@composed.intermediateLayout(
- measure = { measurable, _, size ->
- anim = anim?.apply {
- cScope.launch { animateTo(size, tween(200)) }
- } ?: Animatable(size, IntSize.VectorConverter)
- val (width, height) = anim!!.value
- val placeable = measurable.measure(Constraints.fixed(width, height))
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
+ this.intermediateLayout { measurable, _ ->
+ anim = anim?.apply {
+ launch {
+ if (lookaheadSize != targetValue) {
+ animateTo(lookaheadSize, tween(200))
}
}
- )
+ } ?: Animatable(lookaheadSize, IntSize.VectorConverter)
+ val (width, height) = anim!!.value
+ val placeable = measurable.measure(Constraints.fixed(width, height))
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
}
}
@@ -209,14 +199,12 @@
Modifier
.padding(top = 100.dp)
.fillMaxSize()
- .intermediateLayout(
- measure = { measurable, constraints, _ ->
- measureWithLambdas(
- preMeasure = { parentMeasure = ++counter },
- prePlacement = { parentPlace = ++counter }
- ).invoke(this, measurable, constraints)
- }
- )
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(
+ preMeasure = { parentMeasure = ++counter },
+ prePlacement = { parentPlace = ++counter }
+ ).invoke(this, measurable, constraints)
+ }
.layout(
measureWithLambdas(
preMeasure = {
@@ -240,7 +228,7 @@
Modifier
.size(100.dp)
.background(Color.Red)
- .intermediateLayout { measurable, constraints, _ ->
+ .intermediateLayout { measurable, constraints ->
measureWithLambdas(
preMeasure = { childMeasure = ++counter },
prePlacement = { childPlace = ++counter }
@@ -312,7 +300,7 @@
) {
MyLookaheadLayout {
Box(modifier = Modifier
- .intermediateLayout { measurable, constraints, lookaheadSize ->
+ .intermediateLayout { measurable, constraints ->
assertEquals(width, lookaheadSize.width)
assertEquals(height, lookaheadSize.height)
val placeable = measurable.measure(constraints)
@@ -402,14 +390,14 @@
Box(
Modifier
.size(if (isSmall) 100.dp else 200.dp)
- .animateSize(this)
+ .animateSize()
.layout(
measureWithLambdas(
postMeasure = { measurePlusLookahead++ },
postPlacement = { placePlusLookahead++ },
)
)
- .intermediateLayout { measurable, constraints, _ ->
+ .intermediateLayout { measurable, constraints ->
measureWithLambdas(
postMeasure = { measure++ },
postPlacement = { place++ }
@@ -483,51 +471,38 @@
var child1LookaheadSize = IntSize.Zero
var child2LookaheadSize = IntSize.Zero
rule.setContent {
- LookaheadLayout(
- measurePolicy = { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
- }
- }
- },
- content = {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- Row(
- (if (isLarge) Modifier.size(200.dp) else Modifier.size(50.dp, 100.dp))
- .intermediateLayout { measurable, constraints, lookaheadSize ->
- parentLookaheadSize = lookaheadSize
+ LookaheadScope {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Row(
+ (if (isLarge) Modifier.size(200.dp) else Modifier.size(50.dp, 100.dp))
+ .intermediateLayout { measurable, constraints ->
+ parentLookaheadSize = lookaheadSize
+ measureWithLambdas().invoke(this, measurable, constraints)
+ }
+ ) {
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .weight(2f)
+ .intermediateLayout { measurable, constraints ->
+ child1LookaheadSize = lookaheadSize
measureWithLambdas().invoke(this, measurable, constraints)
}
- ) {
- Box(
- Modifier
- .fillMaxHeight()
- .weight(2f)
- .intermediateLayout { measurable, constraints, lookaheadSize ->
- child1LookaheadSize = lookaheadSize
- measureWithLambdas().invoke(this, measurable, constraints)
- }
- .animateSize(this@LookaheadLayout)
- )
- Box(
- Modifier
- .fillMaxHeight()
- .weight(3f)
- .intermediateLayout { measurable, constraints, lookaheadSize ->
- child2LookaheadSize = lookaheadSize
- measureWithLambdas().invoke(this, measurable, constraints)
- }
- .animateSize(this@LookaheadLayout)
- )
- }
+ .animateSize()
+ )
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .weight(3f)
+ .intermediateLayout { measurable, constraints ->
+ child2LookaheadSize = lookaheadSize
+ measureWithLambdas().invoke(this, measurable, constraints)
+ }
+ .animateSize()
+ )
}
}
- )
+ }
}
rule.waitForIdle()
rule.runOnIdle {
@@ -577,7 +552,7 @@
Row(Modifier.widthIn(100.dp, 200.dp)) {
Box(
modifier = Modifier
- .intermediateLayout { measurable, constraints, _ ->
+ .intermediateLayout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
// skip placement in the post-lookahead placement pass
@@ -589,7 +564,7 @@
prePlacement = { child1TotalPlacement++ }
).invoke(this, measurable, constraints)
}
- .intermediateLayout { measurable, constraints, _ ->
+ .intermediateLayout { measurable, constraints ->
measureWithLambdas(prePlacement = { child1Placement++ })
.invoke(this, measurable, constraints)
}
@@ -601,7 +576,7 @@
prePlacement = { child2TotalPlacement++ }
).invoke(this, measurable, constraints)
}
- .intermediateLayout { measurable, constraints, _ ->
+ .intermediateLayout { measurable, constraints ->
measureWithLambdas(prePlacement = { child2Placement++ })
.invoke(this, measurable, constraints)
}
@@ -628,26 +603,24 @@
@Composable
private fun MyLookaheadLayout(
+ modifier: Modifier = Modifier,
postMeasure: () -> Unit = {},
postPlacement: () -> Unit = {},
- content: @Composable LookaheadLayoutScope.() -> Unit
+ content: @Composable LookaheadScope.() -> Unit
) {
- LookaheadLayout(
- measurePolicy = { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
+ Box(modifier.layout { measurable, constraints ->
+ measurable.measure(constraints).run {
postMeasure()
// Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
- }
+ layout(width, height) {
+ place(0, 0)
+ }.apply {
postPlacement()
}
- },
- content = { content() }
- )
+ }
+ }) {
+ LookaheadScope(content)
+ }
}
@Test
@@ -660,7 +633,7 @@
Layout(
content = {
Box(Modifier
- .intermediateLayout { measurable, constraints, _ ->
+ .intermediateLayout { measurable, constraints ->
measureWithLambdas(prePlacement = {
placementCount++
}).invoke(this, measurable, constraints)
@@ -700,51 +673,61 @@
@Test
fun localLookaheadPositionOfFromDisjointedLookaheadLayoutsTest() {
- var firstCoordinates: LookaheadLayoutCoordinates? = null
- var secondCoordinates: LookaheadLayoutCoordinates? = null
+ var firstCoordinates: LayoutCoordinates? = null
+ var secondCoordinates: LayoutCoordinates? = null
+ fun LookaheadScope.assertEqualOffset() {
+ val offset = secondCoordinates!!.localPositionOf(firstCoordinates!!, Offset.Zero)
+ val lookaheadOffset = secondCoordinates!!.localLookaheadPositionOf(firstCoordinates!!)
+ assertEquals(offset, lookaheadOffset)
+ }
rule.setContent {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
- MyLookaheadLayout {
+ LookaheadScope {
Box(
Modifier
.size(200.dp)
- .onPlaced { _, it -> firstCoordinates = it })
+ .onPlaced { coords ->
+ firstCoordinates = coords
+ })
}
Box(
Modifier
.padding(top = 30.dp, start = 70.dp)
.offset(40.dp, 60.dp)
) {
- MyLookaheadLayout {
+ LookaheadScope {
Box(
Modifier
.size(100.dp, 50.dp)
- .onPlaced { _, it -> secondCoordinates = it })
+ .onPlaced { coords ->
+ secondCoordinates = coords
+ assertEqualOffset()
+ })
}
}
}
}
- rule.runOnIdle {
- val offset = secondCoordinates!!.localPositionOf(firstCoordinates!!, Offset.Zero)
- val lookaheadOffset = secondCoordinates!!.localLookaheadPositionOf(firstCoordinates!!)
- assertEquals(offset, lookaheadOffset)
- }
}
@Test
fun localLookaheadPositionOfFromNestedLookaheadLayoutsTest() {
- var firstCoordinates: LookaheadLayoutCoordinates? = null
- var secondCoordinates: LookaheadLayoutCoordinates? = null
+ var firstCoordinates: LayoutCoordinates? = null
+ var secondCoordinates: LayoutCoordinates? = null
+ fun LookaheadScope.assertEqualOffset() {
+ val offset = secondCoordinates!!.localPositionOf(firstCoordinates!!, Offset.Zero)
+ val lookaheadOffset = secondCoordinates!!.localLookaheadPositionOf(firstCoordinates!!)
+ assertEquals(offset, lookaheadOffset)
+ }
rule.setContent {
MyLookaheadLayout {
Row(
Modifier
.fillMaxSize()
- .onPlaced { _, it -> firstCoordinates = it },
+ .onPlaced { it -> firstCoordinates = it },
horizontalArrangement = SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
@@ -758,17 +741,14 @@
Box(
Modifier
.size(100.dp, 50.dp)
- .onPlaced { _, it -> secondCoordinates = it })
+ .onPlaced { it ->
+ secondCoordinates = it
+ })
}
}
}
}
}
- rule.runOnIdle {
- val offset = secondCoordinates!!.localPositionOf(firstCoordinates!!, Offset.Zero)
- val lookaheadOffset = secondCoordinates!!.localLookaheadPositionOf(firstCoordinates!!)
- assertEquals(offset, lookaheadOffset)
- }
}
@Test
@@ -897,6 +877,213 @@
}
@Test
+ fun intermediateLayoutMaxHeightIntrinsicsTest() {
+ var rowHeight: Int by mutableStateOf(0)
+ var fraction: Float by mutableStateOf(0f)
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Row(
+ Modifier
+ .height(IntrinsicSize.Max)
+ .onGloballyPositioned {
+ rowHeight = it.size.height
+ }
+ .fillMaxWidth()
+ ) {
+ LookaheadScope {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(
+ lookaheadSize.width,
+ (lookaheadSize.height * fraction).roundToInt()
+ ) {
+ place(0, 0)
+ }
+ }
+ }
+ .width(5.dp)
+ .requiredHeight(300.dp))
+ Box(
+ Modifier
+ .requiredHeight(20.dp)
+ .weight(1f)
+ )
+ }
+ }
+ }
+ }
+
+ repeat(11) {
+ fraction = 0.1f * it
+ rule.runOnIdle {
+ val expectedHeight = max((300 * fraction).toInt(), 20)
+ assertEquals(expectedHeight, rowHeight)
+ }
+ }
+ }
+
+ @Test
+ fun intermediateLayoutMinHeightIntrinsicsTest() {
+ var rowHeight: Int by mutableStateOf(0)
+ var fraction: Float by mutableStateOf(0f)
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Row(
+ Modifier
+ .height(IntrinsicSize.Min)
+ .onGloballyPositioned {
+ rowHeight = it.size.height
+ }
+ .fillMaxWidth()
+ ) {
+ LookaheadScope {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(
+ lookaheadSize.width,
+ (lookaheadSize.height * fraction).roundToInt()
+ ) {
+ place(0, 0)
+ }
+ }
+ }
+ .width(5.dp)
+ .requiredHeight(300.dp))
+ Box(
+ Modifier
+ .requiredHeight(20.dp)
+ .weight(1f)
+ )
+ }
+ }
+ }
+ }
+
+ repeat(11) {
+ fraction = 0.1f * it
+ rule.runOnIdle {
+ val expectedHeight = max((300 * fraction).toInt(), 20)
+ assertEquals(expectedHeight, rowHeight)
+ }
+ }
+ }
+
+ @Test
+ fun intermediateLayoutMinWidthIntrinsicsTest() {
+ var rowWidth: Int by mutableStateOf(0)
+ var fraction: Float by mutableStateOf(0f)
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Column(
+ Modifier
+ .width(IntrinsicSize.Min)
+ .onGloballyPositioned {
+ rowWidth = it.size.width
+ }
+ .height(50.dp)
+ ) {
+ LookaheadScope {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(
+ (lookaheadSize.width * fraction).roundToInt(),
+ lookaheadSize.height
+ ) {
+ place(0, 0)
+ }
+ }
+ }
+ .requiredWidth(300.dp))
+ Box(
+ Modifier
+ .requiredWidth(120.dp)
+ .height(10.dp)
+ )
+ Text(
+ text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec" +
+ " non felis euismod nunc commodo pharetra a nec eros. Sed varius," +
+ " metus sed facilisis condimentum, orci orci aliquet arcu",
+ Modifier.fillMaxWidth(),
+ )
+ }
+ }
+ }
+ }
+
+ repeat(11) {
+ fraction = 0.1f * it
+ rule.runOnIdle {
+ val expectedWidth = max((300 * fraction).toInt(), 120)
+ assertEquals(expectedWidth, rowWidth)
+ }
+ }
+ }
+
+ @Test
+ fun intermediateLayoutMaxWidthIntrinsicsTest() {
+ var boxSize: IntSize by mutableStateOf(IntSize.Zero)
+ var childBoxSize: IntSize by mutableStateOf(IntSize.Zero)
+ var fraction: Float by mutableStateOf(0f)
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier
+ .height(50.dp)
+ .width(IntrinsicSize.Max)
+ .onGloballyPositioned {
+ boxSize = it.size
+ }) {
+ LookaheadScope {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(
+ (lookaheadSize.width * fraction).roundToInt(),
+ lookaheadSize.height
+ ) {
+ place(0, 0)
+ }
+ }
+ }
+ .requiredWidth(100.dp))
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(10.dp)
+ .onGloballyPositioned {
+ childBoxSize = it.size
+ })
+ }
+ }
+ }
+ }
+
+ repeat(11) {
+ fraction = 0.1f * it
+ rule.runOnIdle {
+ assertEquals(IntSize((100 * fraction).toInt(), 50), boxSize)
+ assertEquals(IntSize((100 * fraction).toInt(), 10), childBoxSize)
+ }
+ }
+ }
+
+ @Test
fun firstBaselineAlignmentInLookaheadLayout() {
assertSameLayoutWithAndWithoutLookahead { modifier ->
Box(modifier.fillMaxWidth()) {
@@ -982,26 +1169,34 @@
val matrix = Matrix()
rule.setContent {
CompositionLocalProvider(LocalDensity provides Density(1f)) {
- LookaheadLayout(
- measurePolicy = { measurables, constraints ->
- val placeable = measurables[0].measure(constraints)
- // Position the children.
- layout(placeable.width + 10, placeable.height + 10) {
- placeable.place(10, 10)
+ LookaheadScope {
+ Layout(
+ measurePolicy = { measurables, constraints ->
+ val placeable = measurables[0].measure(constraints)
+ // Position the children.
+ layout(placeable.width + 10, placeable.height + 10) {
+ placeable.place(10, 10)
+ }
+ },
+ content = {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ coordinates!!.transformFrom(
+ lookaheadScopeCoordinates,
+ matrix
+ )
+ }
+ }
+ }
+ .size(10.dp))
}
- },
- content = {
- Box(
- Modifier
- .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
- layoutCoordinates.transformFrom(
- lookaheadScopeCoordinates,
- matrix
- )
- }
- .size(10.dp))
- }
- )
+ )
+ }
}
}
rule.waitForIdle()
@@ -1010,10 +1205,338 @@
}
@Test
+ fun moveLookaheadScope() {
+ var scopePositionInRoot by mutableStateOf(IntOffset(Int.MAX_VALUE, Int.MAX_VALUE))
+ var firstBox by mutableStateOf(true)
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity.provides(Density(1f))) {
+ Box {
+ val movableContent = remember {
+ movableContentOf {
+ LookaheadScope {
+ Box(
+ Modifier
+ .layout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ scopePositionInRoot =
+ lookaheadScopeCoordinates
+ .localToRoot(Offset.Zero)
+ .round()
+ place(0, 0)
+ }
+ }
+ }
+ .size(200.dp))
+ }
+ }
+ }
+
+ Box(Modifier.offset(100.dp, 5.dp)) {
+ if (firstBox) {
+ movableContent()
+ }
+ }
+ Box(Modifier.offset(40.dp, 200.dp)) {
+ if (!firstBox) {
+ movableContent()
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertEquals(IntOffset(100, 5), scopePositionInRoot)
+ firstBox = false
+ rule.waitForIdle()
+ assertEquals(IntOffset(40, 200), scopePositionInRoot)
+ }
+
+ @Test
+ fun moveIntermediateLayout() {
+ var positionInScope by mutableStateOf(IntOffset(Int.MAX_VALUE, Int.MAX_VALUE))
+ var boxId by mutableStateOf(1)
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity.provides(Density(1f))) {
+ val movableContent = remember {
+ movableContentOf {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ coordinates?.let {
+ positionInScope =
+ lookaheadScopeCoordinates
+ .localLookaheadPositionOf(
+ it
+ )
+ .round()
+ }
+ }
+ }
+ }
+ .size(200.dp))
+ }
+ }
+ Box {
+ LookaheadScope {
+ Box(Modifier.offset(100.dp, 5.dp)) {
+ if (boxId == 1) {
+ movableContent()
+ }
+ }
+ }
+ }
+ Box(Modifier.offset(40.dp, 200.dp)) {
+ if (boxId == 2) {
+ movableContent()
+ }
+ }
+ Box(Modifier
+ .offset(50.dp, 50.dp)
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ place(0, 0)
+ }
+ }
+ }
+ .offset(60.dp, 60.dp)) {
+ if (boxId == 3) {
+ movableContent()
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertEquals(IntOffset(100, 5), positionInScope)
+ boxId++
+ rule.waitForIdle()
+ // Expect no offset when moving intermediateLayout out of LookaheadScope, as the implicitly
+ // created lookahead scope will have the same coordinates as intermediateLayout
+ assertEquals(IntOffset(0, 0), positionInScope)
+ boxId++
+ rule.waitForIdle()
+ // Expect the lookaheadScope to be created by the ancestor intermediateLayoutModifier
+ assertEquals(IntOffset(60, 60), positionInScope)
+ }
+
+ @Test
+ fun nestedVirtualNodes() {
+ val root = node().also {
+ createDelegate(it)
+ }
+ val virtualGrandParent = LayoutNode(isVirtual = true)
+ val virtualParent = LayoutNode(isVirtual = true)
+ val child = node()
+ root.add(virtualGrandParent)
+ virtualGrandParent.add(virtualParent)
+ virtualParent.add(child)
+ val newChild = node()
+ rule.runOnIdle {
+ assertEquals(1, root.children.size)
+ assertEquals(1, virtualGrandParent.children.size)
+ assertEquals(1, virtualParent.children.size)
+ assertEquals(child, root.children[0])
+ assertEquals(child, virtualGrandParent.children[0])
+ assertEquals(child, virtualParent.children[0])
+ // Add another child to virtual parent.
+ virtualParent.add(newChild)
+ }
+ rule.runOnIdle {
+ assertEquals(2, root.children.size)
+ assertEquals(2, virtualGrandParent.children.size)
+ assertEquals(2, virtualParent.children.size)
+ assertEquals(child, root.children[0])
+ assertEquals(child, virtualGrandParent.children[0])
+ assertEquals(child, virtualParent.children[0])
+ assertEquals(newChild, root.children[1])
+ assertEquals(newChild, virtualGrandParent.children[1])
+ assertEquals(newChild, virtualParent.children[1])
+ }
+ }
+
+ @Test
+ fun nestedLookaheadScope() {
+ rule.setContent {
+ Box(Modifier.offset(20.dp, 30.dp)) {
+ LookaheadScope {
+ Box(
+ Modifier
+ .offset(50.dp, 25.dp)
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(prePlacement = {
+ val outerLookaheadScopeCoords = with(this@LookaheadScope) {
+ lookaheadScopeCoordinates
+ }
+ assertEquals(
+ outerLookaheadScopeCoords,
+ lookaheadScopeCoordinates
+ )
+ })(measurable, constraints)
+ }
+ .offset(15.dp, 20.dp)
+ ) {
+ LookaheadScope {
+ val innerLookaheadScope = this
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(prePlacement = {
+ val innerLookaheadCoords = with(innerLookaheadScope) {
+ lookaheadScopeCoordinates
+ }
+ assertEquals(
+ innerLookaheadCoords,
+ lookaheadScopeCoordinates
+ )
+ })(measurable, constraints)
+ }
+ .size(50.dp)
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(prePlacement = {
+ val innerLookaheadCoords = with(innerLookaheadScope) {
+ lookaheadScopeCoordinates
+ }
+ assertEquals(
+ innerLookaheadCoords,
+ lookaheadScopeCoordinates
+ )
+ })(measurable, constraints)
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun lookaheadScopeInImplicitScope() {
+ rule.setContent {
+ Box(Modifier.offset(20.dp, 30.dp)) {
+ Box(
+ Modifier
+ .offset(50.dp, 25.dp)
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(prePlacement = {
+ assertEquals(
+ coordinates!!,
+ lookaheadScopeCoordinates
+ )
+ })(measurable, constraints)
+ }
+ .offset(15.dp, 20.dp)
+ ) {
+ LookaheadScope {
+ val explicitLookaheadScope = this
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(prePlacement = {
+ val innerLookaheadCoords =
+ with(explicitLookaheadScope) {
+ lookaheadScopeCoordinates
+ }
+ assertEquals(
+ innerLookaheadCoords,
+ lookaheadScopeCoordinates
+ )
+ })(measurable, constraints)
+ }
+ .size(50.dp)
+ .intermediateLayout { measurable, constraints ->
+ measureWithLambdas(prePlacement = {
+ val innerLookaheadCoords =
+ with(explicitLookaheadScope) {
+ lookaheadScopeCoordinates
+ }
+ assertEquals(
+ innerLookaheadCoords,
+ lookaheadScopeCoordinates
+ )
+ })(measurable, constraints)
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun nestedVirtualNodeFromLookaheadScope() {
+ var small by mutableStateOf(true)
+ fun size1(): Int = if (small) 20 else 50
+ fun size2(): Int = if (small) 60 else 120
+ fun offset1(): IntOffset = if (small) IntOffset.Zero else IntOffset(80, 30)
+ fun offset2(): IntOffset = if (small) IntOffset(50, 100) else IntOffset(60, 170)
+ var actualSize1: IntSize = IntSize.Zero
+ var actualSize2: IntSize = IntSize.Zero
+ var actualOffset1: IntOffset = IntOffset.Zero
+ var actualOffset2: IntOffset = IntOffset.Zero
+
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity.provides(Density(1f))) {
+ Box {
+ LookaheadScope {
+ LookaheadScope {
+ Box(
+ Modifier
+ .offset(offset1().x.dp, offset1().y.dp)
+ .onGloballyPositioned {
+ actualSize1 = it.size
+ actualOffset1 = it
+ .localToRoot(Offset.Zero)
+ .round()
+ }
+ .size(size1().dp)
+ )
+ }
+ Box(
+ Modifier
+ .offset(offset2().x.dp, offset2().y.dp)
+ .onGloballyPositioned {
+ actualSize2 = it.size
+ actualOffset2 = it
+ .localToRoot(Offset.Zero)
+ .round()
+ }
+ .size(size2().dp)
+ )
+ }
+ }
+ }
+ }
+ rule.runOnIdle {
+ assertEquals(IntSize(size1(), size1()), actualSize1)
+ assertEquals(IntSize(size2(), size2()), actualSize2)
+ assertEquals(offset1(), actualOffset1)
+ assertEquals(offset2(), actualOffset2)
+ small = !small
+ }
+ // Change the size & position of LayoutNode in the nested virtual node
+ rule.runOnIdle {
+ assertEquals(IntSize(size1(), size1()), actualSize1)
+ assertEquals(IntSize(size2(), size2()), actualSize2)
+ assertEquals(offset1(), actualOffset1)
+ assertEquals(offset2(), actualOffset2)
+ }
+ }
+
+ @Test
fun multiMeasureLayoutInLookahead() {
var horizontal by mutableStateOf(true)
rule.setContent {
- MyLookaheadLayout {
+ LookaheadScope {
@Suppress("DEPRECATION")
MultiMeasureLayout(
content = {
@@ -1081,13 +1604,15 @@
)
})
} else {
- MyLookaheadLayout {
- content(
- modifier = Modifier
- .trackSizeAndPosition(sizes, positions)
- .assertSameSizeAndPosition(this)
- )
- }
+ Layout(measurePolicy = defaultMeasurePolicy, content = {
+ LookaheadScope {
+ content(
+ modifier = Modifier
+ .trackSizeAndPosition(sizes, positions)
+ .assertSameSizeAndPosition(this)
+ )
+ }
+ })
}
}
rule.runOnIdle {
@@ -1111,24 +1636,27 @@
}
}
- private fun Modifier.assertSameSizeAndPosition(scope: LookaheadLayoutScope) = composed {
+ private fun Modifier.assertSameSizeAndPosition(scope: LookaheadScope) = composed {
var lookaheadSize by remember {
mutableStateOf(IntSize.Zero)
}
- var lookaheadLayoutCoordinates: LookaheadLayoutCoordinates? by remember {
+ var lookaheadLayoutCoordinates: LayoutCoordinates? by remember {
mutableStateOf(
null
)
}
- var onPlacedCoordinates: LookaheadLayoutCoordinates? by remember { mutableStateOf(null) }
+ var onPlacedCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
with(scope) {
this@composed
- .intermediateLayout { measurable, constraints, targetSize ->
- lookaheadSize = targetSize
- measureWithLambdas().invoke(this, measurable, constraints)
+ .intermediateLayout { measurable, constraints ->
+ lookaheadSize = this.lookaheadSize
+ measureWithLambdas(
+ prePlacement = {
+ lookaheadLayoutCoordinates = lookaheadScopeCoordinates
+ }
+ ).invoke(this, measurable, constraints)
}
- .onPlaced { scopeRoot, it ->
- lookaheadLayoutCoordinates = scopeRoot
+ .onPlaced { it ->
onPlacedCoordinates = it
}
.onGloballyPositioned {
@@ -1162,22 +1690,23 @@
println()
}
- private val defaultMeasurePolicy: MeasurePolicy = MeasurePolicy { measurables, constraints ->
- val placeables = measurables.map { it.measure(constraints) }
- val maxWidth: Int = placeables.maxOf { it.width }
- val maxHeight = placeables.maxOf { it.height }
- // Position the children.
- layout(maxWidth, maxHeight) {
- placeables.forEach {
- it.place(0, 0)
+ private val defaultMeasurePolicy: MeasurePolicy =
+ MeasurePolicy { measurables, constraints ->
+ val placeables = measurables.map { it.measure(constraints) }
+ val maxWidth: Int = placeables.maxOf { it.width }
+ val maxHeight = placeables.maxOf { it.height }
+ // Position the children.
+ layout(maxWidth, maxHeight) {
+ placeables.forEach {
+ it.place(0, 0)
+ }
}
}
- }
private fun measureWithLambdas(
preMeasure: () -> Unit = {},
postMeasure: (IntSize) -> Unit = {},
- prePlacement: () -> Unit = {},
+ prePlacement: Placeable.PlacementScope.() -> Unit = {},
postPlacement: () -> Unit = {}
): MeasureScope.(Measurable, Constraints) -> MeasureResult = { measurable, constraints ->
preMeasure()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
index b492045..584da15 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
@@ -117,7 +117,7 @@
fun measureInModifierPlacementWithLookaheadLayout() {
var childSize = IntSize.Zero
rule.setContent {
- LookaheadLayout(content = @Composable {
+ LookaheadScope {
val measureInPlaceModifier = Modifier.layout { measurable, constraints ->
layout(100, 100) {
val p = measurable.measure(constraints)
@@ -132,12 +132,7 @@
) {
Box(Modifier.size(10.dp))
}
- }, measurePolicy = { measurables, constraints ->
- val p = measurables[0].measure(constraints)
- layout(p.width, p.height) {
- p.place(0, 0)
- }
- })
+ }
}
rule.waitForIdle()
@@ -154,7 +149,7 @@
fun measureInLayoutPlacementWithLookaheadLayout() {
var childSize = IntSize.Zero
rule.setContent {
- LookaheadLayout(content = @Composable {
+ LookaheadScope {
Layout(modifier = Modifier.fillMaxSize(), content = @Composable {
Box(Modifier.size(10.dp))
}) { measurables, constraints ->
@@ -164,12 +159,7 @@
p.place(0, 0)
}
}
- }, measurePolicy = { measurables, constraints ->
- val p = measurables[0].measure(constraints)
- layout(p.width, p.height) {
- p.place(0, 0)
- }
- })
+ }
}
rule.waitForIdle()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
index 7586484..b048f0e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
@@ -131,7 +131,7 @@
var boxSize by mutableStateOf(IntSize.Zero)
var alignment by mutableStateOf(Alignment.Center)
rule.setContent {
- SimpleLookaheadLayout {
+ LookaheadScope {
Box(Modifier.fillMaxSize()) {
Box(
Modifier
@@ -213,7 +213,7 @@
fun coordinatesWhileAligningWithLookaheadLayout() {
val locations = mutableStateListOf<LayoutCoordinates?>()
rule.setContent {
- SimpleLookaheadLayout {
+ LookaheadScope {
Row(Modifier.fillMaxSize()) {
Box(Modifier.alignByBaseline()) {
Text("Hello")
@@ -279,7 +279,7 @@
fun coordinatesWhileAligningInLookaheadLayout() {
val locations = mutableStateListOf<LayoutCoordinates?>()
rule.setContent {
- SimpleLookaheadLayout {
+ LookaheadScope {
Row(Modifier.fillMaxSize()) {
Box(Modifier.alignByBaseline()) {
Text("Hello")
@@ -670,16 +670,3 @@
assertEquals(1, locations.size)
}
}
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-private fun SimpleLookaheadLayout(content: @Composable LookaheadLayoutScope.() -> Unit) {
- LookaheadLayout(
- content = content, measurePolicy = { measurables, constraints ->
- val p = measurables[0].measure(constraints)
- layout(p.width, p.height) {
- p.place(0, 0)
- }
- }
- )
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
index 327a04d..6ae25e4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/modifier/ModifierNodeReuseAndDeactivationTest.kt
@@ -41,7 +41,10 @@
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverNode
import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Constraints
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -763,6 +766,47 @@
assertThat(receivedValue).isEqualTo(1)
}
}
+
+ @Test
+ fun placingChildWithReusedUnchangedModifier() {
+ // regression test for b/271430143
+ var active by mutableStateOf(true)
+ var modifier by mutableStateOf(StatelessLayoutElement1.then(StatelessLayoutElement2))
+ var childX by mutableStateOf(0)
+
+ rule.setContent {
+ ReusableContentHost(active) {
+ ReusableContent(0) {
+ Layout(content = {
+ Layout(
+ modifier = modifier.testTag("child"),
+ measurePolicy = MeasurePolicy
+ )
+ }) { measurables, constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ childX.toString()
+ placeable.place(childX, 0)
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ active = false
+ }
+
+ rule.runOnIdle {
+ active = true
+ modifier = StatelessLayoutElement1
+ // force relayout parent
+ childX = 10
+ }
+
+ rule.onNodeWithTag("child")
+ .assertLeftPositionInRootIsEqualTo(with(rule.density) { 10.toDp() })
+ }
}
@Composable
@@ -984,3 +1028,41 @@
measureBlock.invoke()
}
}
+
+private object StatelessLayoutElement1 : ModifierNodeElement<StatelessLayoutModifier1>() {
+ override fun create() = StatelessLayoutModifier1()
+ override fun update(node: StatelessLayoutModifier1) = node
+ override fun hashCode(): Int = 241
+ override fun equals(other: Any?) = other === this
+}
+
+private class StatelessLayoutModifier1 : Modifier.Node(), LayoutModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+}
+
+private object StatelessLayoutElement2 : ModifierNodeElement<StatelessLayoutModifier2>() {
+ override fun create() = StatelessLayoutModifier2()
+ override fun update(node: StatelessLayoutModifier2) = node
+ override fun hashCode(): Int = 242
+ override fun equals(other: Any?) = other === this
+}
+
+private class StatelessLayoutModifier2 : Modifier.Node(), LayoutModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt
index 27cee1b..c11ac21 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt
@@ -22,7 +22,6 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.layout.Layout
@@ -35,7 +34,6 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalComposeUiApi::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class CompositionLocalModifierNodeTest {
@@ -188,7 +186,6 @@
}
}
- @ExperimentalComposeUiApi
private inline fun <reified T : Modifier.Node> modifierNodeElementOf(
crossinline create: () -> T
): ModifierNodeElement<T> = object : ModifierNodeElement<T>() {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 60eb74f..7b4378c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -125,6 +125,7 @@
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.platform.MotionEventVerifierApi29.isValidMotionEvent
import androidx.compose.ui.semantics.EmptySemanticsModifierNodeElement
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
@@ -1607,10 +1608,26 @@
}
private fun isBadMotionEvent(event: MotionEvent): Boolean {
- return !event.x.isFinite() ||
+ var eventInvalid = !event.x.isFinite() ||
!event.y.isFinite() ||
!event.rawX.isFinite() ||
!event.rawY.isFinite()
+
+ if (!eventInvalid) {
+ // First event x,y is checked above if block, so we can skip index 0.
+ for (index in 1 until event.pointerCount) {
+ eventInvalid = !event.getX(index).isFinite() || !event.getY(index).isFinite() ||
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ !isValidMotionEvent(event, index)
+ } else {
+ false
+ }
+
+ if (eventInvalid) break
+ }
+ }
+
+ return eventInvalid
}
private fun isPositionChanged(event: MotionEvent): Boolean {
@@ -1618,7 +1635,8 @@
return true
}
val lastEvent = previousMotionEvent
- return lastEvent == null || event.rawX != lastEvent.rawX || event.rawY != lastEvent.rawY
+ return lastEvent == null || lastEvent.pointerCount != event.pointerCount ||
+ event.rawX != lastEvent.rawX || event.rawY != lastEvent.rawY
}
private fun findViewByAccessibilityIdRootedAtCurrentView(
@@ -1939,3 +1957,11 @@
preTransform(tmpMatrix)
}
}
+
+@RequiresApi(29)
+private object MotionEventVerifierApi29 {
+ @DoNotInline
+ fun isValidMotionEvent(event: MotionEvent, index: Int): Boolean {
+ return event.getRawX(index).isFinite() && event.getRawY(index).isFinite()
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 524d0ec..682cb01 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -28,16 +28,13 @@
import androidx.compose.runtime.Updater
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.currentCompositeKeyHash
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
import androidx.compose.runtime.saveable.SaveableStateRegistry
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
-import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.materialize
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.UiApplier
@@ -98,10 +95,7 @@
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
) {
- val dispatcher = remember { NestedScrollDispatcher() }
- val materializedModifier = currentComposer.materialize(
- modifier.nestedScroll(NoOpScrollConnection, dispatcher)
- )
+ val materializedModifier = currentComposer.materialize(modifier)
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
@@ -113,7 +107,7 @@
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
ComposeNode<LayoutNode, UiApplier>(
- factory = createAndroidViewNodeFactory(factory, dispatcher),
+ factory = createAndroidViewNodeFactory(factory),
update = {
updateViewHolderParams<T>(
modifier = materializedModifier,
@@ -192,20 +186,7 @@
update: (T) -> Unit = NoOpUpdate,
onRelease: (T) -> Unit = NoOpUpdate
) {
- // TODO: There is a potential edge case with nested scrolling where this dispatcher may become
- // out of sync in the ViewHolder and in the materialized Modifier. This will happen whenever
- // the node is reused because remember blocks are reset and therefore a new dispatcher will be
- // created when reusing an AndroidView composable, but the new dispatcher will not make its
- // way into the AndroidViewHolder, meaning the composable and view will no longer be connected
- // to one another after reuse. This can be addressed by moving the nested scrolling behavior
- // into AndroidViewHolder, which requires refactoring the nestedScroll modifier to
- // Modifier.Node (in progress in aosp/2404403). This only affects this overload of AndroidView,
- // and not the stable, non-reusable AndroidView API.
- val dispatcher = remember { NestedScrollDispatcher() }
- val materializedModifier = currentComposer.materialize(
- modifier.nestedScroll(NoOpScrollConnection, dispatcher)
- )
-
+ val materializedModifier = currentComposer.materialize(modifier)
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
@@ -217,7 +198,7 @@
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
ReusableComposeNode<LayoutNode, UiApplier>(
- factory = createAndroidViewNodeFactory(factory, dispatcher),
+ factory = createAndroidViewNodeFactory(factory),
update = {
updateViewHolderParams<T>(
modifier = materializedModifier,
@@ -235,8 +216,7 @@
@Composable
private fun <T : View> createAndroidViewNodeFactory(
- factory: (Context) -> T,
- dispatcher: NestedScrollDispatcher
+ factory: (Context) -> T
): () -> LayoutNode {
val context = LocalContext.current
val parentReference = rememberCompositionContext()
@@ -248,7 +228,6 @@
context = context,
factory = factory,
parentContext = parentReference,
- dispatcher = dispatcher,
saveStateRegistry = stateRegistry,
saveStateKey = stateKey
).layoutNode
@@ -286,18 +265,12 @@
*/
val NoOpUpdate: View.() -> Unit = {}
-/**
- * No-op Connection required by nested scroll modifier. This is No-op because we don't want
- * to influence nested scrolling with it and it is required by [Modifier.nestedScroll].
- */
-private val NoOpScrollConnection = object : NestedScrollConnection {}
-
internal class ViewFactoryHolder<T : View> private constructor(
context: Context,
parentContext: CompositionContext? = null,
val typedView: T,
// NestedScrollDispatcher that will be passed/used for nested scroll interop
- val dispatcher: NestedScrollDispatcher,
+ val dispatcher: NestedScrollDispatcher = NestedScrollDispatcher(),
private val saveStateRegistry: SaveableStateRegistry?,
private val saveStateKey: String
) : AndroidViewHolder(context, parentContext, dispatcher, typedView), ViewRootForInspector {
@@ -306,13 +279,11 @@
context: Context,
factory: (Context) -> T,
parentContext: CompositionContext? = null,
- dispatcher: NestedScrollDispatcher,
saveStateRegistry: SaveableStateRegistry?,
saveStateKey: String
) : this(
context = context,
typedView = factory(context),
- dispatcher = dispatcher,
parentContext = parentContext,
saveStateRegistry = saveStateRegistry,
saveStateKey = saveStateKey,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 8831633..4f322a1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -33,8 +33,10 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
@@ -306,6 +308,7 @@
layoutNode.interopViewFactoryHolder = this@AndroidViewHolder
val coreModifier = Modifier
+ .nestedScroll(NoOpScrollConnection, dispatcher)
.semantics(true) {}
.pointerInteropFilter(this)
.drawBehind {
@@ -549,6 +552,12 @@
private const val Unmeasured = Int.MIN_VALUE
+/**
+ * No-op Connection required by nested scroll modifier. This is No-op because we don't want
+ * to influence nested scrolling with it and it is required by [Modifier.nestedScroll].
+ */
+private val NoOpScrollConnection = object : NestedScrollConnection {}
+
private fun Int.toComposeOffset() = toFloat() * -1
private fun Float.toComposeVelocity(): Float = this * -1f
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 3c9fcbc..f2a353b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -38,7 +38,7 @@
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.node.invalidateLayer
-import androidx.compose.ui.node.invalidateLayout
+import androidx.compose.ui.node.invalidateMeasurements
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -81,7 +81,7 @@
* IMPORTANT NOTE: This class sets [androidx.compose.ui.node.ModifierNodeElement.autoInvalidate]
* to false which means it MUST invalidate both draw and the layout. It invalidates both in the
* [PainterModifierNodeElement.update] method through [LayoutModifierNode.invalidateLayer]
- * (invalidates draw) and [LayoutModifierNode.invalidateLayout] (invalidates layout).
+ * (invalidates draw) and [LayoutModifierNode.invalidateMeasurements] (invalidates measure).
*
* @param painter used to paint content
* @param sizeToIntrinsics `true` to size the element relative to [Painter.intrinsicSize]
@@ -115,7 +115,7 @@
}
override fun update(node: PainterModifierNode): PainterModifierNode {
- val invalidateLayout = node.sizeToIntrinsics != sizeToIntrinsics ||
+ val intrinsicsChanged = node.sizeToIntrinsics != sizeToIntrinsics ||
(sizeToIntrinsics && node.painter.intrinsicSize != painter.intrinsicSize)
node.painter = painter
@@ -125,13 +125,12 @@
node.alpha = alpha
node.colorFilter = colorFilter
- // Only invalidate layout if Intrinsics have changed.
- if (invalidateLayout) {
- node.invalidateLayout()
- } else {
- // Otherwise, redraw because one of the node properties has changed.
- node.invalidateDraw()
+ // Only remeasure if intrinsics have changed.
+ if (intrinsicsChanged) {
+ node.invalidateMeasurements()
}
+ // redraw because one of the node properties has changed.
+ node.invalidateDraw()
return node
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index 7044bb7..5b9ce1f4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -140,7 +140,7 @@
* [Modifier.focusProperties][focusProperties] implies that we want to block focus search
* from proceeding in the specified [direction][FocusDirection].
*
- * @sample androidx.compose.ui.samples.CancelFocusMoveSample()
+ * @sample androidx.compose.ui.samples.CancelFocusMoveSample
*/
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalComposeUiApi
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt
index 2381aee..1bf2e8c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifier.kt
@@ -16,16 +16,15 @@
package androidx.compose.ui.input.nestedscroll
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.modifier.ModifierLocalNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Velocity
import kotlinx.coroutines.CoroutineScope
-import androidx.compose.ui.internal.JvmDefaultWithCompatibility
/**
* Interface to connect to the nested scroll system.
@@ -115,11 +114,13 @@
*/
class NestedScrollDispatcher {
+ internal var modifierLocalNode: ModifierLocalNode? = null
+
// lambda to calculate the most outer nested scroll scope for this dispatcher on demand
- internal var calculateNestedScrollScope: () -> CoroutineScope? = { originNestedScrollScope }
+ internal var calculateNestedScrollScope: () -> CoroutineScope? = { scope }
// the original nested scroll scope for this dispatcher (immediate scope it was created in)
- internal var originNestedScrollScope: CoroutineScope? = null
+ internal var scope: CoroutineScope? = null
/**
* Get the outer coroutine scope to dispatch nested fling on.
@@ -151,7 +152,10 @@
* Parent to be set when attached to nested scrolling chain. `null` is valid and means there no
* nested scrolling parent above
*/
- internal var parent: NestedScrollConnection? = null
+ internal val parent: NestedScrollConnection?
+ get() = modifierLocalNode?.run {
+ ModifierLocalNestedScroll.current
+ }
/**
* Dispatch pre scroll pass. This triggers [NestedScrollConnection.onPreScroll] on all the
@@ -232,6 +236,7 @@
Fling -> "Fling"
@OptIn(ExperimentalComposeUiApi::class)
Relocate -> "Relocate"
+
else -> "Invalid"
}
}
@@ -327,18 +332,38 @@
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
+): Modifier = this then NestedScrollElement(connection, dispatcher)
+
+private class NestedScrollElement(
+ val connection: NestedScrollConnection,
+ val dispatcher: NestedScrollDispatcher?
+) : ModifierNodeElement<NestedScrollNode>() {
+ override fun create(): NestedScrollNode {
+ return NestedScrollNode(connection, dispatcher)
+ }
+
+ override fun update(node: NestedScrollNode): NestedScrollNode {
+ node.connection = connection
+ node.updateDispatcher(dispatcher)
+ return node
+ }
+
+ override fun hashCode(): Int {
+ var result = connection.hashCode()
+ result = 31 * result + dispatcher.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is NestedScrollElement) return false
+ if (other.connection != connection) return false
+ if (other.dispatcher != dispatcher) return false
+ return true
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
name = "nestedScroll"
properties["connection"] = connection
properties["dispatcher"] = dispatcher
}
-) {
- val scope = rememberCoroutineScope()
- // provide noop dispatcher if needed
- val resolvedDispatcher = dispatcher ?: remember { NestedScrollDispatcher() }
- remember(connection, resolvedDispatcher, scope) {
- resolvedDispatcher.originNestedScrollScope = scope
- NestedScrollModifierLocal(resolvedDispatcher, connection)
- }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierLocal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierLocal.kt
deleted file mode 100644
index f137040..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierLocal.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.input.nestedscroll
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.unit.Velocity
-import kotlinx.coroutines.CoroutineScope
-
-internal val ModifierLocalNestedScroll = modifierLocalOf<NestedScrollModifierLocal?> { null }
-
-/**
- * NestedScroll using ModifierLocal as implementation.
- */
-internal class NestedScrollModifierLocal(
- val dispatcher: NestedScrollDispatcher,
- val connection: NestedScrollConnection
-) : ModifierLocalConsumer, ModifierLocalProvider<NestedScrollModifierLocal?>,
- NestedScrollConnection {
- init {
- dispatcher.calculateNestedScrollScope = { nestedCoroutineScope }
- }
-
- private var parent: NestedScrollModifierLocal? by mutableStateOf(null)
-
- private val nestedCoroutineScope: CoroutineScope
- get() = parent?.nestedCoroutineScope
- ?: dispatcher.originNestedScrollScope
- ?: throw IllegalStateException(
- "in order to access nested coroutine scope you need to attach dispatcher to the " +
- "`Modifier.nestedScroll` first."
- )
-
- override val key: ProvidableModifierLocal<NestedScrollModifierLocal?>
- get() = ModifierLocalNestedScroll
-
- override val value: NestedScrollModifierLocal
- get() = this
-
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
- parent = ModifierLocalNestedScroll.current
- dispatcher.parent = parent
- }
-
- override fun onPreScroll(
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- val parentPreConsumed = parent?.onPreScroll(available, source) ?: Offset.Zero
- val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
- return parentPreConsumed + selfPreConsumed
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- val selfConsumed = connection.onPostScroll(consumed, available, source)
- val parentConsumed =
- parent?.onPostScroll(consumed + selfConsumed, available - selfConsumed, source)
- ?: Offset.Zero
- return selfConsumed + parentConsumed
- }
-
- override suspend fun onPreFling(available: Velocity): Velocity {
- val parentPreConsumed = parent?.onPreFling(available) ?: Velocity.Zero
- val selfPreConsumed = connection.onPreFling(available - parentPreConsumed)
- return parentPreConsumed + selfPreConsumed
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- val selfConsumed = connection.onPostFling(consumed, available)
- val parentConsumed =
- parent?.onPostFling(consumed + selfConsumed, available - selfConsumed) ?: Velocity.Zero
- return selfConsumed + parentConsumed
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt
new file mode 100644
index 0000000..093b26d
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollNode.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.nestedscroll
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.modifier.ModifierLocalMap
+import androidx.compose.ui.modifier.ModifierLocalNode
+import androidx.compose.ui.modifier.modifierLocalMapOf
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.unit.Velocity
+import kotlinx.coroutines.CoroutineScope
+
+internal val ModifierLocalNestedScroll = modifierLocalOf<NestedScrollNode?> { null }
+
+/**
+ * NestedScroll using ModifierLocal as implementation.
+ */
+internal class NestedScrollNode(
+ var connection: NestedScrollConnection,
+ dispatcher: NestedScrollDispatcher?
+) : ModifierLocalNode, NestedScrollConnection, DelegatingNode() {
+
+ // Resolved dispatcher for re-use in case of null dispatcher is passed.
+ private var resolvedDispatcher: NestedScrollDispatcher
+
+ init {
+ resolvedDispatcher = dispatcher ?: NestedScrollDispatcher() // Resolve null dispatcher
+ }
+
+ private val parentModifierLocal: NestedScrollNode?
+ get() = if (isAttached) ModifierLocalNestedScroll.current else null
+
+ private val parentConnection: NestedScrollConnection?
+ get() = ModifierLocalNestedScroll.current
+
+ override val providedValues: ModifierLocalMap
+ get() = modifierLocalMapOf(ModifierLocalNestedScroll to this)
+
+ private val nestedCoroutineScope: CoroutineScope
+ get() = parentModifierLocal?.nestedCoroutineScope
+ ?: resolvedDispatcher.scope
+ ?: throw IllegalStateException(
+ "in order to access nested coroutine scope you need to attach dispatcher to the " +
+ "`Modifier.nestedScroll` first."
+ )
+
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val parentPreConsumed = parentConnection?.onPreScroll(available, source) ?: Offset.Zero
+ val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
+ return parentPreConsumed + selfPreConsumed
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val selfConsumed = connection.onPostScroll(consumed, available, source)
+ val parentConsumed = parentConnection?.onPostScroll(
+ consumed + selfConsumed,
+ available - selfConsumed,
+ source
+ ) ?: Offset.Zero
+ return selfConsumed + parentConsumed
+ }
+
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ val parentPreConsumed = parentConnection?.onPreFling(available) ?: Velocity.Zero
+ val selfPreConsumed = connection.onPreFling(available - parentPreConsumed)
+ return parentPreConsumed + selfPreConsumed
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+
+ val selfConsumed = connection.onPostFling(consumed, available)
+ val parentConsumed = parentConnection?.onPostFling(
+ consumed + selfConsumed,
+ available - selfConsumed
+ ) ?: Velocity.Zero
+ return selfConsumed + parentConsumed
+ }
+
+ // On receiving a new dispatcher, re-setting fields
+ fun updateDispatcher(newDispatcher: NestedScrollDispatcher?) {
+ resetDispatcherFields() // Reset fields of current dispatcher.
+
+ // Update dispatcher associated with this node.
+ if (newDispatcher == null) {
+ resolvedDispatcher = NestedScrollDispatcher()
+ } else if (newDispatcher != resolvedDispatcher) {
+ resolvedDispatcher = newDispatcher
+ }
+
+ // Update fields of the newly set dispatcher.
+ if (isAttached) {
+ updateDispatcherFields()
+ }
+ }
+
+ override fun onAttach() {
+ assert(resolvedDispatcher.modifierLocalNode == null) {
+ "This dispatcher should only be used by a single Modifier.nestedScroll."
+ }
+ updateDispatcherFields()
+ }
+
+ override fun onDetach() {
+ resetDispatcherFields()
+ }
+
+ /**
+ * If the node changes (onAttach) or if the dispatcher changes (node.update). We'll need
+ * to reset the dispatcher properties accordingly.
+ */
+ private fun updateDispatcherFields() {
+ resolvedDispatcher.modifierLocalNode = this
+ resolvedDispatcher.calculateNestedScrollScope = { nestedCoroutineScope }
+ resolvedDispatcher.scope = coroutineScope
+ }
+
+ private fun resetDispatcherFields() {
+ resolvedDispatcher.modifierLocalNode = null
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
index d24885c..ea2846b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
@@ -20,7 +20,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D.Strategy
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
@@ -154,9 +153,13 @@
Impulse,
}
// Circular buffer; current sample at index.
- private val samples: Array<DataPointAtTime?> = Array(HistorySize) { null }
+ private val samples: Array<DataPointAtTime?> = arrayOfNulls(HistorySize)
private var index: Int = 0
+ // Reusable arrays to avoid allocation inside calculateVelocity.
+ private val reusableDataPointsArray = FloatArray(HistorySize)
+ private val reusableTimeArray = FloatArray(HistorySize)
+
/**
* Adds a data point for velocity calculation at a given time, [timeMillis]. The data ponit
* represents an amount of a change in position (for differential data points), or an absolute
@@ -181,8 +184,8 @@
* This can be expensive. Only call this when you need the velocity.
*/
fun calculateVelocity(): Float {
- val dataPoints: MutableList<Float> = mutableListOf()
- val time: MutableList<Float> = mutableListOf()
+ val dataPoints = reusableDataPointsArray
+ val time = reusableTimeArray
var sampleCount = 0
var index: Int = index
@@ -204,8 +207,8 @@
break
}
- dataPoints.add(sample.dataPoint)
- time.add(-age)
+ dataPoints[sampleCount] = sample.dataPoint
+ time[sampleCount] = -age
index = (if (index == 0) HistorySize else index) - 1
sampleCount += 1
@@ -213,12 +216,14 @@
if (sampleCount >= minSampleSize) {
// Choose computation logic based on strategy.
- // Multiply by "1000" to convert from units/ms to units/s
return when (strategy) {
- Strategy.Impulse ->
- calculateImpulseVelocity(dataPoints, time, isDataDifferential) * 1000
- Strategy.Lsq2 -> calculateLeastSquaresVelocity(dataPoints, time) * 1000
- }
+ Strategy.Impulse -> {
+ calculateImpulseVelocity(dataPoints, time, sampleCount, isDataDifferential)
+ }
+ Strategy.Lsq2 -> {
+ calculateLeastSquaresVelocity(dataPoints, time, sampleCount)
+ }
+ } * 1000 // Multiply by "1000" to convert from units/ms to units/s
}
// We're unable to make a velocity estimate but we did have at least one
@@ -239,12 +244,16 @@
* should be provided in reverse chronological order. The returned velocity is in "units/ms",
* where "units" is unit of the [dataPoints].
*/
- private fun calculateLeastSquaresVelocity(dataPoints: List<Float>, time: List<Float>): Float {
+ private fun calculateLeastSquaresVelocity(
+ dataPoints: FloatArray,
+ time: FloatArray,
+ sampleCount: Int
+ ): Float {
// The 2nd coefficient is the derivative of the quadratic polynomial at
// x = 0, and that happens to be the last timestamp that we end up
// passing to polyFitLeastSquares.
try {
- return polyFitLeastSquares(time, dataPoints, 2)[1]
+ return polyFitLeastSquares(time, dataPoints, sampleCount, 2)[1]
} catch (exception: IllegalArgumentException) {
return 0f
}
@@ -336,40 +345,36 @@
* Throws an IllegalArgumentException if:
* <ul>
* <li>[degree] is not a positive integer.
- * <li>[x] and [y] are not the same size.
- * <li>[x] or [y] are empty.
- * <li>(some other reason that
+ * <li>[sampleCount] is zero.
* </ul>
*
*/
internal fun polyFitLeastSquares(
/** The x-coordinates of each data point. */
- x: List<Float>,
+ x: FloatArray,
/** The y-coordinates of each data point. */
- y: List<Float>,
+ y: FloatArray,
+ /** number of items in each array */
+ sampleCount: Int,
degree: Int
-): List<Float> {
+): FloatArray {
if (degree < 1) {
throw IllegalArgumentException("The degree must be at positive integer")
}
- if (x.size != y.size) {
- throw IllegalArgumentException("x and y must be the same length")
- }
- if (x.isEmpty()) {
+ if (sampleCount == 0) {
throw IllegalArgumentException("At least one point must be provided")
}
val truncatedDegree =
- if (degree >= x.size) {
- x.size - 1
+ if (degree >= sampleCount) {
+ sampleCount - 1
} else {
degree
}
- val coefficients = MutableList(degree + 1) { 0.0f }
-
+ val coefficients = FloatArray(degree + 1)
// Shorthands for the purpose of notation equivalence to original C++ code.
- val m: Int = x.size
+ val m: Int = sampleCount
val n: Int = truncatedDegree + 1
// Expand the X vector to a matrix A, pre-multiplied by the weights.
@@ -513,15 +518,15 @@
* the boundary condition must be applied to the oldest sample to be accurate.
*/
private fun calculateImpulseVelocity(
- dataPoints: List<Float>,
- time: List<Float>,
+ dataPoints: FloatArray,
+ time: FloatArray,
+ sampleCount: Int,
isDataDifferential: Boolean
): Float {
- val numDataPoints = dataPoints.size
- if (numDataPoints < 2) {
+ if (sampleCount < 2) {
return 0f
}
- if (numDataPoints == 2) {
+ if (sampleCount == 2) {
if (time[0] == time[1]) {
return 0f
}
@@ -534,7 +539,7 @@
return dataPointsDelta / (time[0] - time[1])
}
var work = 0f
- for (i in (numDataPoints - 1) downTo 1) {
+ for (i in (sampleCount - 1) downTo 1) {
if (time[i] == time[i - 1]) {
continue
}
@@ -544,7 +549,7 @@
else dataPoints[i] - dataPoints[i - 1]
val vCurr = dataPointsDelta / (time[i] - time[i - 1])
work += (vCurr - vPrev) * abs(vCurr)
- if (i == (numDataPoints - 1)) {
+ if (i == (sampleCount - 1)) {
work = (work * 0.5f)
}
}
@@ -563,7 +568,7 @@
private class Vector(
val length: Int
) {
- val elements: Array<Float> = Array(length) { 0.0f }
+ val elements: FloatArray = FloatArray(length)
operator fun get(i: Int) = elements[i]
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifier.kt
deleted file mode 100644
index 005aa5b..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifier.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.compose.ui.layout
-
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntSize
-
-/**
- * IntermediateLayoutModifier is a [LayoutModifier] that will be skipped when
- * looking ahead. During measure pass, [measure] will be invoked with the constraints from the
- * look-ahead, as well as the target size.
- */
-@ExperimentalComposeUiApi
-internal interface IntermediateLayoutModifier : LayoutModifier {
- var targetSize: IntSize
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal class LookaheadIntermediateLayoutModifierImpl(
- val measureBlock: MeasureScope.(
- measurable: Measurable,
- constraints: Constraints,
- lookaheadSize: IntSize
- ) -> MeasureResult,
- inspectorInfo: InspectorInfo.() -> Unit
-) : IntermediateLayoutModifier, InspectorValueInfo(inspectorInfo) {
-
- override var targetSize: IntSize = IntSize.Zero
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult =
- measureBlock(measurable, constraints, targetSize)
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is LookaheadIntermediateLayoutModifierImpl) return false
-
- return measureBlock == other.measureBlock && targetSize == other.targetSize
- }
-
- override fun hashCode(): Int {
- return measureBlock.hashCode() * 31 + targetSize.hashCode()
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
new file mode 100644
index 0000000..27e0459
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
@@ -0,0 +1,330 @@
+/*
+ * 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.ui.layout
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.layout.Placeable.PlacementScope.Companion.place
+import androidx.compose.ui.layout.Placeable.PlacementScope.Companion.placeWithLayer
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.NodeMeasuringIntrinsics
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitAncestors
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * This establishes an internal IntermediateLayoutModifierNode. This node implicitly creates
+ * a [LookaheadScope], unless there is already a [LookaheadScope] in its ancestor. This allows
+ * lookahead to function "locally" without an explicit [LookaheadScope] defined.
+ *
+ * [coroutineScope] is a CoroutineScope that we provide to the IntermediateMeasureBlock for
+ * the intermediate changes to launch from. It is scoped to the lifecycle of the
+ * Modifier.intermediateLayout.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+internal class IntermediateLayoutModifierNode(
+ internal var measureBlock: IntermediateMeasureScope.(
+ measurable: Measurable,
+ constraints: Constraints,
+ ) -> MeasureResult
+) : LayoutModifierNode, Modifier.Node() {
+
+ // This is the union scope of LookaheadScope, CoroutineScope and MeasureScope that will be
+ // used as the receiver for user-provided measure block.
+ private val intermediateMeasureScope = IntermediateMeasureScopeImpl()
+
+ // If there's no lookahead scope in the ancestor, this is the lookahead scope that
+ // we'll provide to the intermediateLayout modifier
+ private val localLookaheadScope: LookaheadScopeImpl = LookaheadScopeImpl {
+ coordinator!!
+ }
+
+ /**
+ * Closest LookaheadScope in the ancestor. Defaults to local lookahead scope, and
+ * gets modified if there was already a parent scope.
+ */
+ private var closestLookaheadScope: LookaheadScope = localLookaheadScope
+
+ // TODO: This needs to be wired up with a user provided lambda to explicitly tell us when the
+ // intermediate changes are finished. The functionality to support this has been implemented,
+ // but the API change to get this lambda from devs has to be deferred until Modifier.Node
+ // delegate design is finished.
+ var isIntermediateChangeActive: Boolean = true
+
+ // Caches the lookahead constraints in order to snap to lookahead constraints in main pass
+ // when the intermediate changes are finished.
+ private var lookaheadConstraints: Constraints? = null
+
+ // Measurable & Placeable that serves as a middle layer between intermediateLayout logic and
+ // child measurable/placeable. This allows the middle layer to overwrite any changes in
+ // intermediateLayout when [IntermediateMeasureBlock#isIntermediateChangeActive] = false.
+ // This ensures a convergence between main pass and lookahead pass when there's no
+ // intermediate changes.
+ private var intermediateMeasurable: IntermediateMeasurablePlaceable? = null
+
+ override fun onAttach() {
+ val layoutNode = coordinator!!.layoutNode
+
+ val coordinates = coordinator!!.lookaheadDelegate?.lookaheadLayoutCoordinates
+ require(coordinates != null)
+
+ val closestParentLookaheadRoot = layoutNode.parent?.lookaheadRoot
+ closestLookaheadScope = if (closestParentLookaheadRoot?.hasExplicitLookaheadScope == true) {
+ // The closest explicit scope in the tree will be the closest scope, as all
+ // descendant intermediateLayoutModifiers will be using that as their LookaheadScope
+ LookaheadScopeImpl {
+ closestParentLookaheadRoot
+ .innerCoordinator
+ }
+ } else {
+ // If no explicit scope is ever defined, then fallback to implicitly created scopes
+ var ancestorNode: IntermediateLayoutModifierNode? = null
+ visitAncestors(Nodes.IntermediateMeasure) {
+ // Find the closest ancestor, and return
+ ancestorNode = it
+ return@visitAncestors
+ }
+ ancestorNode?.localLookaheadScope ?: localLookaheadScope
+ }
+ }
+
+ /**
+ * This gets call in the lookahead pass. Since intermediateLayout is designed to only make
+ * transient changes that don't affect lookahead, we simply pass through for the lookahead
+ * pass.
+ */
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult = measurable.measure(constraints).run {
+ layout(width, height) {
+ place(0, 0)
+ }
+ }
+
+ /**
+ * This gets called in the main pass to allow intermediate measurements & placements gradually
+ * converging to the lookahead results.
+ */
+ fun MeasureScope.intermediateMeasure(
+ measurable: Measurable,
+ constraints: Constraints,
+ lookaheadSize: IntSize,
+ lookaheadConstraints: Constraints,
+ ): MeasureResult {
+ intermediateMeasureScope.lookaheadSize = lookaheadSize
+ this@IntermediateLayoutModifierNode.lookaheadConstraints = lookaheadConstraints
+
+ return (intermediateMeasurable ?: IntermediateMeasurablePlaceable(measurable)).apply {
+ intermediateMeasurable = this
+ wrappedMeasurable = measurable
+ }.let { wrappedMeasurable ->
+ intermediateMeasureScope.measureBlock(wrappedMeasurable, constraints)
+ }
+ }
+
+ /**
+ * The function used to calculate minIntrinsicWidth for intermediate changes.
+ */
+ internal fun IntrinsicMeasureScope.minIntermediateIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = NodeMeasuringIntrinsics.minWidth(
+ { intrinsicMeasurable, constraints ->
+ intermediateMeasureScope.measureBlock(intrinsicMeasurable, constraints)
+ },
+ this,
+ measurable,
+ height
+ )
+
+ /**
+ * The function used to calculate minIntrinsicHeight for intermediate changes.
+ */
+ internal fun IntrinsicMeasureScope.minIntermediateIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = NodeMeasuringIntrinsics.minHeight(
+ { intrinsicMeasurable, constraints ->
+ intermediateMeasureScope.measureBlock(intrinsicMeasurable, constraints)
+ },
+ this,
+ measurable,
+ width
+ )
+
+ /**
+ * The function used to calculate maxIntrinsicWidth for intermediate changes.
+ */
+ internal fun IntrinsicMeasureScope.maxIntermediateIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = NodeMeasuringIntrinsics.maxWidth(
+ { intrinsicMeasurable, constraints ->
+ intermediateMeasureScope.measureBlock(intrinsicMeasurable, constraints)
+ },
+ this,
+ measurable,
+ height
+ )
+
+ /**
+ * The function used to calculate maxIntrinsicHeight for intermediate changes.
+ */
+ internal fun IntrinsicMeasureScope.maxIntermediateIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = NodeMeasuringIntrinsics.maxHeight(
+ { intrinsicMeasurable, constraints ->
+ intermediateMeasureScope.measureBlock(intrinsicMeasurable, constraints)
+ },
+ this,
+ measurable,
+ width
+ )
+
+ /**
+ * This class serves as a layer between measure and layout logic defined in the [measureBlock]
+ * and the child measurable (i.e. the next LayoutModifierNodeCoordinator). This class allows
+ * us to prevent any change in the [measureBlock] from impacting the child when there is _no_
+ * active changes in the given CoroutineScope.
+ */
+ private inner class IntermediateMeasurablePlaceable(
+ var wrappedMeasurable: Measurable
+ ) : Measurable, Placeable() {
+ var wrappedPlaceable: Placeable? = null
+ override fun measure(constraints: Constraints): Placeable {
+ wrappedPlaceable = if (isIntermediateChangeActive) {
+ wrappedMeasurable.measure(constraints).also {
+ measurementConstraints = constraints
+ measuredSize = IntSize(it.width, it.height)
+ }
+ } else {
+ // If the intermediate change isn't active, we'll measure with
+ // lookahead constraints and return lookahead size.
+ wrappedMeasurable.measure(lookaheadConstraints!!).also {
+ measurementConstraints = lookaheadConstraints!!
+ // isIntermediateChangeActive could change from false to true between
+ // measurement & returning measure results. Use case: animateContentSize
+ measuredSize = if (isIntermediateChangeActive) {
+ IntSize(it.width, it.height)
+ } else {
+ intermediateMeasureScope.lookaheadSize
+ }
+ }
+ }
+ return this
+ }
+
+ override fun placeAt(
+ position: IntOffset,
+ zIndex: Float,
+ layerBlock: (GraphicsLayerScope.() -> Unit)?
+ ) {
+ val offset =
+ if (isIntermediateChangeActive) position else IntOffset.Zero
+ layerBlock?.let {
+ wrappedPlaceable?.placeWithLayer(
+ offset,
+ zIndex,
+ it
+ )
+ } ?: wrappedPlaceable?.place(offset, zIndex)
+ }
+
+ override val parentData: Any?
+ get() = wrappedMeasurable.parentData
+
+ override fun get(alignmentLine: AlignmentLine): Int =
+ wrappedPlaceable!!.get(alignmentLine)
+
+ override fun minIntrinsicWidth(height: Int): Int =
+ wrappedMeasurable.minIntrinsicWidth(height)
+
+ override fun maxIntrinsicWidth(height: Int): Int =
+ wrappedMeasurable.maxIntrinsicWidth(height)
+
+ override fun minIntrinsicHeight(width: Int): Int =
+ wrappedMeasurable.minIntrinsicHeight(width)
+
+ override fun maxIntrinsicHeight(width: Int): Int =
+ wrappedMeasurable.maxIntrinsicHeight(width)
+ }
+
+ @ExperimentalComposeUiApi
+ private inner class IntermediateMeasureScopeImpl : IntermediateMeasureScope,
+ CoroutineScope {
+ override var lookaheadSize: IntSize = IntSize.Zero
+
+ override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates =
+ with(closestLookaheadScope) { this@toLookaheadCoordinates.toLookaheadCoordinates() }
+
+ override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
+ get() = with(closestLookaheadScope) {
+ this@lookaheadScopeCoordinates.lookaheadScopeCoordinates
+ }
+
+ @Suppress("DEPRECATION")
+ @Deprecated(
+ "onPlaced in LookaheadLayoutScope has been deprecated. It's replaced" +
+ " with reading LookaheadLayoutCoordinates directly during placement in" +
+ "IntermediateMeasureScope"
+ )
+ override fun Modifier.onPlaced(
+ onPlaced: (
+ lookaheadScopeCoordinates: LookaheadLayoutCoordinates,
+ layoutCoordinates: LookaheadLayoutCoordinates
+ ) -> Unit
+ ): Modifier = with(closestLookaheadScope) {
+ this@onPlaced.onPlaced(onPlaced)
+ }
+
+ override fun layout(
+ width: Int,
+ height: Int,
+ alignmentLines: Map<AlignmentLine, Int>,
+ placementBlock: Placeable.PlacementScope.() -> Unit
+ ) = object : MeasureResult {
+ override val width = width
+ override val height = height
+ override val alignmentLines = alignmentLines
+ override fun placeChildren() {
+ Placeable.PlacementScope.executeWithRtlMirroringValues(
+ width,
+ layoutDirection,
+ this@IntermediateLayoutModifierNode.coordinator,
+ placementBlock
+ )
+ }
+ }
+
+ override val layoutDirection: LayoutDirection
+ get() = coordinator!!.layoutDirection
+ override val density: Float
+ get() = coordinator!!.density
+ override val fontScale: Float
+ get() = coordinator!!.fontScale
+ override val coroutineContext: CoroutineContext
+ get() = coroutineScope.coroutineContext
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt
index 1483930..b3417ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt
@@ -19,74 +19,43 @@
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReusableComposeNode
-import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
-import androidx.compose.ui.composed
-import androidx.compose.ui.materialize
-import androidx.compose.ui.node.ComposeUiNode
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.NodeCoordinator
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
+import kotlinx.coroutines.CoroutineScope
-/**
- * [LookaheadLayout] is a Layout that runs a lookahead measure and placement pass to determine the
- * layout. Immediately afterwards, another measure and placement pass will begin, in which the
- * measurement and placement of any layout can be adjusted
- * based on the lookahead results via [LookaheadLayoutScope.intermediateLayout].
- *
- * During the lookahead pass, the layout adjustment logic defined in
- * [LookaheadLayoutScope.intermediateLayout] will be skipped, so that any transient morphing of
- * the layout is not taken into account when predetermining the target layout.
- *
- * Once the lookahead is finished, another measure & layout pass will begin.
- * [LookaheadLayoutScope.intermediateLayout] can be used to create an intermediate layout based on
- * incoming constraints and the lookahead results. This can result in layouts
- * that gradually change their sizes & positions towards the target layout calculated by the
- * lookahead.
- *
- * *Caveat:* [SubcomposeLayout] is not yet supported in [LookaheadLayout]. It will be supported in
- * an upcoming release.
- *
- * @sample androidx.compose.ui.samples.LookaheadLayoutSample
- *
- * @param content The children composable to be laid out.
- * @param modifier Modifiers to be applied to the layout.
- * @param measurePolicy The policy defining the measurement and positioning of the layout.
- */
@Suppress("ComposableLambdaParameterPosition")
+@Deprecated(
+ "LookaheadLayout has been replaced with LookaheadScope that does not require" +
+ " a Modifier or a MeasurePolicy.",
+ replaceWith = ReplaceWith(
+ "LookaheadScope { Layout(content = { content() }, \n" +
+ " modifier = modifier, measurePolicy = measurePolicy) }"
+ )
+)
@ExperimentalComposeUiApi
@UiComposable
@Composable
fun LookaheadLayout(
- content: @Composable @UiComposable LookaheadLayoutScope.() -> Unit,
+ content: @Composable @UiComposable LookaheadScope.() -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
- val localMap = currentComposer.currentCompositionLocalMap
- val materialized = currentComposer.materialize(modifier)
- val scope = remember { LookaheadLayoutScopeImpl() }
- ReusableComposeNode<LayoutNode, Applier<Any>>(
- factory = LayoutNode.Constructor,
- update = {
- set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
- set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
- set(scope) { scope ->
- scope.root = innerCoordinator
- }
- set(materialized, ComposeUiNode.SetModifier)
- init {
- isLookaheadRoot = true
- }
- },
- content = {
- scope.content()
- }
- )
+ LookaheadScope {
+ Layout(
+ content = { content() },
+ modifier = modifier,
+ measurePolicy = measurePolicy
+ )
+ }
}
/**
@@ -97,22 +66,195 @@
*
* @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
*/
+@Deprecated(
+ "LookaheadLayoutScope has been renamed to LookaheadScope",
+ ReplaceWith("LookaheadScope")
+)
@ExperimentalComposeUiApi
interface LookaheadLayoutScope {
-
+ @Deprecated(
+ "onPlaced in LookaheadLayoutScope has been deprecated. It's replaced" +
+ " with reading LookaheadLayoutCoordinates directly during placement in" +
+ " IntermediateMeasureScope. See example below."
+ )
/**
* [onPlaced] gets invoked after the parent [LayoutModifier] has been placed
* and before child [LayoutModifier] is placed. This allows child [LayoutModifier] to adjust
* its own placement based on its parent.
*
- * [onPlaced] callback will be invoked with the [LookaheadLayoutCoordinates] of the LayoutNode
- * emitted by [LookaheadLayout] as the first parameter, and the [LookaheadLayoutCoordinates] of
- * this modifier as the second parameter. Given the [LookaheadLayoutCoordinates]s, both
- * lookahead position and current position of this modifier in the [LookaheadLayout]'s
- * coordinates system can be calculated using
- * [LookaheadLayoutCoordinates.localLookaheadPositionOf] and
- * [LookaheadLayoutCoordinates.localPositionOf], respectively.
+ * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
*/
+ @Suppress("DEPRECATION")
+ fun Modifier.onPlaced(
+ onPlaced: (
+ lookaheadScopeCoordinates: LookaheadLayoutCoordinates,
+ layoutCoordinates: LookaheadLayoutCoordinates
+ ) -> Unit
+ ): Modifier
+}
+
+/**
+ * [LookaheadScope] starts a scope in which all layouts scope will receive a lookahead pass
+ * preceding the main measure/layout pass. This lookahead pass will calculate the layout
+ * size and position for all child layouts, and make the lookahead results available in
+ * [Modifier.intermediateLayout]. [Modifier.intermediateLayout] gets invoked in the main
+ * pass to allow transient layout changes in the main pass that gradually morph the layout
+ * over the course of multiple frames until it catches up with lookahead.
+ *
+ * *Caveat:* [SubcomposeLayout] is not yet supported in [LookaheadScope]. It will be supported in
+ * an upcoming release.
+ *
+ * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
+ *
+ * @param content The child composable to be laid out.
+ */
+@ExperimentalComposeUiApi
+@UiComposable
+@Composable
+fun LookaheadScope(content: @Composable @UiComposable LookaheadScope.() -> Unit) {
+ val scope = remember { LookaheadScopeImpl() }
+ ReusableComposeNode<LayoutNode, Applier<Any>>(
+ factory = { LayoutNode(isVirtual = true) },
+ update = {
+ init { isVirtualLookaheadRoot = true }
+ set(scope) { scope ->
+ // This internal lambda will be invoked during placement.
+ scope.scopeCoordinates = {
+ parent!!.innerCoordinator.coordinates
+ }
+ }
+ },
+ content = {
+ scope.content()
+ }
+ )
+}
+
+/**
+ * Creates an intermediate layout intended to help morph the layout from the current layout
+ * to the lookahead (i.e. pre-calculated future) layout.
+ *
+ * This modifier will be invoked _after_ lookahead pass and will have access to the lookahead
+ * results in [measure]. Therefore:
+ * 1) [intermediateLayout] measure/layout logic will not affect lookahead pass, but only be
+ * invoked during the main measure/layout pass,
+ * and 2) [measure] block can define intermediate changes that morphs the layout in the
+ * main pass gradually until it converges lookahead pass.
+ *
+ * @sample androidx.compose.ui.samples.IntermediateLayoutSample
+ */
+@ExperimentalComposeUiApi
+fun Modifier.intermediateLayout(
+ measure: IntermediateMeasureScope.(
+ measurable: Measurable,
+ constraints: Constraints,
+ ) -> MeasureResult,
+): Modifier = this then IntermediateLayoutElement(measure)
+
+@OptIn(ExperimentalComposeUiApi::class)
+private data class IntermediateLayoutElement(
+ val measure: IntermediateMeasureScope.(
+ measurable: Measurable,
+ constraints: Constraints,
+ ) -> MeasureResult,
+) : ModifierNodeElement<IntermediateLayoutModifierNode>() {
+ override fun create() = IntermediateLayoutModifierNode(measure)
+ override fun update(node: IntermediateLayoutModifierNode): IntermediateLayoutModifierNode =
+ node.apply { this.measureBlock = measure }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "intermediateLayout"
+ properties["measure"] = measure
+ }
+}
+
+/**
+ * [IntermediateMeasureScope] provides access to lookahead results to allow
+ * [intermediateLayout] to leverage lookahead results to define intermediate measurements
+ * and placements to gradually converge with lookahead.
+ *
+ * [IntermediateMeasureScope.lookaheadSize] provides the target size of the layout.
+ * [IntermediateMeasureScope] is also a [LookaheadScope], thus allowing layouts to
+ * read their [LookaheadLayoutCoordinates] during placement using
+ * [LookaheadScope.toLookaheadCoordinates], as well as the [LookaheadLayoutCoordinates] of the
+ * closest lookahead scope via [LookaheadScope.lookaheadScopeCoordinates].
+ * By knowing the target size and position, layout adjustments such as animations can be defined
+ * in [intermediateLayout] to morph the layout gradually in both size and position
+ * to arrive at its precalculated bounds.
+ *
+ * Note that [IntermediateMeasureScope] is the closest lookahead scope in the tree.
+ * This [LookaheadScope] enables convenient query of the layout's relative position to the
+ * [LookaheadScope]. Hence it becomes straightforward to animate position relative to the closest
+ * scope, which usually yields a natural looking animation, unless there are specific UX
+ * requirements to change position relative to a particular [LookaheadScope].
+ *
+ * [IntermediateMeasureScope] is a CoroutineScope, as a convenient scope for all the
+ * coroutine-based intermediate changes (e.g. animations) to be launched from.
+ */
+@ExperimentalComposeUiApi
+sealed interface IntermediateMeasureScope : LookaheadScope, CoroutineScope, MeasureScope {
+ /**
+ * Indicates the target size of the [intermediateLayout].
+ */
+ val lookaheadSize: IntSize
+}
+
+/**
+ * [LookaheadScope] provides a receiver scope for all (direct and indirect) child layouts in
+ * [LookaheadScope]. This receiver scope allows access to [lookaheadScopeCoordinates] from
+ * any child's [Placeable.PlacementScope]. It also allows any child to convert
+ * [LayoutCoordinates] (which can be retrieved in [Placeable.PlacementScope]) to
+ * [LookaheadLayoutCoordinates] using [toLookaheadCoordinates].
+ *
+ * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
+ */
+@ExperimentalComposeUiApi
+interface LookaheadScope {
+ /**
+ * Converts a [LayoutCoordinates] into a [LayoutCoordinates] in the Lookahead coordinates space.
+ * This is only applicable to child layouts within [LookaheadScope].
+ */
+ fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates
+
+ /**
+ * Returns the [LayoutCoordinates] of the [LookaheadScope]. This is
+ * only accessible from [Placeable.PlacementScope] (i.e. during placement time).
+ */
+ val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
+
+ /**
+ * Calculates the localPosition in the Lookahead coordinate space. This is a convenient
+ * method for 1) converting the given [LayoutCoordinates] to lookahead coordinates using
+ * [toLookaheadCoordinates], and 2) invoking [LayoutCoordinates.localPositionOf] with the
+ * converted coordinates.
+ */
+ fun LayoutCoordinates.localLookaheadPositionOf(coordinates: LayoutCoordinates) =
+ this.toLookaheadCoordinates().localPositionOf(
+ coordinates.toLookaheadCoordinates(),
+ Offset.Zero
+ )
+
+ @Suppress("DEPRECATION")
+ @Deprecated(
+ "onPlaced in LookaheadLayoutScope has been deprecated. It's replaced" +
+ " with reading LookaheadLayoutCoordinates directly during placement in" +
+ " IntermediateMeasureScope. See example below."
+ )
+ /**
+ * [onPlaced] gets invoked after the parent [LayoutModifier] has been placed
+ * and before child [LayoutModifier] is placed. This allows child [LayoutModifier] to adjust
+ * its own placement based on its parent.
+ *
+ * [onPlaced] callback will be invoked with the [LookaheadLayoutCoordinates] of the LayoutNode
+ * emitted by [LookaheadLayout] as the first parameter, and the [LookaheadLayoutCoordinates] of
+ * this modifier as the second parameter. Given the [LookaheadLayoutCoordinates]s, both
+ * lookahead position and current position of this modifier in the [LookaheadLayout]'s
+ * coordinates system can be calculated using
+ * [LookaheadLayoutCoordinates.localLookaheadPositionOf] and
+ * [LookaheadLayoutCoordinates.localPositionOf], respectively.
+ *
+ * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
+ */
fun Modifier.onPlaced(
onPlaced: (
lookaheadScopeCoordinates: LookaheadLayoutCoordinates,
@@ -120,73 +262,63 @@
) -> Unit
): Modifier
- /**
- * Creates an intermediate layout based on target size of the child layout calculated
- * in the lookahead. This allows the intermediate layout to morph the child layout
- * after lookahead through [measure], in which the size of the child layout calculated from the
- * lookahead is provided. [intermediateLayout] does _not_ participate in the lookahead. It is
- * only invoked for retroactively changing the layout based on the lookahead before the layout
- * is drawn.
- *
- * @sample androidx.compose.ui.samples.LookaheadLayoutSample
- */
+ @Deprecated(
+ "",
+ ReplaceWith(
+ "intermediateLayout { measurable, constraints ->" +
+ "measure.invoke(this, measurable, constraints, lookaheadSize)" +
+ "}"
+ )
+ )
+ /**
+ * Creates an intermediate layout based on target size of the child layout calculated
+ * in the lookahead. This allows the intermediate layout to morph the child layout
+ * after lookahead through [measure], in which the size of the child layout calculated from the
+ * lookahead is provided. [intermediateLayout] does _not_ participate in the lookahead. It is
+ * only invoked for retroactively changing the layout based on the lookahead before the layout
+ * is drawn.
+ *
+ * @sample androidx.compose.ui.samples.IntermediateLayoutSample
+ */
fun Modifier.intermediateLayout(
measure: MeasureScope.(
measurable: Measurable,
constraints: Constraints,
lookaheadSize: IntSize
) -> MeasureResult,
- ): Modifier
+ ): Modifier =
+ this.intermediateLayout { measurable: Measurable, constraints: Constraints ->
+ measure(measurable, constraints, lookaheadSize)
+ }
}
@OptIn(ExperimentalComposeUiApi::class)
-internal class LookaheadOnPlacedModifier(
- val callback: (
- lookaheadScopeRootCoordinates: LookaheadLayoutCoordinates,
- coordinates: LookaheadLayoutCoordinates
- ) -> Unit,
- val rootCoordinates: () -> LookaheadLayoutCoordinates,
-) : Modifier.Element {
-
- fun onPlaced(coordinates: LookaheadLayoutCoordinates) {
- callback(rootCoordinates(), coordinates)
+internal class LookaheadScopeImpl(
+ var scopeCoordinates: (() -> LayoutCoordinates)? = null
+) : LookaheadScope {
+ override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates {
+ return this as? LookaheadLayoutCoordinatesImpl
+ ?: (this as NodeCoordinator).lookaheadDelegate!!.lookaheadLayoutCoordinates
}
-}
-@OptIn(ExperimentalComposeUiApi::class)
-private class LookaheadLayoutScopeImpl : LookaheadLayoutScope {
- var root: NodeCoordinator? = null
+ override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
+ get() = scopeCoordinates!!()
+
+ @Suppress("DEPRECATION")
+ @Deprecated(
+ "onPlaced in LookaheadLayoutScope has been deprecated. It's replaced" +
+ " with reading LookaheadLayoutCoordinates directly during placement in" +
+ "IntermediateMeasureScope"
+ )
override fun Modifier.onPlaced(
onPlaced: (
lookaheadScopeCoordinates: LookaheadLayoutCoordinates,
layoutCoordinates: LookaheadLayoutCoordinates
) -> Unit
- ): Modifier = composed(
- debugInspectorInfo {
- name = "onPlaced"
- properties["onPlaced"] = onPlaced
- }
- ) {
- this.then(remember {
- LookaheadOnPlacedModifier(onPlaced) {
- root?.run { lookaheadDelegate!!.lookaheadLayoutCoordinates }
- ?: error("Lookahead root has not been set up yet")
- }
- })
- }
-
- override fun Modifier.intermediateLayout(
- measure: MeasureScope.(
- measurable: Measurable,
- constraints: Constraints,
- lookaheadSize: IntSize
- ) -> MeasureResult
- ): Modifier = this.then(
- LookaheadIntermediateLayoutModifierImpl(measure,
- debugInspectorInfo {
- name = "intermediateLayout"
- properties["measure"] = measure
- }
+ ): Modifier = this.onPlaced { coordinates ->
+ onPlaced(
+ scopeCoordinates!!().toLookaheadCoordinates() as LookaheadLayoutCoordinates,
+ coordinates.toLookaheadCoordinates() as LookaheadLayoutCoordinates
)
- )
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index e81434d..6f6694f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -22,8 +22,8 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.node.LookaheadDelegate
+import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.toOffset
@@ -32,76 +32,85 @@
* [LookaheadLayoutCoordinates] interface holds layout coordinates from both the lookahead
* calculation and the post-lookahead layout pass.
*/
+@Deprecated(
+ "LookaheadLayoutCoordinates class has been removed. localLookaheadPositionOf" +
+ "can be achieved in LookaheadScope using" +
+ " LayoutCoordinates.localLookaheadPositionOf(LayoutCoordinates) function.",
+ replaceWith = ReplaceWith("LayoutCoordinates")
+)
@ExperimentalComposeUiApi
-sealed interface LookaheadLayoutCoordinates : LayoutCoordinates {
- /**
- * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
- * [sourceCoordinates] may be any [LookaheadLayoutCoordinates] that belong to the same
- * compose layout hierarchy. Unlike [localPositionOf], [localLookaheadPositionOf] uses
- * the lookahead positions for coordinates calculation.
- */
- fun localLookaheadPositionOf(
- sourceCoordinates: LookaheadLayoutCoordinates,
- relativeToSource: Offset = Offset.Zero
- ): Offset
-}
+sealed interface LookaheadLayoutCoordinates : LayoutCoordinates
+@Suppress("DEPRECATION")
internal class LookaheadLayoutCoordinatesImpl(val lookaheadDelegate: LookaheadDelegate) :
LookaheadLayoutCoordinates {
val coordinator: NodeCoordinator
get() = lookaheadDelegate.coordinator
- override fun localLookaheadPositionOf(
- sourceCoordinates: LookaheadLayoutCoordinates,
- relativeToSource: Offset
- ): Offset {
- val source = (sourceCoordinates as LookaheadLayoutCoordinatesImpl).lookaheadDelegate
- val commonAncestor = coordinator.findCommonAncestor(source.coordinator)
-
- return commonAncestor.lookaheadDelegate?.let { ancestor ->
- // Common ancestor is in lookahead
- (source.positionIn(ancestor) + relativeToSource.round() -
- lookaheadDelegate.positionIn(ancestor)).toOffset()
- } ?: commonAncestor.let {
- // The two coordinates are in two separate LookaheadLayouts
- val sourceRoot = source.rootLookaheadDelegate
- val relativePosition = source.positionIn(sourceRoot) +
- sourceRoot.position + relativeToSource.round() -
- with(lookaheadDelegate) {
- (positionIn(rootLookaheadDelegate) + rootLookaheadDelegate.position)
- }
-
- lookaheadDelegate.rootLookaheadDelegate.coordinator.wrappedBy!!.localPositionOf(
- sourceRoot.coordinator.wrappedBy!!, relativePosition.toOffset()
- )
- }
- }
-
override val size: IntSize
- get() = coordinator.size
+ get() = lookaheadDelegate.let { IntSize(it.width, it.height) }
override val providedAlignmentLines: Set<AlignmentLine>
get() = coordinator.providedAlignmentLines
override val parentLayoutCoordinates: LayoutCoordinates?
- get() = coordinator.parentLayoutCoordinates
+ get() {
+ check(isAttached) { NodeCoordinator.ExpectAttachedLayoutCoordinates }
+ return coordinator.layoutNode.outerCoordinator.wrappedBy?.let {
+ it.lookaheadDelegate?.coordinates
+ }
+ }
override val parentCoordinates: LayoutCoordinates?
- get() = coordinator.parentCoordinates
+ get() {
+ check(isAttached) { NodeCoordinator.ExpectAttachedLayoutCoordinates }
+ return coordinator.wrappedBy?.lookaheadDelegate?.coordinates
+ }
+
override val isAttached: Boolean
get() = coordinator.isAttached
+ private val lookaheadOffset: Offset
+ get() = lookaheadDelegate.rootLookaheadDelegate.let {
+ localPositionOf(it.coordinates, Offset.Zero) -
+ coordinator.localPositionOf(it.coordinator, Offset.Zero)
+ }
override fun windowToLocal(relativeToWindow: Offset): Offset =
- coordinator.windowToLocal(relativeToWindow)
+ coordinator.windowToLocal(relativeToWindow) + lookaheadOffset
override fun localToWindow(relativeToLocal: Offset): Offset =
- coordinator.localToWindow(relativeToLocal)
+ coordinator.localToWindow(relativeToLocal + lookaheadOffset)
override fun localToRoot(relativeToLocal: Offset): Offset =
- coordinator.localToRoot(relativeToLocal)
+ coordinator.localToRoot(relativeToLocal + lookaheadOffset)
override fun localPositionOf(
sourceCoordinates: LayoutCoordinates,
relativeToSource: Offset
- ): Offset = coordinator.localPositionOf(sourceCoordinates, relativeToSource)
+ ): Offset {
+ if (sourceCoordinates is LookaheadLayoutCoordinatesImpl) {
+ val source = sourceCoordinates.lookaheadDelegate
+ val commonAncestor = coordinator.findCommonAncestor(source.coordinator)
+
+ return commonAncestor.lookaheadDelegate?.let { ancestor ->
+ // Common ancestor is in lookahead
+ (source.positionIn(ancestor) + relativeToSource.round() -
+ lookaheadDelegate.positionIn(ancestor)).toOffset()
+ } ?: commonAncestor.let {
+ // The two coordinates are in two separate LookaheadLayouts
+ val sourceRoot = source.rootLookaheadDelegate
+ val relativePosition = source.positionIn(sourceRoot) +
+ sourceRoot.position + relativeToSource.round() -
+ with(lookaheadDelegate) {
+ (positionIn(rootLookaheadDelegate) + rootLookaheadDelegate.position)
+ }
+
+ lookaheadDelegate.rootLookaheadDelegate.coordinator.wrappedBy!!.localPositionOf(
+ sourceRoot.coordinator.wrappedBy!!, relativePosition.toOffset()
+ )
+ }
+ } else {
+ return coordinator.localPositionOf(sourceCoordinates, relativeToSource)
+ }
+ }
override fun localBoundingBoxOf(
sourceCoordinates: LayoutCoordinates,
@@ -112,8 +121,14 @@
coordinator.transformFrom(sourceCoordinates, matrix)
}
- override fun get(alignmentLine: AlignmentLine): Int = coordinator.get(alignmentLine)
+ override fun get(alignmentLine: AlignmentLine): Int = lookaheadDelegate.get(alignmentLine)
}
private val LookaheadDelegate.rootLookaheadDelegate: LookaheadDelegate
- get() = lookaheadScope.root.outerCoordinator.lookaheadDelegate!!
+ get() {
+ var root = layoutNode.lookaheadRoot!!
+ while (root.parent?.lookaheadRoot != null) {
+ root = root.parent!!.lookaheadRoot!!
+ }
+ return root.outerCoordinator.lookaheadDelegate!!
+ }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt
deleted file mode 100644
index 646fa3c..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.layout
-
-import androidx.compose.runtime.snapshots.MutableSnapshot
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.node.LayoutNode
-
-/**
- * [LookaheadScope] manages a disposable snapshot. Lookahead measure pass runs in this
- * snapshot, which gets disposed after lookahead such that lookahead pass does not result
- * in any state changes to the global snapshot.
- */
-internal class LookaheadScope(val root: LayoutNode) {
-
- private var disposableSnapshot: MutableSnapshot? = null
-
- /**
- * This method runs the [block] in a snapshot that will be disposed. It is used
- * in the lookahead pass, where no state changes are intended to be applied to the global
- * snapshot.
- */
- fun <T> withDisposableSnapshot(block: () -> T): T {
- check(disposableSnapshot == null) {
- "Disposable snapshot is already active"
- }
- return Snapshot.takeMutableSnapshot().let {
- disposableSnapshot = it
- try {
- it.enter(block)
- } finally {
- it.dispose()
- disposableSnapshot = null
- }
- }
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index 65f3f8e..e106fb9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -37,13 +37,10 @@
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.layout.IntermediateLayoutModifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.LookaheadLayoutCoordinates
-import androidx.compose.ui.layout.LookaheadOnPlacedModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
@@ -79,7 +76,6 @@
@OptIn(ExperimentalComposeUiApi::class)
internal class BackwardsCompatNode(element: Modifier.Element) :
LayoutModifierNode,
- IntermediateLayoutModifierNode,
DrawModifierNode,
SemanticsModifierNode,
PointerInputModifierNode,
@@ -309,11 +305,6 @@
}
override val isValidOwnerScope: Boolean get() = isAttached
- override var targetSize: IntSize
- get() = (element as IntermediateLayoutModifier).targetSize
- set(value) {
- (element as IntermediateLayoutModifier).targetSize = value
- }
override fun MeasureScope.measure(
measurable: Measurable,
@@ -404,14 +395,6 @@
(element as OnGloballyPositionedModifier).onGloballyPositioned(coordinates)
}
- @OptIn(ExperimentalComposeUiApi::class)
- override fun onLookaheadPlaced(coordinates: LookaheadLayoutCoordinates) {
- val element = element
- if (element is LookaheadOnPlacedModifier) {
- element.onPlaced(coordinates)
- }
- }
-
override fun onRemeasured(size: IntSize) {
val element = element
if (element is OnRemeasuredModifier) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt
index 647b2dc..d87ed28 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.node
import androidx.compose.runtime.CompositionLocal
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
/**
@@ -32,7 +31,6 @@
* @see Modifier.Node
* @see CompositionLocal
*/
-@ExperimentalComposeUiApi
interface CompositionLocalConsumerModifierNode : DelegatableNode
/**
@@ -62,7 +60,6 @@
* before the node is [attached][Modifier.Node.onAttach] or after the node is
* [detached][Modifier.Node.onDetach].
*/
-@ExperimentalComposeUiApi
fun <T> CompositionLocalConsumerModifierNode.currentValueOf(local: CompositionLocal<T>): T {
check(node.isAttached) {
"Cannot read CompositionLocal because the Modifier node is not currently attached. Make " +
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index 25ff32b..b2e69f4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -25,7 +25,6 @@
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
@@ -39,14 +38,16 @@
return "<tail>"
}
}
+
init {
@OptIn(ExperimentalComposeUiApi::class)
tail.updateCoordinator(this)
}
- private inner class LookaheadDelegateImpl(
- scope: LookaheadScope
- ) : LookaheadDelegate(this, scope) {
+ override var lookaheadDelegate: LookaheadDelegate? =
+ if (layoutNode.lookaheadRoot != null) LookaheadDelegateImpl() else null
+
+ private inner class LookaheadDelegateImpl : LookaheadDelegate(this) {
// Lookahead measure
override fun measure(constraints: Constraints): Placeable =
@@ -89,8 +90,10 @@
layoutNode.intrinsicsPolicy.maxLookaheadIntrinsicHeight(width)
}
- override fun createLookaheadDelegate(scope: LookaheadScope): LookaheadDelegate {
- return LookaheadDelegateImpl(scope)
+ override fun ensureLookaheadDelegateCreated() {
+ if (lookaheadDelegate == null) {
+ lookaheadDelegate = LookaheadDelegateImpl()
+ }
}
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
index d165da8..9f6af91 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
@@ -16,11 +16,8 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.LookaheadLayout
-import androidx.compose.ui.layout.LookaheadLayoutCoordinates
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
@@ -46,18 +43,6 @@
fun onPlaced(coordinates: LayoutCoordinates) {}
/**
- * [onLookaheadPlaced] callback will be invoked with the [LookaheadLayoutCoordinates] of the
- * LayoutNode emitted by [LookaheadLayout] as the first parameter, and the
- * [LookaheadLayoutCoordinates] of this modifier as the second parameter. Given the
- * [LookaheadLayoutCoordinates]s, both lookahead position and current position of this
- * modifier in the [LookaheadLayout]'s coordinates system can be calculated using
- * [LookaheadLayoutCoordinates.localLookaheadPositionOf] and
- * [LookaheadLayoutCoordinates.localPositionOf], respectively.
- */
- @ExperimentalComposeUiApi
- fun onLookaheadPlaced(coordinates: LookaheadLayoutCoordinates) {}
-
- /**
* This method is called when the layout content is remeasured. The
* most common usage is [onSizeChanged].
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
index 1c39356..c1b1344 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
@@ -80,7 +80,9 @@
measurable: IntrinsicMeasurable,
height: Int
): Int = NodeMeasuringIntrinsics.minWidth(
- this@LayoutModifierNode,
+ { intrinsicMeasurable, constraints ->
+ measure(intrinsicMeasurable, constraints)
+ },
this,
measurable,
height
@@ -93,7 +95,9 @@
measurable: IntrinsicMeasurable,
width: Int
): Int = NodeMeasuringIntrinsics.minHeight(
- this@LayoutModifierNode,
+ { intrinsicMeasurable, constraints ->
+ measure(intrinsicMeasurable, constraints)
+ },
this,
measurable,
width
@@ -106,7 +110,9 @@
measurable: IntrinsicMeasurable,
height: Int
): Int = NodeMeasuringIntrinsics.maxWidth(
- this@LayoutModifierNode,
+ { intrinsicMeasurable, constraints ->
+ measure(intrinsicMeasurable, constraints)
+ },
this,
measurable,
height
@@ -119,7 +125,9 @@
measurable: IntrinsicMeasurable,
width: Int
): Int = NodeMeasuringIntrinsics.maxHeight(
- this@LayoutModifierNode,
+ { intrinsicMeasurable, constraints ->
+ measure(intrinsicMeasurable, constraints)
+ },
this,
measurable,
width
@@ -134,31 +142,27 @@
requireCoordinator(Nodes.Layout).invalidateLayer()
/**
- * This will invalidate the current node's layout pass, and ensure that relayout of this node will
- * happen for the next frame.
+ * This will invalidate the current node's placement result, and ensure that relayout
+ * (the placement block rerun) of this node will happen for the next frame .
*/
-fun LayoutModifierNode.invalidateLayout() = requireLayoutNode().requestRelayout()
+fun LayoutModifierNode.invalidatePlacement() = requireLayoutNode().requestRelayout()
/**
- * This invalidates the current node's measure result, and ensures that a remeasurement of this node
- * will happen for the next frame.
+ * This invalidates the current node's measure result, and ensures that a remeasurement
+ * (the measurement block rerun) of this node will happen for the next frame.
*/
fun LayoutModifierNode.invalidateMeasurements() = requireLayoutNode().invalidateMeasurements()
internal fun LayoutModifierNode.requestRemeasure() = requireLayoutNode().requestRemeasure()
-/**
- * IntermediateLayoutModifier is a [LayoutModifierNode] that will be skipped when
- * looking ahead. During measure pass, [measure] will be invoked with the constraints from the
- * look-ahead, as well as the target size.
- */
-interface IntermediateLayoutModifierNode : LayoutModifierNode {
- var targetSize: IntSize
-}
+internal object NodeMeasuringIntrinsics {
+ // Fun interface for measure block to avoid autoBoxing of Constraints
+ internal fun interface MeasureBlock {
+ fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult
+ }
-private object NodeMeasuringIntrinsics {
internal fun minWidth(
- node: LayoutModifierNode,
+ measureBlock: MeasureBlock,
instrinsicMeasureScope: IntrinsicMeasureScope,
intrinsicMeasurable: IntrinsicMeasurable,
h: Int
@@ -169,15 +173,19 @@
IntrinsicWidthHeight.Width
)
val constraints = Constraints(maxHeight = h)
- val layoutResult = with(node) {
- IntrinsicsMeasureScope(instrinsicMeasureScope, instrinsicMeasureScope.layoutDirection)
- .measure(measurable, constraints)
- }
+ val layoutResult =
+ with(measureBlock) {
+ IntrinsicsMeasureScope(
+ instrinsicMeasureScope,
+ instrinsicMeasureScope.layoutDirection
+ ).measure(measurable, constraints)
+ }
+
return layoutResult.width
}
internal fun minHeight(
- node: LayoutModifierNode,
+ measureBlock: MeasureBlock,
instrinsicMeasureScope: IntrinsicMeasureScope,
intrinsicMeasurable: IntrinsicMeasurable,
w: Int
@@ -188,7 +196,7 @@
IntrinsicWidthHeight.Height
)
val constraints = Constraints(maxWidth = w)
- val layoutResult = with(node) {
+ val layoutResult = with(measureBlock) {
IntrinsicsMeasureScope(instrinsicMeasureScope, instrinsicMeasureScope.layoutDirection)
.measure(measurable, constraints)
}
@@ -196,7 +204,7 @@
}
internal fun maxWidth(
- node: LayoutModifierNode,
+ measureBlock: MeasureBlock,
instrinsicMeasureScope: IntrinsicMeasureScope,
intrinsicMeasurable: IntrinsicMeasurable,
h: Int
@@ -207,7 +215,7 @@
IntrinsicWidthHeight.Width
)
val constraints = Constraints(maxHeight = h)
- val layoutResult = with(node) {
+ val layoutResult = with(measureBlock) {
IntrinsicsMeasureScope(instrinsicMeasureScope, instrinsicMeasureScope.layoutDirection)
.measure(measurable, constraints)
}
@@ -215,7 +223,7 @@
}
internal fun maxHeight(
- node: LayoutModifierNode,
+ measureBlock: MeasureBlock,
instrinsicMeasureScope: IntrinsicMeasureScope,
intrinsicMeasurable: IntrinsicMeasurable,
w: Int
@@ -226,7 +234,7 @@
IntrinsicWidthHeight.Height
)
val constraints = Constraints(maxWidth = w)
- val layoutResult = with(node) {
+ val layoutResult = with(measureBlock) {
IntrinsicsMeasureScope(instrinsicMeasureScope, instrinsicMeasureScope.layoutDirection)
.measure(measurable, constraints)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
index 43d50b6..462d133 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
@@ -25,10 +25,8 @@
import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.HorizontalAlignmentLine
-import androidx.compose.ui.layout.IntermediateLayoutModifier
+import androidx.compose.ui.layout.IntermediateLayoutModifierNode
import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
@@ -47,22 +45,21 @@
val wrappedNonNull: NodeCoordinator get() = wrapped!!
- private var lookAheadTransientMeasureNode: IntermediateLayoutModifierNode? = measureNode.run {
- if (node.isKind(Nodes.IntermediateMeasure) && this is IntermediateLayoutModifierNode) this
- else null
- }
+ private var lookaheadConstraints: Constraints? = null
+
+ override var lookaheadDelegate: LookaheadDelegate? =
+ if (layoutNode.lookaheadRoot != null) LookaheadDelegateForLayoutModifierNode() else null
/**
* LookaheadDelegate impl for when the modifier is any [LayoutModifier] except
- * [IntermediateLayoutModifier]. This impl will invoke [LayoutModifier.measure] for
+ * IntermediateLayoutModifier. This impl will invoke [LayoutModifier.measure] for
* the lookahead measurement.
*/
- private inner class LookaheadDelegateForLayoutModifierNode(
- scope: LookaheadScope
- ) : LookaheadDelegate(this, scope) {
+ private inner class LookaheadDelegateForLayoutModifierNode : LookaheadDelegate(this) {
// LookaheadMeasure
override fun measure(constraints: Constraints): Placeable =
performingMeasure(constraints) {
+ lookaheadConstraints = constraints
with(layoutModifierNode) {
measure(
// This allows `measure` calls in the modifier to be redirected to
@@ -99,60 +96,25 @@
}
}
- /**
- * LookaheadDelegate impl for when the [layoutModifierNode] is an
- * [IntermediateLayoutModifierNode]. This impl will redirect the measure call to the next
- * lookahead delegate in the chain, without invoking the measure lambda defined in the modifier.
- * This is necessary because [IntermediateLayoutModifierNode] does not participate in lookahead.
- */
- private inner class LookaheadDelegateForIntermediateLayoutModifier(
- scope: LookaheadScope,
- val intermediateMeasureNode: IntermediateLayoutModifierNode
- ) : LookaheadDelegate(this, scope) {
- private inner class PassThroughMeasureResult : MeasureResult {
- override val width: Int
- get() = wrappedNonNull.lookaheadDelegate!!.measureResult.width
- override val height: Int
- get() = wrappedNonNull.lookaheadDelegate!!.measureResult.height
- override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
-
- override fun placeChildren() {
- with(PlacementScope) {
- wrappedNonNull.lookaheadDelegate!!.place(0, 0)
- }
- }
+ override fun ensureLookaheadDelegateCreated() {
+ if (lookaheadDelegate == null) {
+ lookaheadDelegate = LookaheadDelegateForLayoutModifierNode()
}
- private val passThroughMeasureResult = PassThroughMeasureResult()
-
- // LookaheadMeasure
- override fun measure(constraints: Constraints): Placeable =
- with(intermediateMeasureNode) {
- performingMeasure(constraints) {
- wrappedNonNull.lookaheadDelegate!!.run {
- measure(constraints)
- targetSize = IntSize(measureResult.width, measureResult.height)
- }
- passThroughMeasureResult
- }
- }
-
- override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
- return calculateAlignmentAndPlaceChildAsNeeded(alignmentLine).also {
- cachedAlignmentLinesMap[alignmentLine] = it
- }
- }
- }
-
- override fun createLookaheadDelegate(scope: LookaheadScope): LookaheadDelegate {
- return lookAheadTransientMeasureNode?.let {
- LookaheadDelegateForIntermediateLayoutModifier(scope, it)
- } ?: LookaheadDelegateForLayoutModifierNode(scope)
}
override fun measure(constraints: Constraints): Placeable {
performingMeasure(constraints) {
with(layoutModifierNode) {
- measureResult = measure(wrappedNonNull, constraints)
+ measureResult = if (this is IntermediateLayoutModifierNode) {
+ intermediateMeasure(
+ wrappedNonNull,
+ constraints,
+ lookaheadDelegate!!.measureResult.let { IntSize(it.width, it.height) },
+ lookaheadConstraints!!
+ )
+ } else {
+ measure(wrappedNonNull, constraints)
+ }
this@LayoutModifierNodeCoordinator
}
}
@@ -161,23 +123,31 @@
}
override fun minIntrinsicWidth(height: Int): Int {
- return with(layoutModifierNode) {
+ return (layoutModifierNode as? IntermediateLayoutModifierNode)?.run {
+ minIntermediateIntrinsicWidth(wrappedNonNull, height)
+ } ?: with(layoutModifierNode) {
minIntrinsicWidth(wrappedNonNull, height)
}
}
override fun maxIntrinsicWidth(height: Int): Int =
- with(layoutModifierNode) {
+ (layoutModifierNode as? IntermediateLayoutModifierNode)?.run {
+ maxIntermediateIntrinsicWidth(wrappedNonNull, height)
+ } ?: with(layoutModifierNode) {
maxIntrinsicWidth(wrappedNonNull, height)
}
override fun minIntrinsicHeight(width: Int): Int =
- with(layoutModifierNode) {
+ (layoutModifierNode as? IntermediateLayoutModifierNode)?.run {
+ minIntermediateIntrinsicHeight(wrappedNonNull, width)
+ } ?: with(layoutModifierNode) {
minIntrinsicHeight(wrappedNonNull, width)
}
override fun maxIntrinsicHeight(width: Int): Int =
- with(layoutModifierNode) {
+ (layoutModifierNode as? IntermediateLayoutModifierNode)?.run {
+ maxIntermediateIntrinsicHeight(wrappedNonNull, width)
+ } ?: with(layoutModifierNode) {
maxIntrinsicHeight(wrappedNonNull, width)
}
@@ -203,30 +173,6 @@
}
}
- override fun onLayoutModifierNodeChanged() {
- super.onLayoutModifierNodeChanged()
- layoutModifierNode.let { node ->
- // Creates different [LookaheadDelegate]s based on the type of the modifier.
- if (node.node.isKind(Nodes.IntermediateMeasure) &&
- node is IntermediateLayoutModifierNode
- ) {
- lookAheadTransientMeasureNode = node
- lookaheadDelegate?.let {
- updateLookaheadDelegate(
- LookaheadDelegateForIntermediateLayoutModifier(it.lookaheadScope, node)
- )
- }
- } else {
- lookAheadTransientMeasureNode = null
- lookaheadDelegate?.let {
- updateLookaheadDelegate(
- LookaheadDelegateForLayoutModifierNode(it.lookaheadScope)
- )
- }
- }
- }
- }
-
override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
return lookaheadDelegate?.getCachedAlignmentLine(alignmentLine)
?: calculateAlignmentAndPlaceChildAsNeeded(alignmentLine)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index f2a7b8e..6ca357e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -32,7 +32,6 @@
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LayoutInfo
import androidx.compose.ui.layout.LayoutNodeSubcompositionsState
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureScope
@@ -90,6 +89,59 @@
InteroperableComposeUiNode,
Owner.OnLayoutCompletedListener {
+ internal var isVirtualLookaheadRoot: Boolean = false
+
+ // Indicates whether there's an explicit lookahead scope defined on any virtual child.
+ internal val hasExplicitLookaheadScope: Boolean
+ get() = virtualLookaheadChildren?.isNotEmpty() ?: false
+
+ // Indicates whether there's any IntermediateLayoutModifierNode on this node. This gets updated
+ // when an IntermediateLayoutModifierNodes gets attached or detached.
+ internal var hasLocalLookahead: Boolean = false
+ set(value) {
+ field = value
+ if (isAttached) {
+ updateLookaheadRoot()
+ }
+ }
+
+ // Update the lookaheadRoot to reference the LayoutNode itself if it contains modifiers that
+ // require local lookahead, or if it has a virtual child created for an explicit lookahead
+ // scope. If neither of the two conditions are met, then we fallback to use parent's
+ // lookahead root.
+ private fun updateLookaheadRoot() {
+ val newRoot =
+ if (hasExplicitLookaheadScope || (parent?.lookaheadRoot == null && hasLocalLookahead)) {
+ this
+ } else {
+ parent?.lookaheadRoot
+ }
+ lookaheadRoot = newRoot
+ }
+
+ /**
+ * This lookaheadRoot references the closest root to the LayoutNode, not the top-level
+ * lookahead root.
+ */
+ internal var lookaheadRoot: LayoutNode? = null
+ private set(newRoot) {
+ if (newRoot != field) {
+ field = newRoot
+ if (newRoot != null) {
+ layoutDelegate.ensureLookaheadDelegateCreated()
+ forEachCoordinatorIncludingInner {
+ it.ensureLookaheadDelegateCreated()
+ }
+ }
+ if (isAttached) {
+ updateSubtreeLookaheadRoot()
+ }
+ invalidateMeasurements()
+ }
+ }
+
+ private var virtualLookaheadChildren: MutableVector<LayoutNode>? = null
+
val isPlacedInLookahead: Boolean?
get() = lookaheadPassDelegate?.isPlaced
@@ -137,7 +189,8 @@
unfoldedVirtualChildrenListDirty = true
}
if (isVirtual) {
- this.parent?.unfoldedVirtualChildrenListDirty = true
+ // Invalidate all virtual unfolded parent until we reach a non-virtual one
+ this._foldedParent?.invalidateUnfoldedVirtualChildren()
}
}
@@ -184,7 +237,11 @@
*/
internal val parent: LayoutNode?
get() {
- return if (_foldedParent?.isVirtual == true) _foldedParent?.parent else _foldedParent
+ var parent = _foldedParent
+ while (parent?.isVirtual == true) {
+ parent = parent._foldedParent
+ }
+ return parent
}
/**
@@ -267,24 +324,10 @@
onZSortedChildrenInvalidated()
if (instance.isVirtual) {
- require(!isVirtual) { "Virtual LayoutNode can't be added into a virtual parent" }
virtualChildrenCount++
}
invalidateUnfoldedVirtualChildren()
- instance.outerCoordinator.wrappedBy = if (isVirtual) {
- // if this node is virtual we use the inner coordinator of our parent
- _foldedParent?.innerCoordinator
- } else {
- innerCoordinator
- }
- // and if the child is virtual we set our inner coordinator for the grandchildren
- if (instance.isVirtual) {
- instance._foldedChildren.forEach {
- it.outerCoordinator.wrappedBy = innerCoordinator
- }
- }
-
val owner = this.owner
if (owner != null) {
instance.attach(owner)
@@ -403,6 +446,9 @@
isPlaced = true
}
+ // Use the inner coordinator of first non-virtual parent
+ outerCoordinator.wrappedBy = parent?.innerCoordinator
+
this.owner = owner
this.depth = (parent?.depth ?: -1) + 1
@OptIn(ExperimentalComposeUiApi::class)
@@ -410,11 +456,13 @@
owner.onSemanticsChange()
}
owner.onAttach(this)
- // Update lookahead scope when attached. For nested cases, we'll always use the
- // lookahead scope from the out-most LookaheadRoot.
- mLookaheadScope =
- parent?.mLookaheadScope ?: if (isLookaheadRoot) LookaheadScope(this) else null
+ if (isVirtualLookaheadRoot) {
+ parent!!.addVirtualLookaheadChild(this)
+ }
+ // Update lookahead root when attached. For nested cases, we'll always use the
+ // closest lookahead root
+ updateLookaheadRoot()
nodes.attach(performInvalidations = false)
_foldedChildren.forEach { child ->
child.attach(owner)
@@ -456,6 +504,12 @@
nodes.detach()
owner.onDetach(this)
this.owner = null
+
+ if (isVirtualLookaheadRoot) {
+ parent!!.removeVirtualLookaheadChild(this)
+ }
+ hasLocalLookahead = false
+ lookaheadRoot = null
depth = 0
_foldedChildren.forEach { child ->
child.detach()
@@ -465,6 +519,32 @@
isPlaced = false
}
+ private fun addVirtualLookaheadChild(layoutNode: LayoutNode) {
+ virtualLookaheadChildren?.add(layoutNode) ?: run {
+ virtualLookaheadChildren = mutableVectorOf(this)
+ }
+ updateLookaheadRoot()
+ }
+
+ private fun removeVirtualLookaheadChild(layoutNode: LayoutNode) {
+ virtualLookaheadChildren?.remove(layoutNode)
+ updateLookaheadRoot()
+ }
+
+ // Update subtree (excluding self)'s lookahead root. If the lookahead root is null, the subtree
+ // may propagate local lookahead roots down the tree.
+ private fun updateSubtreeLookaheadRoot() {
+ forEachChild {
+ if (it.isAttached) {
+ val oldRoot = it.lookaheadRoot
+ it.updateLookaheadRoot()
+ if (oldRoot != it.lookaheadRoot) {
+ it.updateSubtreeLookaheadRoot()
+ }
+ }
+ }
+ }
+
private val _zSortedChildren = mutableVectorOf<LayoutNode>()
private var zSortedChildrenInvalidated = true
@@ -584,17 +664,6 @@
}
}
- internal var mLookaheadScope: LookaheadScope? = null
- private set(newScope) {
- if (newScope != field) {
- field = newScope
- layoutDelegate.onLookaheadScopeChanged(newScope)
- forEachCoordinatorIncludingInner { coordinator ->
- coordinator.updateLookaheadScope(newScope)
- }
- }
- }
-
/**
* The layout direction of the layout node.
*/
@@ -703,18 +772,6 @@
@Deprecated("Temporary API to support ConstraintLayout prototyping.")
internal var canMultiMeasure: Boolean = false
- var isLookaheadRoot: Boolean = false
- set(value) {
- if (value != field) {
- if (!value) {
- mLookaheadScope = null
- } else {
- mLookaheadScope = LookaheadScope(this)
- }
- field = value
- }
- }
-
internal val nodes = NodeChain(this)
internal val innerCoordinator: NodeCoordinator
get() = nodes.innerCoordinator
@@ -779,6 +836,7 @@
/**
* The [Modifier] currently applied to this node.
*/
+ @OptIn(ExperimentalComposeUiApi::class)
override var modifier: Modifier = Modifier
set(value) {
require(!isVirtual || modifier === Modifier) {
@@ -786,14 +844,10 @@
}
field = value
nodes.updateFrom(value)
-
- // TODO(lmr): we don't need to do this every time and should attempt to avoid it
- // whenever possible!
- forEachCoordinatorIncludingInner {
- it.updateLookaheadScope(mLookaheadScope)
- }
-
layoutDelegate.updateParentData()
+ if (nodes.has(Nodes.IntermediateMeasure)) {
+ hasLocalLookahead = true
+ }
}
private fun resetModifierState() {
@@ -1045,6 +1099,7 @@
// no extra work required and node is ready to be displayed
}
}
+
else -> throw IllegalStateException("Unexpected state ${it.layoutState}")
}
}
@@ -1074,7 +1129,7 @@
* measure and layout from the owner.
*/
internal fun requestLookaheadRemeasure(forceRequest: Boolean = false) {
- check(mLookaheadScope != null) {
+ check(lookaheadRoot != null) {
"Lookahead measure cannot be requested on a node that is not a part of the" +
"LookaheadLayout"
}
@@ -1090,7 +1145,7 @@
* measurement need to be re-done. Such events include modifier change, attach/detach, etc.
*/
internal fun invalidateMeasurements() {
- if (mLookaheadScope != null) {
+ if (lookaheadRoot != null) {
requestLookaheadRemeasure()
} else {
requestRemeasure()
@@ -1180,7 +1235,7 @@
): Boolean {
// Only lookahead remeasure when the constraints are valid and the node is in
// a LookaheadLayout (by checking whether the lookaheadScope is set)
- return if (constraints != null && mLookaheadScope != null) {
+ return if (constraints != null && lookaheadRoot != null) {
lookaheadPassDelegate!!.remeasure(constraints)
} else {
false
@@ -1515,4 +1570,4 @@
*/
internal fun LayoutNode.add(child: LayoutNode) {
insertAt(children.size, child)
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 2c4d5cb..64a3e9b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -19,7 +19,6 @@
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.LayoutNode.LayoutState
@@ -645,9 +644,7 @@
* [LookaheadPassDelegate] manages the measure/layout and alignmentLine related queries for
* the lookahead pass.
*/
- inner class LookaheadPassDelegate(
- private val lookaheadScope: LookaheadScope,
- ) : Placeable(), Measurable, AlignmentLinesOwner {
+ inner class LookaheadPassDelegate : Placeable(), Measurable, AlignmentLinesOwner {
internal var duringAlignmentLinesQuery: Boolean = false
private var placedOnce: Boolean = false
@@ -657,9 +654,7 @@
private var lookaheadConstraints: Constraints? = null
private var lastPosition: IntOffset = IntOffset.Zero
- // isPlaced is set to true when created because the construction of LookaheadPassDelegate
- // is triggered by [LayoutNode.attach]
- override var isPlaced: Boolean = true
+ override var isPlaced: Boolean = false
private var isPreviouslyPlaced: Boolean = false
override val innerCoordinator: NodeCoordinator
get() = layoutNode.innerCoordinator
@@ -987,9 +982,17 @@
}
when (intrinsicsUsageByParent) {
LayoutNode.UsageByParent.InMeasureBlock ->
- intrinsicsUsingParent.requestLookaheadRemeasure(forceRequest)
+ if (intrinsicsUsingParent.lookaheadRoot != null) {
+ intrinsicsUsingParent.requestLookaheadRemeasure(forceRequest)
+ } else {
+ intrinsicsUsingParent.requestRemeasure(forceRequest)
+ }
LayoutNode.UsageByParent.InLayoutBlock ->
- intrinsicsUsingParent.requestLookaheadRelayout(forceRequest)
+ if (intrinsicsUsingParent.lookaheadRoot != null) {
+ intrinsicsUsingParent.requestLookaheadRelayout(forceRequest)
+ } else {
+ intrinsicsUsingParent.requestRelayout(forceRequest)
+ }
else -> error("Intrinsics isn't used by the parent")
}
}
@@ -1071,7 +1074,7 @@
* has a lookahead root.
*/
private fun LayoutNode.isOutMostLookaheadRoot(): Boolean =
- mLookaheadScope?.root == this
+ lookaheadRoot != null && parent?.lookaheadRoot == null
/**
* Performs measure with the given constraints and perform necessary state mutations before
@@ -1119,9 +1122,9 @@
layoutState = LayoutState.Idle
}
- internal fun onLookaheadScopeChanged(newScope: LookaheadScope?) {
- lookaheadPassDelegate = newScope?.let {
- LookaheadPassDelegate(it)
+ internal fun ensureLookaheadDelegateCreated() {
+ if (lookaheadPassDelegate == null) {
+ lookaheadPassDelegate = LookaheadPassDelegate()
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
index c15db28..eca8ea0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
@@ -90,7 +90,7 @@
return relayoutNodes.contains(this) ||
parent?.lookaheadMeasurePending == true ||
parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
- (parent?.measurePending == true && mLookaheadScope!!.root == this)
+ (parent?.measurePending == true && lookaheadRoot == this)
}
if (lookaheadLayoutPending) {
return relayoutNodes.contains(this) ||
@@ -99,7 +99,7 @@
parent.lookaheadLayoutPending ||
parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
parentLayoutState == LayoutNode.LayoutState.LookaheadLayingOut ||
- (parent.layoutPending && mLookaheadScope!!.root == this)
+ (parent.layoutPending && lookaheadRoot == this)
}
}
return true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index a0b1a02..b34b523 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -23,7 +23,6 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.VerticalAlignmentLine
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
@@ -81,7 +80,6 @@
internal abstract class LookaheadDelegate(
val coordinator: NodeCoordinator,
- val lookaheadScope: LookaheadScope
) : Measurable, LookaheadCapablePlaceable() {
override val child: LookaheadCapablePlaceable?
get() = coordinator.wrapped?.lookaheadDelegate
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 4dc5466..feb4ffd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -20,9 +20,9 @@
import androidx.compose.ui.layout.OnGloballyPositionedModifier
import androidx.compose.ui.node.LayoutNode.LayoutState.Idle
import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut
-import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadLayingOut
import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadMeasuring
+import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
import androidx.compose.ui.node.LayoutNode.UsageByParent.InLayoutBlock
import androidx.compose.ui.node.LayoutNode.UsageByParent.InMeasureBlock
import androidx.compose.ui.unit.Constraints
@@ -112,14 +112,14 @@
* Requests lookahead remeasure for this [layoutNode] and nodes affected by its measure result
*
* Note: This should only be called on a [LayoutNode] in the subtree defined in a
- * LookaheadLayout. The caller is responsible for checking with [LayoutNode.mLookaheadScope]
+ * LookaheadLayout. The caller is responsible for checking with [LayoutNode.lookaheadRoot]
* is valid (i.e. non-null) before calling this method.
*
* @return true if the [measureAndLayout] execution should be scheduled as a result
* of the request.
*/
fun requestLookaheadRemeasure(layoutNode: LayoutNode, forced: Boolean = false): Boolean {
- check(layoutNode.mLookaheadScope != null) {
+ check(layoutNode.lookaheadRoot != null) {
"Error: requestLookaheadRemeasure cannot be called on a node outside" +
" LookaheadLayout"
}
@@ -282,7 +282,7 @@
* @return true if the [LayoutNode] size has been changed.
*/
private fun doLookaheadRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean {
- if (layoutNode.mLookaheadScope == null) return false
+ if (layoutNode.lookaheadRoot == null) return false
val lookaheadSizeChanged = if (constraints != null) {
layoutNode.lookaheadRemeasure(constraints)
} else {
@@ -291,7 +291,7 @@
val parent = layoutNode.parent
if (lookaheadSizeChanged && parent != null) {
- if (parent.mLookaheadScope == null) {
+ if (parent.lookaheadRoot == null) {
requestRemeasure(parent)
} else if (layoutNode.measuredByParentInLookahead == InMeasureBlock) {
requestLookaheadRemeasure(parent)
@@ -548,7 +548,7 @@
get() = measurePending && measureAffectsParent
private val LayoutNode.canAffectParentInLookahead
- get() = lookaheadLayoutPending &&
+ get() = lookaheadMeasurePending &&
(measuredByParentInLookahead == InMeasureBlock ||
layoutDelegate.lookaheadAlignmentLinesOwner?.alignmentLines?.required == true)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index a01e089..e6eeda9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -241,7 +241,7 @@
var node: Modifier.Node? = tail.parent
while (node != null) {
if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
- val next = if (node.isAttached) {
+ val next = if (node.coordinator != null) {
val c = node.coordinator as LayoutModifierNodeCoordinator
val prevNode = c.layoutModifierNode
c.layoutModifierNode = node
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 19ff440..9bdb4f2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -36,7 +36,6 @@
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadLayoutCoordinatesImpl
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
@@ -187,31 +186,12 @@
}
}
- internal var lookaheadDelegate: LookaheadDelegate? = null
- private set
+ abstract var lookaheadDelegate: LookaheadDelegate?
+ protected set
private var oldAlignmentLines: MutableMap<AlignmentLine, Int>? = null
- /**
- * Creates a new lookaheadDelegate instance when the scope changes. If the provided scope is
- * null, it means the lookahead root does not exit (or no longer exists), set
- * the [lookaheadDelegate] to null.
- */
- internal fun updateLookaheadScope(scope: LookaheadScope?) {
- lookaheadDelegate = scope?.let {
- if (it != lookaheadDelegate?.lookaheadScope) {
- createLookaheadDelegate(it)
- } else {
- lookaheadDelegate
- }
- }
- }
-
- protected fun updateLookaheadDelegate(lookaheadDelegate: LookaheadDelegate) {
- this.lookaheadDelegate = lookaheadDelegate
- }
-
- abstract fun createLookaheadDelegate(scope: LookaheadScope): LookaheadDelegate
+ abstract fun ensureLookaheadDelegateCreated()
override val providedAlignmentLines: Set<AlignmentLine>
get() {
@@ -262,10 +242,10 @@
if (layoutNode.nodes.has(Nodes.ParentData)) {
with(layoutNode.density) {
layoutNode.nodes.tailToHead {
- if (it === thisNode) return@tailToHead
if (it.isKind(Nodes.ParentData) && it is ParentDataModifierNode) {
data = with(it) { modifyParentData(data) }
}
+ if (it === thisNode) return@tailToHead
}
}
}
@@ -377,12 +357,6 @@
@OptIn(ExperimentalComposeUiApi::class)
fun onPlaced() {
- val lookahead = lookaheadDelegate
- if (lookahead != null) {
- visitNodes(Nodes.LayoutAware) {
- it.onLookaheadPlaced(lookahead.lookaheadLayoutCoordinates)
- }
- }
visitNodes(Nodes.LayoutAware) {
it.onPlaced(this)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 4785506..04a697e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -30,9 +30,8 @@
import androidx.compose.ui.input.key.KeyInputModifierNode
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.input.rotary.RotaryInputModifierNode
-import androidx.compose.ui.layout.IntermediateLayoutModifier
+import androidx.compose.ui.layout.IntermediateLayoutModifierNode
import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.LookaheadOnPlacedModifier
import androidx.compose.ui.layout.OnGloballyPositionedModifier
import androidx.compose.ui.layout.OnPlacedModifier
import androidx.compose.ui.layout.OnRemeasuredModifier
@@ -105,9 +104,6 @@
if (element is LayoutModifier) {
mask = mask or Nodes.Layout
}
- if (element is IntermediateLayoutModifier) {
- mask = mask or Nodes.IntermediateMeasure
- }
if (element is DrawModifier) {
mask = mask or Nodes.Draw
}
@@ -137,8 +133,7 @@
}
if (
element is OnPlacedModifier ||
- element is OnRemeasuredModifier ||
- element is LookaheadOnPlacedModifier
+ element is OnRemeasuredModifier
) {
mask = mask or Nodes.LayoutAware
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
index 1c6e779..22391a4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
@@ -71,7 +71,7 @@
affectsLookahead: Boolean = true,
block: () -> Unit
) {
- if (affectsLookahead && node.mLookaheadScope != null) {
+ if (affectsLookahead && node.lookaheadRoot != null) {
observeReads(node, onCommitAffectingLookaheadLayout, block)
} else {
observeReads(node, onCommitAffectingLayout, block)
@@ -86,7 +86,7 @@
affectsLookahead: Boolean = true,
block: () -> Unit
) {
- if (affectsLookahead && node.mLookaheadScope != null) {
+ if (affectsLookahead && node.lookaheadRoot != null) {
observeReads(node, onCommitAffectingLayoutModifierInLookahead, block)
} else {
observeReads(node, onCommitAffectingLayoutModifier, block)
@@ -101,7 +101,7 @@
affectsLookahead: Boolean = true,
block: () -> Unit
) {
- if (affectsLookahead && node.mLookaheadScope != null) {
+ if (affectsLookahead && node.lookaheadRoot != null) {
observeReads(node, onCommitAffectingLookaheadMeasure, block)
} else {
observeReads(node, onCommitAffectingMeasure, block)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurerHelper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurerHelper.kt
index 9d3b1c9..b94e5ef 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurerHelper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurerHelper.kt
@@ -37,7 +37,6 @@
* @param cacheSize Capacity of internal cache inside TextMeasurer. Size unit is the number of
* unique text layout inputs that are measured.
*/
-@ExperimentalTextApi
@Composable
fun rememberTextMeasurer(
cacheSize: Int = DefaultCacheSize
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt
index 7c4f310..5dd6560 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/PolyFitLeastSquaresTest.kt
@@ -27,182 +27,182 @@
@Test
fun polyFitLeastSquares_linear2PointsSlopeOf0Intercept1_isCorrect() {
- val x = listOf(0f, 1f)
- val y = listOf(1f, 1f)
+ val x = floatArrayOf(0f, 1f)
+ val y = floatArrayOf(1f, 1f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(1f, 0f))
+ assertIsCloseToEquals(actual, floatArrayOf(1f, 0f))
}
@Test
fun polyFitLeastSquares_linear2PointsSlopeOf1Intercept0_isCorrect() {
- val x = listOf(0f, 1f)
- val y = listOf(0f, 1f)
+ val x = floatArrayOf(0f, 1f)
+ val y = floatArrayOf(0f, 1f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(0f, 1f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 1f))
}
@Test
fun polyFitLeastSquares_linear2PointsSlopeOf1000000_isCorrect() {
- val x = listOf(0f, 1f)
- val y = listOf(0f, 1000000f)
+ val x = floatArrayOf(0f, 1f)
+ val y = floatArrayOf(0f, 1000000f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(0f, 1000000f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 1000000f))
}
@Test
fun polyFitLeastSquares_linear2PointsSlopeOfNegative5_isCorrect() {
- val x = listOf(0f, 1f)
- val y = listOf(0f, -5f)
+ val x = floatArrayOf(0f, 1f)
+ val y = floatArrayOf(0f, -5f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(0f, -5f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, -5f))
}
@Test
fun polyFitLeastSquares_linear2PointsRandom1_isCorrect() {
- val x = listOf(-8f, -2f)
- val y = listOf(7f, 5f)
+ val x = floatArrayOf(-8f, -2f)
+ val y = floatArrayOf(7f, 5f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(4.33333f, -.33333f))
+ assertIsCloseToEquals(actual, floatArrayOf(4.33333f, -.33333f))
}
@Test
fun polyFitLeastSquares_linear2PointsRandom2_isCorrect() {
- val x = listOf(-7f, 4f)
- val y = listOf(4f, -2f)
+ val x = floatArrayOf(-7f, 4f)
+ val y = floatArrayOf(4f, -2f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(.181818f, -.545454f))
+ assertIsCloseToEquals(actual, floatArrayOf(.181818f, -.545454f))
}
@Test
fun polyFitLeastSquares_linear2PointsRandom3_isCorrect() {
- val x = listOf(4f, -6f)
- val y = listOf(-6f, 0f)
+ val x = floatArrayOf(4f, -6f)
+ val y = floatArrayOf(-6f, 0f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(-3.6f, -.6f))
+ assertIsCloseToEquals(actual, floatArrayOf(-3.6f, -.6f))
}
@Test
fun polyFitLeastSquares_linear4PointsImperfect_isCorrect() {
- val x = listOf(0f, 2f, 0f, 2f)
- val y = listOf(0f, 1f, 2f, 3f)
+ val x = floatArrayOf(0f, 2f, 0f, 2f)
+ val y = floatArrayOf(0f, 1f, 2f, 3f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(1f, .5f))
+ assertIsCloseToEquals(actual, floatArrayOf(1f, .5f))
}
@Test
fun polyFitLeastSquares_quadratic3PointsActuallyLinear_isCorrect() {
- val x = listOf(0f, 1f, 2f)
- val y = listOf(0f, 1f, 2f)
+ val x = floatArrayOf(0f, 1f, 2f)
+ val y = floatArrayOf(0f, 1f, 2f)
- val actual = polyFitLeastSquares(x, y, 2)
+ val actual = polyFitLeastSquares(x, y, x.size, 2)
- assertIsCloseToEquals(actual, listOf(0f, 1f, 0f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 1f, 0f))
}
@Test
fun polyFitLeastSquares_quadratic3Points_isCorrect() {
- val x = listOf(0f, 1f, 2f)
- val y = listOf(0f, 1f, 0f)
+ val x = floatArrayOf(0f, 1f, 2f)
+ val y = floatArrayOf(0f, 1f, 0f)
- val actual = polyFitLeastSquares(x, y, 2)
+ val actual = polyFitLeastSquares(x, y, x.size, 2)
- assertIsCloseToEquals(actual, listOf(0f, 2f, -1f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 2f, -1f))
}
@Test
fun polyFitLeastSquares_quadratic5Points_isCorrect() {
- val x = listOf(0f, 1f, 2f, 3f, 4f)
- val y = listOf(0f, 1f, 4f, 9f, 16f)
+ val x = floatArrayOf(0f, 1f, 2f, 3f, 4f)
+ val y = floatArrayOf(0f, 1f, 4f, 9f, 16f)
- val actual = polyFitLeastSquares(x, y, 2)
+ val actual = polyFitLeastSquares(x, y, x.size, 2)
- assertIsCloseToEquals(actual, listOf(0f, 0f, 1f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 0f, 1f))
}
@Test
fun polyFitLeastSquares_quadratic4PointsImperfect_isCorrect() {
- val x = listOf(0f, 1f, 2f, 1f)
- val y = listOf(0f, -1f, 0f, -2f)
+ val x = floatArrayOf(0f, 1f, 2f, 1f)
+ val y = floatArrayOf(0f, -1f, 0f, -2f)
- val actual = polyFitLeastSquares(x, y, 2)
+ val actual = polyFitLeastSquares(x, y, x.size, 2)
- assertIsCloseToEquals(actual, listOf(0f, -3f, 1.5f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, -3f, 1.5f))
}
@Test
fun polyFitLeastSquares_cubic4PointsActuallyLinear_isCorrect() {
- val x = listOf(0f, 1f, 2f, 3f)
- val y = listOf(0f, 1f, 2f, 3f)
+ val x = floatArrayOf(0f, 1f, 2f, 3f)
+ val y = floatArrayOf(0f, 1f, 2f, 3f)
- val actual = polyFitLeastSquares(x, y, 3)
+ val actual = polyFitLeastSquares(x, y, x.size, 3)
- assertIsCloseToEquals(actual, listOf(0f, 1f, 0f, 0f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 1f, 0f, 0f))
}
@Test
fun polyFitLeastSquares_cubic4Points_isCorrect() {
- val x = listOf(-1f, 0f, 1f, 2f)
- val y = listOf(1f, 0f, 1f, 0f)
+ val x = floatArrayOf(-1f, 0f, 1f, 2f)
+ val y = floatArrayOf(1f, 0f, 1f, 0f)
- val actual = polyFitLeastSquares(x, y, 3)
+ val actual = polyFitLeastSquares(x, y, x.size, 3)
- assertIsCloseToEquals(actual, listOf(0f, .66666f, 1f, -.66666f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, .66666f, 1f, -.66666f))
}
@Test
fun polyFitLeastSquares_cubic6PointsImperfect_isCorrect() {
- val x = listOf(-1f, 0f, 1f, 2f, 0f, 1f)
- val y = listOf(1f, 0f, 1f, 0f, 1f, 0f)
+ val x = floatArrayOf(-1f, 0f, 1f, 2f, 0f, 1f)
+ val y = floatArrayOf(1f, 0f, 1f, 0f, 1f, 0f)
- val actual = polyFitLeastSquares(x, y, 3)
+ val actual = polyFitLeastSquares(x, y, x.size, 3)
- assertIsCloseToEquals(actual, listOf(.5f, -.083333f, .25f, -.16666f))
+ assertIsCloseToEquals(actual, floatArrayOf(.5f, -.083333f, .25f, -.16666f))
}
@Test
fun polyFitLeastSquares_1Point_isCorrect() {
- val x = listOf(0f)
- val y = listOf(13f)
+ val x = floatArrayOf(0f)
+ val y = floatArrayOf(13f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(13f, 0f))
+ assertIsCloseToEquals(actual, floatArrayOf(13f, 0f))
}
@Test
fun polyFitLeastSquares_degreeLargerThanData_isCorrect() {
- val x = listOf(0f, 1f)
- val y = listOf(0f, 1f)
+ val x = floatArrayOf(0f, 1f)
+ val y = floatArrayOf(0f, 1f)
- val actual = polyFitLeastSquares(x, y, 2)
+ val actual = polyFitLeastSquares(x, y, x.size, 2)
- assertIsCloseToEquals(actual, listOf(0f, 1f, 0f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 1f, 0f))
}
@Test
fun polyFitLeastSquares_3Points2IdenticalDegree1_isCorrect() {
- val x = listOf(0f, 0f, 1f)
- val y = listOf(0f, 0f, 1f)
+ val x = floatArrayOf(0f, 0f, 1f)
+ val y = floatArrayOf(0f, 0f, 1f)
- val actual = polyFitLeastSquares(x, y, 1)
+ val actual = polyFitLeastSquares(x, y, x.size, 1)
- assertIsCloseToEquals(actual, listOf(0f, 1f))
+ assertIsCloseToEquals(actual, floatArrayOf(0f, 1f))
}
@Test
@@ -215,23 +215,11 @@
private fun polyFitLeastSquares_degreeIsNegative_throwsIllegalArgumentException(
degree: Int
) {
- val x = listOf(0f, 1f)
- val y = listOf(0f, 1f)
+ val x = floatArrayOf(0f, 1f)
+ val y = floatArrayOf(0f, 1f)
val throwable = catchThrowable {
- polyFitLeastSquares(x, y, degree)
- }
-
- assertThat(throwable is IllegalArgumentException).isTrue()
- }
-
- @Test
- fun polyFitLeastSquares_missingData_throwsIllegalArgumentException() {
- val x = listOf(-1f, 1f, 3f)
- val y = listOf(1f, 3f)
-
- val throwable = catchThrowable {
- polyFitLeastSquares(x, y, 1)
+ polyFitLeastSquares(x, y, x.size, degree)
}
assertThat(throwable is IllegalArgumentException).isTrue()
@@ -239,11 +227,11 @@
@Test
fun polyFitLeastSquares_noData_throwsIllegalArgumentException() {
- val x = listOf<Float>()
- val y = listOf<Float>()
+ val x = floatArrayOf()
+ val y = floatArrayOf()
val throwable = catchThrowable {
- polyFitLeastSquares(x, y, 1)
+ polyFitLeastSquares(x, y, x.size, 1)
}
assertThat(throwable is IllegalArgumentException).isTrue()
@@ -251,11 +239,11 @@
@Test
fun polyFitLeastSquares_extremeSlope_throwsException() {
- val x = listOf(0f, Float.MIN_VALUE)
- val y = listOf(0f, Float.MAX_VALUE)
+ val x = floatArrayOf(0f, Float.MIN_VALUE)
+ val y = floatArrayOf(0f, Float.MAX_VALUE)
val throwable = catchThrowable {
- polyFitLeastSquares(x, y, 1)
+ polyFitLeastSquares(x, y, x.size, 1)
}
assertThat(throwable is IllegalArgumentException).isTrue()
@@ -263,11 +251,11 @@
@Test
fun polyFitLeastSquares_3Points2IdenticalDegree2_throwsException() {
- val x = listOf(0f, 0f, 1f)
- val y = listOf(0f, 0f, 1f)
+ val x = floatArrayOf(0f, 0f, 1f)
+ val y = floatArrayOf(0f, 0f, 1f)
val throwable = catchThrowable {
- polyFitLeastSquares(x, y, 2)
+ polyFitLeastSquares(x, y, x.size, 2)
}
assertThat(throwable is IllegalArgumentException).isTrue()
@@ -286,8 +274,8 @@
}
private fun assertIsCloseToEquals(
- actual: List<Float>,
- expected: List<Float>
+ actual: FloatArray,
+ expected: FloatArray
) {
assertThat(expected.size).isEqualTo(expected.size)
expected.forEachIndexed() { index, value ->
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index ba2f84a9..f2b3795 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -381,7 +381,7 @@
}
public static final class Easing.Companion {
- method public androidx.constraintlayout.compose.Easing Cubic(@FloatRange(from=0.0, to=1.0) float x1, @FloatRange(from=0.0, to=1.0) float y1, @FloatRange(from=0.0, to=1.0) float x2, @FloatRange(from=0.0, to=1.0) float y2);
+ method public androidx.constraintlayout.compose.Easing Cubic(float x1, float y1, float x2, float y2);
method public androidx.constraintlayout.compose.Easing getAccelerate();
method public androidx.constraintlayout.compose.Easing getAnticipate();
method public androidx.constraintlayout.compose.Easing getDecelerate();
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 9023b3d..96395d1 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -28,7 +28,7 @@
dependencies {
if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- implementation("androidx.compose.ui:ui:1.4.0-beta02")
+ implementation(project(":compose:ui:ui"))
implementation("androidx.compose.ui:ui-unit:1.4.0-beta02")
implementation("androidx.compose.ui:ui-util:1.4.0-beta02")
implementation("androidx.compose.foundation:foundation:1.4.0-beta02")
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index 733694d..f790ba9 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -17,6 +17,7 @@
package androidx.constraintlayout.compose
import android.content.Context
+import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
@@ -36,6 +37,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.layout
@@ -50,10 +53,13 @@
import androidx.compose.ui.platform.ValueElement
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
@@ -62,7 +68,9 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import kotlin.math.roundToInt
+import kotlin.test.assertNotEquals
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@@ -2465,6 +2473,75 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun testVisibility_withInlineDsl() = with(rule.density) {
+ val rootSizePx = 100
+ val boxSizePx = 10
+ var box1Position = IntOffset.Zero
+ val expectedInitialBox1Position = Offset(
+ (rootSizePx + boxSizePx) / 2f,
+ (rootSizePx - boxSizePx) / 2f
+ ).round()
+
+ val boxVisibility = mutableStateOf(Visibility.Visible)
+
+ rule.setContent {
+ ConstraintLayout(Modifier.size(rootSizePx.toDp())) {
+ val (box0, box1) = createRefs()
+
+ Box(
+ Modifier
+ .testTag("box0")
+ .constrainAs(box0) {
+ centerTo(parent)
+ visibility = boxVisibility.value
+ }
+ .background(Color.Red)
+ ) {
+ Box(Modifier.size(boxSizePx.toDp()))
+ }
+ Box(
+ Modifier
+ .testTag("box1")
+ .constrainAs(box1) {
+ width = boxSizePx.toDp().asDimension
+ height = boxSizePx.toDp().asDimension
+
+ top.linkTo(box0.top)
+ start.linkTo(box0.end)
+ }
+ .background(Color.Blue)
+ .onGloballyPositioned {
+ box1Position = it
+ .positionInParent()
+ .round()
+ }
+ )
+ }
+ }
+ rule.waitForIdle()
+
+ var color = rule.onNodeWithTag("box0").captureToImage().asAndroidBitmap().getColor(5, 5)
+ assertEquals(Color.Red.toArgb(), color.toArgb())
+ assertEquals(expectedInitialBox1Position, box1Position)
+
+ boxVisibility.value = Visibility.Invisible
+ rule.waitForIdle()
+
+ color = rule.onNodeWithTag("box0").captureToImage().asAndroidBitmap().getColor(5, 5)
+ assertNotEquals(Color.Red.toArgb(), color.toArgb())
+ assertEquals(expectedInitialBox1Position, box1Position)
+
+ boxVisibility.value = Visibility.Gone
+ rule.waitForIdle()
+
+ // Dp.Unspecified since Gone Composables are not placed
+ rule.onNodeWithTag("box0").assertWidthIsEqualTo(Dp.Unspecified)
+ rule.onNodeWithTag("box0").assertHeightIsEqualTo(Dp.Unspecified)
+ assertEquals(Offset(rootSizePx / 2f, rootSizePx / 2f).round(), box1Position)
+ }
+
private fun listAnchors(box: ConstrainedLayoutReference): List<ConstrainScope.() -> Unit> {
// TODO(172055763) directly construct an immutable list when Lint supports it
val anchors = mutableListOf<ConstrainScope.() -> Unit>()
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
index 72139b8..92890ea 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
@@ -1654,6 +1654,12 @@
frame: WidgetFrame,
offset: IntOffset = IntOffset.Zero
) {
+ if (frame.visibility == ConstraintWidget.GONE) {
+ if (DEBUG) {
+ Log.d("CCL", "Widget: ${frame.id} is Gone. Skipping placement.")
+ }
+ return
+ }
if (frame.isDefaultTransform) {
val x = frame.left - offset.x
val y = frame.top - offset.y
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
index 662566b..9dfca0b 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
@@ -36,9 +36,11 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.layout.LookaheadLayout
-import androidx.compose.ui.layout.LookaheadLayoutScope
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
@@ -110,18 +112,20 @@
}
}
}
- LookaheadLayout(
- modifier = modifier,
- content = {
- val scope = remember {
- MotionScope(
- lookaheadLayoutScope = this
- )
- }
- scope.content()
- },
- measurePolicy = policy
- )
+ LookaheadScope {
+ Layout(
+ content = {
+ val scope = remember {
+ MotionScope(
+ lookaheadScope = this
+ )
+ }
+ scope.content()
+ },
+ modifier = modifier,
+ measurePolicy = policy
+ )
+ }
}
@DslMarker
@@ -137,8 +141,8 @@
@MotionDslScope
@OptIn(ExperimentalComposeUiApi::class)
private class MotionScope(
- lookaheadLayoutScope: LookaheadLayoutScope
-) : LookaheadLayoutScope by lookaheadLayoutScope {
+ lookaheadScope: LookaheadScope
+) : LookaheadScope by lookaheadScope {
private var nextId: Int = 1000
private var lastId: Int = nextId
@@ -240,6 +244,23 @@
remember { Ref<IntSize>().apply { value = IntSize.Zero } }
val lastPosition: Ref<IntOffset> = remember { Ref<IntOffset>().apply { value = null } }
+ fun Placeable.PlacementScope.onPlaced(scope: LookaheadScope) {
+ coordinates?.let {
+ with(scope) {
+ parentSize.value = lookaheadScopeCoordinates.toLookaheadCoordinates().size
+ val localPosition = lookaheadScopeCoordinates
+ .localPositionOf(it, Offset.Zero)
+ .round()
+ val lookAheadPosition = lookaheadScopeCoordinates
+ .localLookaheadPositionOf(it)
+ .round()
+ targetOffset = lookAheadPosition
+ placementOffset = localPosition
+ commitLookAheadChanges(targetOffset!!, targetSize!!)
+ }
+ }
+ }
+
LaunchedEffect(Unit) {
launch {
snapshotFlow {
@@ -277,73 +298,57 @@
}
}
}
- this
- .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
- parentSize.value = lookaheadScopeCoordinates.size
- val localPosition = lookaheadScopeCoordinates
- .localPositionOf(
- layoutCoordinates,
- Offset.Zero
+ this.intermediateLayout { measurable, _ ->
+ targetSize = lookaheadSize
+ if (targetBounds == IntRect.Zero) {
+ // Unset, this is first measure
+ val newConstraints =
+ Constraints.fixed(lookaheadSize.width, lookaheadSize.height)
+ val placeable = measurable.measure(newConstraints)
+ layout(placeable.width, placeable.height) {
+ onPlaced(this@intermediateLayout)
+ placeable.place(targetOffset!! - placementOffset)
+ }
+ } else {
+ // Following measures
+ val width: Int
+ val height: Int
+ if (progressAnimation.isRunning) {
+ val fraction =
+ 1.0f - abs(progressAnimation.value - progressAnimation.targetValue)
+ widgetState.interpolate(
+ parentSize.value!!.width,
+ parentSize.value!!.height,
+ fraction,
+ transitionState
)
- .round()
- val lookAheadPosition = lookaheadScopeCoordinates
- .localLookaheadPositionOf(
- layoutCoordinates
- )
- .round()
- targetOffset = lookAheadPosition
- placementOffset = localPosition
- commitLookAheadChanges(targetOffset!!, targetSize!!)
- }
- .intermediateLayout { measurable, _, lookaheadSize ->
- targetSize = lookaheadSize
- if (targetBounds == IntRect.Zero) {
- // Unset, this is first measure
- val newConstraints =
- Constraints.fixed(lookaheadSize.width, lookaheadSize.height)
- val placeable = measurable.measure(newConstraints)
- layout(placeable.width, placeable.height) {
- placeable.place(targetOffset!! - placementOffset)
- }
+ width = widgetState
+ .getFrame(2)
+ .width()
+ height = widgetState
+ .getFrame(2)
+ .height()
} else {
- // Following measures
- val width: Int
- val height: Int
+ width = lastSize.value?.width ?: targetBounds.width
+ height = lastSize.value?.height ?: targetBounds.height
+ }
+ val animatedConstraints = Constraints.fixed(width, height)
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, placeable.height) {
+ onPlaced(this@intermediateLayout)
if (progressAnimation.isRunning) {
- val fraction =
- 1.0f - abs(progressAnimation.value - progressAnimation.targetValue)
- widgetState.interpolate(
- parentSize.value!!.width,
- parentSize.value!!.height,
- fraction,
- transitionState
+ placeWithFrameTransform(
+ placeable,
+ widgetState.getFrame(2),
+ placementOffset
)
- width = widgetState
- .getFrame(2)
- .width()
- height = widgetState
- .getFrame(2)
- .height()
} else {
- width = lastSize.value?.width ?: targetBounds.width
- height = lastSize.value?.height ?: targetBounds.height
- }
- val animatedConstraints = Constraints.fixed(width, height)
- val placeable = measurable.measure(animatedConstraints)
- layout(placeable.width, placeable.height) {
- if (progressAnimation.isRunning) {
- placeWithFrameTransform(
- placeable,
- widgetState.getFrame(2),
- placementOffset
- )
- } else {
- val (x, y) = (lastPosition.value ?: IntOffset.Zero) - placementOffset
- placeable.place(x, y)
- }
+ val (x, y) = (lastPosition.value ?: IntOffset.Zero) - placementOffset
+ placeable.place(x, y)
}
}
}
+ }
}
private fun ConstraintWidget.applyBounds(rect: IntRect) {
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
index 57d277d..788173c 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
@@ -16,7 +16,6 @@
package androidx.constraintlayout.compose
-import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -390,13 +389,16 @@
/**
* Defines a Cubic-Bezier curve where the points P1 and P2 are at the given coordinate
* ratios.
+ *
+ * P1 and P2 are typically defined within (0f, 0f) and (1f, 1f), but may be assigned beyond
+ * these values for overshoot curves.
+ *
+ * @param x1 X-axis value for P1. Value is typically defined within 0f-1f.
+ * @param y1 Y-axis value for P1. Value is typically defined within 0f-1f.
+ * @param x2 X-axis value for P2. Value is typically defined within 0f-1f.
+ * @param y2 Y-axis value for P2. Value is typically defined within 0f-1f.
*/
- fun Cubic(
- @FloatRange(from = 0.0, to = 1.0) x1: Float,
- @FloatRange(from = 0.0, to = 1.0) y1: Float,
- @FloatRange(from = 0.0, to = 1.0) x2: Float,
- @FloatRange(from = 0.0, to = 1.0) y2: Float
- ) = Easing("cubic($x1, $y1, $x2, $y2)")
+ fun Cubic(x1: Float, y1: Float, x2: Float, y2: Float) = Easing("cubic($x1, $y1, $x2, $y2)")
}
}
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
index 372d3ad..582bb9b 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
@@ -1630,6 +1630,7 @@
break;
case "invisible":
reference.visibility(ConstraintWidget.INVISIBLE);
+ reference.alpha(0f);
break;
case "gone":
reference.visibility(ConstraintWidget.GONE);
diff --git a/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java b/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java
index cc017a7..fc71818 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/AnimatorSet.java
@@ -920,7 +920,6 @@
* @param frameTime The frame start time, in the {@link android.os.SystemClock#uptimeMillis()}
* time base.
* @return
- * @hide
*/
@Override
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
index 4654d57..3d468ca 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
@@ -1333,7 +1333,6 @@
*
* @param frameTime The frame time.
* @return true if the animation has ended.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
index 2debfff..69e3629 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksum.java
@@ -113,7 +113,6 @@
*/
public static final int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512 = 0x00000040;
- /** @hide */
@RestrictTo(LIBRARY)
@IntDef(flag = true, value = {
TYPE_WHOLE_MERKLE_ROOT_4K_SHA256,
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
index 078fdd8..b21f414 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
@@ -49,7 +49,6 @@
/**
* Provides a listener on changes to shortcuts in ShortcutInfoCompat.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class ShortcutInfoChangeListenerImpl extends ShortcutInfoChangeListener {
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java
index 9f5b265..a4f3992 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/TrampolineActivity.java
@@ -42,7 +42,6 @@
* Activity used to receives shortcut intents sent from Google, extracts its shortcut url, and
* launches it in the scope of the app.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public class TrampolineActivity extends Activity {
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java
index 5414efd..cd29c16 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java
@@ -29,7 +29,6 @@
/**
* Builder for the Capability section in the Shortcut Corpus.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public class CapabilityBuilder extends IndexableBuilder<CapabilityBuilder> {
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/Constants.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/Constants.java
index 35f8981..3b0f9bb 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/Constants.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/Constants.java
@@ -23,7 +23,6 @@
/**
* Constants for the Shortcut Corpus.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public class Constants {
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java
index f6751d2..394d4a9 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java
@@ -29,7 +29,6 @@
/**
* Builder for the Parameter section in the Shortcut Corpus.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public class ParameterBuilder
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java
index 07086ec..488738c 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java
@@ -32,7 +32,6 @@
/**
* Builder for the Shortcut Corpus.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public class ShortcutBuilder extends IndexableBuilder<ShortcutBuilder> {
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java
index fe061a9..df7e598 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP)
package androidx.core.google.shortcuts;
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ShortcutUtils.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ShortcutUtils.java
index d6b8393..fe8aaa6 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ShortcutUtils.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ShortcutUtils.java
@@ -41,7 +41,6 @@
/**
* Utility methods and constants used by the Google shortcuts library.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public class ShortcutUtils {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/DateTimeFormatterJdkStyleOptions.kt b/core/core-i18n/src/main/java/androidx/core/i18n/DateTimeFormatterJdkStyleOptions.kt
index e8f7547c..48b00bc 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/DateTimeFormatterJdkStyleOptions.kt
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/DateTimeFormatterJdkStyleOptions.kt
@@ -17,13 +17,14 @@
package androidx.core.i18n
import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
import java.text.DateFormat
/**
* Date/time formatting styles,
* compatible to the ones in [java.text.DateFormat]
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.SOURCE)
@IntDef(
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java
index 219e458..e43da90 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUConfig.java
@@ -18,7 +18,6 @@
/**
* ICUConfig is a class used for accessing ICU4J runtime configuration.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ICUConfig {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java
index 55df20b..a20acca 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/ICUData.java
@@ -19,7 +19,6 @@
/**
* Provides access to ICU data files as InputStreams. Implements security checking.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class ICUData {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/PatternProps.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/PatternProps.java
index fdc433c..31e35fb 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/PatternProps.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/impl/PatternProps.java
@@ -29,7 +29,6 @@
* \u2190-\u245F\u2500-\u2775\u2794-\u2BFF\u2E00-\u2E7F
* \u3001-\u3003\u3008-\u3020\u3030\uFD3E\uFD3F\uFE45\uFE46]
* @author mscherer
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class PatternProps {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/LocaleElements_plurals.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/LocaleElements_plurals.java
index 9c1922f..c75db71 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/LocaleElements_plurals.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/LocaleElements_plurals.java
@@ -16,7 +16,6 @@
*
* WARNING: don't edit directly.
* It is generated with {@code genrb} after building icu4c.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class LocaleElements_plurals extends ListResourceBundle {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/MessageFormat.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/MessageFormat.java
index 1a3fd47..145e8ef 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/MessageFormat.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/MessageFormat.java
@@ -363,7 +363,6 @@
* @author Mark Davis
* @author Markus Scherer
* icu_annot::stable ICU 3.0
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class MessageFormat extends Format {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralFormat.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralFormat.java
index 40ed2b9..161ab4c 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralFormat.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralFormat.java
@@ -128,7 +128,6 @@
*
* @author tschumann (Tim Schumann)
* icu_annot::stable ICU 3.8
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PluralFormat /* extends UFormat */ {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRules.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRules.java
index ab9643d..157b8fc 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRules.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRules.java
@@ -164,7 +164,6 @@
* </p>
*
* icu_annot::stable ICU 3.8
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PluralRules implements Serializable {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRulesLoader.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRulesLoader.java
index 6d1b6fa..2bf07f6 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRulesLoader.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/simple/PluralRulesLoader.java
@@ -20,7 +20,6 @@
/**
* Loader for plural rules data.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PluralRulesLoader extends PluralRules.Factory {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java
index 08da3d2..b0b190d7 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/MessagePattern.java
@@ -77,7 +77,6 @@
*
* icu_annot::stable ICU 4.8
* @author Markus Scherer
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class MessagePattern implements Cloneable, Freezable<MessagePattern> {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/SelectFormat.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/SelectFormat.java
index ad499a1..99cc5b4 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/SelectFormat.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/text/SelectFormat.java
@@ -147,7 +147,6 @@
* </p>
*
* icu_annot::stable ICU 4.4
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class SelectFormat extends Format{
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Freezable.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Freezable.java
index f8b61b3c..6882f3a 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Freezable.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Freezable.java
@@ -299,7 +299,6 @@
*
* </blockquote>
* icu_annot::stable ICU 3.8
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public interface Freezable<T> extends Cloneable {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUCloneNotSupportedException.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUCloneNotSupportedException.java
index 9bc2ac8..3a0f3e0 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUCloneNotSupportedException.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUCloneNotSupportedException.java
@@ -15,7 +15,6 @@
*
* icu_annot::draft ICU 53
* icu_annot::provisional This API might change or be removed in a future release.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ICUCloneNotSupportedException extends ICUException {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUException.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUException.java
index 290eb54..a78f814 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUException.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUException.java
@@ -13,7 +13,6 @@
*
* icu_annot::draft ICU 53
* icu_annot::provisional This API might change or be removed in a future release.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ICUException extends RuntimeException {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUUncheckedIOException.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUUncheckedIOException.java
index 3e2510b..2c1e2b5 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUUncheckedIOException.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/ICUUncheckedIOException.java
@@ -19,7 +19,6 @@
*
* icu_annot::draft ICU 53
* icu_annot::provisional This API might change or be removed in a future release.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ICUUncheckedIOException extends RuntimeException {
diff --git a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Output.java b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Output.java
index 12b9fdc..0133443 100644
--- a/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Output.java
+++ b/core/core-i18n/src/main/java/androidx/core/i18n/messageformat_icu/util/Output.java
@@ -12,7 +12,6 @@
* Simple struct-like class for output parameters.
* @param <T> The type of the parameter.
* icu_annot::stable ICU 4.8
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class Output<T> {
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
index 37e448c..f3cd443 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
@@ -20,6 +20,7 @@
import android.location.Location;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
@@ -30,8 +31,8 @@
* Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
* into ones above Mean Sea Level.
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class AltitudeConverterCompat {
private static final double MAX_ABS_VALID_LATITUDE = 90;
@@ -56,7 +57,6 @@
*
* <p>NOTE: Currently throws {@link UnsupportedOperationException} for a valid {@code location}.
*
- * @hide
*/
@NonNull
public static ListenableFuture<Location> addMslAltitudeAsync(
diff --git a/core/core-remoteviews/api/restricted_1.0.0-beta04.txt b/core/core-remoteviews/api/restricted_1.0.0-beta04.txt
index 479f021..41be713 100644
--- a/core/core-remoteviews/api/restricted_1.0.0-beta04.txt
+++ b/core/core-remoteviews/api/restricted_1.0.0-beta04.txt
@@ -291,6 +291,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class RemoteViewsCompatService extends android.widget.RemoteViewsService {
+ ctor public RemoteViewsCompatService();
method public android.widget.RemoteViewsService.RemoteViewsFactory onGetViewFactory(android.content.Intent intent);
}
diff --git a/core/core-remoteviews/api/restricted_current.txt b/core/core-remoteviews/api/restricted_current.txt
index 479f021..41be713 100644
--- a/core/core-remoteviews/api/restricted_current.txt
+++ b/core/core-remoteviews/api/restricted_current.txt
@@ -291,6 +291,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class RemoteViewsCompatService extends android.widget.RemoteViewsService {
+ ctor public RemoteViewsCompatService();
method public android.widget.RemoteViewsService.RemoteViewsFactory onGetViewFactory(android.content.Intent intent);
}
diff --git a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
index 8fa7f2f..b39eeef 100644
--- a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
+++ b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
@@ -118,7 +118,6 @@
}
}
- /** @hide */
internal constructor(parcel: Parcel) {
val length = parcel.readInt()
mIds = LongArray(length)
@@ -128,7 +127,6 @@
mViewTypeCount = parcel.readInt()
}
- /** @hide */
internal fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(mIds.size)
dest.writeLongArray(mIds)
diff --git a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
index e386f7c..bf571c91 100644
--- a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
+++ b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
@@ -37,7 +37,6 @@
/**
* [RemoteViewsService] to provide [RemoteViews] set using [RemoteViewsCompat.setRemoteAdapter].
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class RemoteViewsCompatService : RemoteViewsService() {
diff --git a/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java
index 5f229c4..899b3e6 100644
--- a/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/TraceCompatTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import static java.nio.charset.StandardCharsets.UTF_8;
import android.app.UiAutomation;
@@ -37,6 +39,7 @@
import org.junit.Test;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -46,21 +49,34 @@
public final class TraceCompatTest {
private static final int TRACE_BUFFER_SIZE = 8192;
+
+ private static final boolean TRACE_AVAILABLE;
+
+ static {
+ // Check if tracing is available via debugfs or tracefs
+ TRACE_AVAILABLE = new File("/sys/kernel/debug/tracing/trace_marker").exists()
+ || new File("/sys/kernel/tracing/trace_marker").exists();
+ }
+
private ByteArrayOutputStream mByteArrayOutputStream;
@Before
public void setUp() {
+ assumeTrue("Tracing is not available via debugfs or tracefs.", TRACE_AVAILABLE);
mByteArrayOutputStream = new ByteArrayOutputStream();
}
@After
public void stopAtrace() throws IOException {
- // Since API 23, 'async_stop' will work. On lower API levels it was broken (see aosp/157142)
- if (Build.VERSION.SDK_INT >= 23) {
- executeCommand("atrace --async_stop");
- } else {
- // Ensure tracing is not currently running by performing a short synchronous trace.
- executeCommand("atrace -t 0");
+ if (TRACE_AVAILABLE) {
+ // Since API 23, 'async_stop' will work. On lower API levels it was broken
+ // (see aosp/157142)
+ if (Build.VERSION.SDK_INT >= 23) {
+ executeCommand("atrace --async_stop");
+ } else {
+ // Ensure tracing is not currently running by performing a short synchronous trace.
+ executeCommand("atrace -t 0");
+ }
}
}
diff --git a/core/core/src/main/java/android/support/v4/os/ResultReceiver.java b/core/core/src/main/java/android/support/v4/os/ResultReceiver.java
index 81ec408..9d4db1d 100644
--- a/core/core/src/main/java/android/support/v4/os/ResultReceiver.java
+++ b/core/core/src/main/java/android/support/v4/os/ResultReceiver.java
@@ -40,7 +40,6 @@
* lifecycle management (you must be using some higher-level component to tell
* the system that your process needs to continue running), the connection will
* break if your process goes away for any reason, etc.</p>
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@SuppressLint("BanParcelableUsage")
diff --git a/core/core/src/main/java/androidx/core/app/ActivityCompat.java b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
index b57647b..5ec885e 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
@@ -147,7 +147,6 @@
}
/**
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public interface RequestPermissionsRequestCodeValidator {
@@ -176,7 +175,6 @@
}
/**
- * @hide
*/
@Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/app/ActivityRecreator.java b/core/core/src/main/java/androidx/core/app/ActivityRecreator.java
index 40762ff..02f60f9 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityRecreator.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityRecreator.java
@@ -65,7 +65,6 @@
* <p>The fix for this is to add the missing onStop() call, by using reflection to call into
* ActivityThread.
*
- * @hide
*/
@RestrictTo(LIBRARY)
@SuppressWarnings("PrivateApi")
diff --git a/core/core/src/main/java/androidx/core/app/ComponentActivity.java b/core/core/src/main/java/androidx/core/app/ComponentActivity.java
index 206cfd2..384c454 100644
--- a/core/core/src/main/java/androidx/core/app/ComponentActivity.java
+++ b/core/core/src/main/java/androidx/core/app/ComponentActivity.java
@@ -45,7 +45,6 @@
* lower level building blocks are included. Higher level components can then be used as needed
* without enforcing a deep Activity class hierarchy or strong coupling between components.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class ComponentActivity extends Activity implements
@@ -72,7 +71,6 @@
* <p>Note that these objects are not retained across configuration changes</p>
*
* @see #getExtraData
- * @hide
* @deprecated Use {@link View#setTag(int, Object)} with the window's decor view.
*/
@SuppressWarnings("deprecation")
@@ -100,7 +98,6 @@
* Retrieves a previously set {@link ExtraData} by class name.
*
* @see #putExtraData
- * @hide
* @deprecated Use {@link View#getTag(int)} with the window's decor view.
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -117,7 +114,6 @@
}
/**
- * @hide
* @param event
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -189,7 +185,6 @@
}
/**
- * @hide
* @deprecated Store the object you want to save directly by using
* {@link View#setTag(int, Object)} with the window's decor view.
*/
diff --git a/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java b/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java
index 9db5e7f..d20458c 100644
--- a/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java
+++ b/core/core/src/main/java/androidx/core/app/CoreComponentFactory.java
@@ -32,7 +32,6 @@
/**
* Instance of AppComponentFactory for support libraries.
* @see CompatWrapped
- * @hide
*/
@RequiresApi(api = 28)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@@ -89,7 +88,6 @@
/**
* Implement this interface to allow a different class to be returned when instantiating
* on certain API levels.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public interface CompatWrapped {
diff --git a/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java b/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java
index 6837b6d..dace804 100644
--- a/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java
+++ b/core/core/src/main/java/androidx/core/app/FrameMetricsAggregator.java
@@ -171,7 +171,6 @@
private final FrameMetricsBaseImpl mInstance;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@IntDef(
diff --git a/core/core/src/main/java/androidx/core/app/NotificationBuilderWithBuilderAccessor.java b/core/core/src/main/java/androidx/core/app/NotificationBuilderWithBuilderAccessor.java
index 61ad3fc..c5f98e6 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationBuilderWithBuilderAccessor.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationBuilderWithBuilderAccessor.java
@@ -27,7 +27,6 @@
* an accessor for {@link Notification.Builder}. {@link Notification.Builder}
* was introduced in HoneyComb.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface NotificationBuilderWithBuilderAccessor {
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 1aeaaf3..124fa91 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -719,12 +719,10 @@
/**
* Maximum number of (generic) action buttons in a notification (contextual action buttons are
* handled separately).
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static final int MAX_ACTION_BUTTONS = 3;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_SYSTEM, AudioManager.STREAM_RING,
AudioManager.STREAM_MUSIC, AudioManager.STREAM_ALARM, AudioManager.STREAM_NOTIFICATION,
@@ -732,7 +730,6 @@
@Retention(RetentionPolicy.SOURCE)
public @interface StreamType {}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(SOURCE)
@IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
@@ -862,7 +859,6 @@
*/
public static final String CATEGORY_MISSED_CALL = "missed_call";
- /** @hide */
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({BADGE_ICON_NONE, BADGE_ICON_SMALL, BADGE_ICON_LARGE})
@@ -884,7 +880,6 @@
*/
public static final int BADGE_ICON_LARGE = Notification.BADGE_ICON_LARGE;
- /** @hide */
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({GROUP_ALERT_ALL, GROUP_ALERT_SUMMARY, GROUP_ALERT_CHILDREN})
@@ -929,7 +924,6 @@
*/
public static final String GROUP_KEY_SILENT = "silent";
- /** @hide */
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({FOREGROUND_SERVICE_DEFAULT,
@@ -1011,15 +1005,12 @@
// All these variables are declared public/hidden so they can be accessed by a builder
// extender.
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public Context mContext;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public ArrayList<Action> mActions = new ArrayList<>();
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
public ArrayList<Person> mPersonList = new ArrayList<>();
@@ -2545,7 +2536,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public RemoteViews getContentView() {
@@ -2553,7 +2543,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public RemoteViews getBigContentView() {
@@ -2561,7 +2550,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public RemoteViews getHeadsUpContentView() {
@@ -2571,7 +2559,6 @@
/**
* return when if it is showing or 0 otherwise
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public long getWhenIfShowing() {
@@ -2581,7 +2568,6 @@
/**
* @return the priority set on the notification
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public int getPriority() {
@@ -2591,7 +2577,6 @@
/**
* @return the foreground service behavior defined for the notification
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public int getForegroundServiceBehavior() {
@@ -2601,7 +2586,6 @@
/**
* @return the color of the notification
*
- * @hide
*/
@ColorInt
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -2612,7 +2596,6 @@
/**
* @return the {@link BubbleMetadata} of the notification
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public @Nullable BubbleMetadata getBubbleMetadata() {
@@ -2711,7 +2694,6 @@
*/
public static abstract class Style {
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected Builder mBuilder;
@@ -2744,7 +2726,6 @@
}
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -2757,14 +2738,12 @@
* Applies the compat style data to the framework {@link Notification} in a backwards
* compatible way. All other data should be stored within the Notification's extras.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void apply(NotificationBuilderWithBuilderAccessor builder) {
}
/**
- * @hide
* @return Whether custom content views are displayed inline in the style
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -2773,7 +2752,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
@@ -2781,7 +2759,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
@@ -2789,7 +2766,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) {
@@ -2805,7 +2781,6 @@
* Moreover, recovering builders and styles is only supported at API 19 and above, no
* implementation is required for current BigTextStyle, BigPictureStyle, or InboxStyle.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void addCompatExtras(@NonNull Bundle extras) {
@@ -2822,7 +2797,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void restoreFromCompatExtras(@NonNull Bundle extras) {
@@ -2834,7 +2808,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void clearCompatExtraKeys(@NonNull Bundle extras) {
@@ -2844,7 +2817,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
@@ -2949,7 +2921,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
@@ -3107,7 +3078,6 @@
* Create a bitmap using the given icon together with a color filter created from the given
* color.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public Bitmap createColoredBitmap(int iconId, int color) {
@@ -3157,7 +3127,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void buildIntoRemoteViews(RemoteViews outerView,
@@ -3355,7 +3324,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3365,7 +3333,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3419,7 +3386,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3436,7 +3402,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
@@ -3469,7 +3434,6 @@
}
/**
- * @hide
*/
@Override
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -3646,7 +3610,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3656,7 +3619,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3673,7 +3635,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3684,7 +3645,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -3700,7 +3660,6 @@
}
/**
- * @hide
*/
@Override
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -4032,7 +3991,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -4042,7 +4000,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -4214,7 +4171,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -4251,7 +4207,6 @@
}
/**
- * @hide
*/
@Override
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -4506,7 +4461,6 @@
/**
* Converts this compat {@link Message} to the base Android framework
* {@link Notification.MessagingStyle.Message}.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
@@ -4706,7 +4660,6 @@
"androidx.core.app.NotificationCompat$CallStyle";
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@@ -4927,7 +4880,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -4939,7 +4891,6 @@
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -4975,7 +4926,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5016,7 +4966,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5026,7 +4975,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5219,7 +5167,6 @@
* the correct place. This returns the correct result even if the system actions have
* already been added, and even if more actions were added since then.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
@@ -5602,7 +5549,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5612,7 +5558,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5630,7 +5575,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5644,7 +5588,6 @@
}
/**
- * @hide
*/
@Override
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -5726,7 +5669,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5736,7 +5678,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5745,7 +5686,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5758,7 +5698,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5775,7 +5714,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -5796,7 +5734,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -6222,7 +6159,6 @@
/**
* Creates a {@link Builder} from an {@link android.app.Notification.Action}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(19)
@@ -7980,13 +7916,11 @@
* to access values.
*/
public static final class CarExtender implements Extender {
- /** @hide **/
@RestrictTo(LIBRARY_GROUP_PREFIX)
static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
private static final String EXTRA_LARGE_ICON = "large_icon";
private static final String EXTRA_CONVERSATION = "car_conversation";
private static final String EXTRA_COLOR = "app_color";
- /** @hide **/
@RestrictTo(LIBRARY_GROUP_PREFIX)
static final String EXTRA_INVISIBLE_ACTIONS = "invisible_actions";
@@ -8639,7 +8573,6 @@
return (mFlags & FLAG_SUPPRESS_NOTIFICATION) != 0;
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void setFlags(int flags) {
mFlags = flags;
@@ -9437,7 +9370,6 @@
}
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
static boolean getHighPriority(@NonNull Notification notification) {
return (notification.flags & Notification.FLAG_HIGH_PRIORITY) != 0;
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 72af80f..54cc20f 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -50,7 +50,6 @@
/**
* Wrapper around {@link Notification.Builder} that works in a backwards compatible way.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccessor {
diff --git a/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java b/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
index db89748..35fe299 100644
--- a/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
+++ b/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
@@ -30,6 +30,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -37,7 +38,6 @@
/** Helper for accessing features in {@link PendingIntent}. */
public final class PendingIntentCompat {
- /** @hide */
@IntDef(
flag = true,
value = {
@@ -55,6 +55,7 @@
Intent.FILL_IN_CLIP_DATA
})
@Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public @interface Flags {}
/**
diff --git a/core/core/src/main/java/androidx/core/app/Person.java b/core/core/src/main/java/androidx/core/app/Person.java
index 32bfb95..6fd9467 100644
--- a/core/core/src/main/java/androidx/core/app/Person.java
+++ b/core/core/src/main/java/androidx/core/app/Person.java
@@ -62,7 +62,6 @@
* can be created from a {@link Person} using {@link #toPersistableBundle()}. The Icon of the
* Person will not be extracted from the PersistableBundle.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
@@ -74,7 +73,6 @@
/**
* Converts an Android framework {@link android.app.Person} to a compat {@link Person}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(28)
@@ -127,7 +125,6 @@
* bundle can be converted back by using {@link #fromPersistableBundle(PersistableBundle)}. The
* Icon of the Person will not be included in the resulting PersistableBundle.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
@@ -145,7 +142,6 @@
/**
* Converts this compat {@link Person} to the base Android framework {@link android.app.Person}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
@@ -214,7 +210,6 @@
/**
* @return the URI associated with this person, or "name:mName" otherwise
- * @hide
*/
@NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java b/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java
index 9023394..61cc975 100644
--- a/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java
+++ b/core/core/src/main/java/androidx/core/app/RemoteActionCompat.java
@@ -43,7 +43,6 @@
@VersionedParcelize(jetifyAs = "android.support.v4.app.RemoteActionCompat")
public final class RemoteActionCompat implements VersionedParcelable {
/**
- * @hide
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
@NonNull
@@ -51,7 +50,6 @@
@ParcelField(1)
public IconCompat mIcon;
/**
- * @hide
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
@NonNull
@@ -59,7 +57,6 @@
@ParcelField(2)
public CharSequence mTitle;
/**
- * @hide
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
@NonNull
@@ -67,7 +64,6 @@
@ParcelField(3)
public CharSequence mContentDescription;
/**
- * @hide
*/
@SuppressWarnings("NotNullFieldNotInitialized") // VersionedParceleble inits this field.
@NonNull
@@ -75,13 +71,11 @@
@ParcelField(4)
public PendingIntent mActionIntent;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP)
@ParcelField(5)
public boolean mEnabled;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP)
@ParcelField(6)
@@ -99,7 +93,6 @@
/**
* Used for VersionedParcelable.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public RemoteActionCompat() {}
diff --git a/core/core/src/main/java/androidx/core/app/RemoteInput.java b/core/core/src/main/java/androidx/core/app/RemoteInput.java
index 1823783..c7a2872 100644
--- a/core/core/src/main/java/androidx/core/app/RemoteInput.java
+++ b/core/core/src/main/java/androidx/core/app/RemoteInput.java
@@ -55,7 +55,6 @@
/** Extra added to a clip data intent object identifying the {@link Source} of the results. */
private static final String EXTRA_RESULTS_SOURCE = "android.remoteinput.resultsSource";
- /** @hide */
@IntDef({SOURCE_FREE_FORM_INPUT, SOURCE_CHOICE})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@@ -67,7 +66,6 @@
/** The user selected one of the choices from {@link #getChoices}. */
public static final int SOURCE_CHOICE = 1;
- /** @hide */
@IntDef(value = {EDIT_CHOICES_BEFORE_SENDING_AUTO, EDIT_CHOICES_BEFORE_SENDING_DISABLED,
EDIT_CHOICES_BEFORE_SENDING_ENABLED})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/app/ServiceCompat.java b/core/core/src/main/java/androidx/core/app/ServiceCompat.java
index e0ef45d..69d1a20 100644
--- a/core/core/src/main/java/androidx/core/app/ServiceCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ServiceCompat.java
@@ -79,7 +79,6 @@
*/
public static final int STOP_FOREGROUND_DETACH = 1<<1;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef(flag = true,
value = {
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 0408fee..2f27b65 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -205,7 +205,6 @@
".DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION";
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef(flag = true, value = {
RECEIVER_VISIBLE_TO_INSTANT_APPS, RECEIVER_EXPORTED, RECEIVER_NOT_EXPORTED,
diff --git a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
index f932e1d..6bee967 100644
--- a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
@@ -57,7 +57,6 @@
/* Hide constructor */
}
- /** @hide */
@RestrictTo(LIBRARY)
public static final String LOG_TAG = "PackageManagerCompat";
@@ -71,7 +70,6 @@
/**
* The status of Unused App Restrictions for this app.
- * @hide
*/
@IntDef({ERROR, FEATURE_NOT_AVAILABLE, DISABLED, API_30_BACKPORT, API_30, API_31})
@Retention(SOURCE)
@@ -194,7 +192,6 @@
/**
* Returns whether any Unused App Restrictions are available on the device.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public static boolean areUnusedAppRestrictionsAvailable(
@@ -213,7 +210,6 @@
* permission revocation. If none exist, this will return {@code null}. Likewise, if multiple
* Verifiers exist, this method will return the first Verifier's package name.
*
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
diff --git a/core/core/src/main/java/androidx/core/content/PermissionChecker.java b/core/core/src/main/java/androidx/core/content/PermissionChecker.java
index fad8c62..12bec8f 100644
--- a/core/core/src/main/java/androidx/core/content/PermissionChecker.java
+++ b/core/core/src/main/java/androidx/core/content/PermissionChecker.java
@@ -67,7 +67,6 @@
/** Permission result: The permission is denied because the app op is not allowed. */
public static final int PERMISSION_DENIED_APP_OP = PackageManager.PERMISSION_DENIED - 1;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({PERMISSION_GRANTED,
PERMISSION_DENIED,
diff --git a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java
index 34dd875..755717e 100644
--- a/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java
+++ b/core/core/src/main/java/androidx/core/content/UnusedAppRestrictionsBackportCallback.java
@@ -34,7 +34,6 @@
private IUnusedAppRestrictionsBackportCallback mCallback;
- /** @hide */
@RestrictTo(LIBRARY)
public UnusedAppRestrictionsBackportCallback(
@NonNull IUnusedAppRestrictionsBackportCallback callback) {
diff --git a/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
index d1679f4..bee25f3 100644
--- a/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
@@ -36,7 +36,6 @@
private PermissionInfoCompat() {
}
- /** @hide */
@IntDef(value = {
PermissionInfo.PROTECTION_NORMAL,
PermissionInfo.PROTECTION_DANGEROUS,
@@ -47,7 +46,6 @@
@Retention(RetentionPolicy.SOURCE)
public @interface Protection {}
- /** @hide */
@SuppressLint("UniqueConstants") // because _SYSTEM and _PRIVILEGED are aliases.
@IntDef(flag = true, value = {
PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
index ac1c1b7..06bab24 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoChangeListener.java
@@ -28,7 +28,6 @@
* Defines a listener for {@link ShortcutInfoCompat} changes in {@link ShortcutManagerCompat}. This
* class is no-op as is and may be overridden to provide the required functionality.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public abstract class ShortcutInfoChangeListener {
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
index 33bc87c..b5b9106 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -66,7 +66,6 @@
private static final String EXTRA_SLICE_URI = "extraSliceUri";
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@IntDef(flag = true, value = {SURFACE_LAUNCHER})
@Retention(RetentionPolicy.SOURCE)
@@ -170,7 +169,6 @@
}
/**
- * @hide
*/
@RequiresApi(22)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -340,7 +338,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public IconCompat getIcon() {
@@ -348,7 +345,6 @@
}
/**
- * @hide
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -369,7 +365,6 @@
}
/**
- * @hide
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -382,7 +377,6 @@
}
/**
- * @hide
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -403,7 +397,6 @@
/**
* Get additional extras from the shortcut, which will not be persisted anywhere once the
* shortcut is published.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
@@ -528,7 +521,6 @@
}
/**
- * @hide
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -557,7 +549,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public Builder(@NonNull ShortcutInfoCompat shortcutInfo) {
@@ -598,7 +589,6 @@
}
/**
- * @hide
*/
@RequiresApi(25)
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -882,7 +872,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java
index caac3a7..a2aa00f 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutInfoCompatSaver.java
@@ -30,7 +30,6 @@
* Defines APIs to access and update a persistable list of {@link ShortcutInfoCompat}. This class
* is no-op as is and may be overridden to provide the required functionality.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public abstract class ShortcutInfoCompatSaver<T> {
@@ -51,7 +50,6 @@
/**
* Implementation that does nothing and returns null from asynchronous methods.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public static class NoopImpl extends ShortcutInfoCompatSaver<Void> {
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
index 1034b61..ea50f63 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
@@ -91,7 +91,6 @@
*/
public static final int FLAG_MATCH_CACHED = 1 << 3;
- /** @hide */
@RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
@IntDef(flag = true, value = {
FLAG_MATCH_MANIFEST,
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
index 5b1e2ef..82eeb8f 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
@@ -43,7 +43,6 @@
/**
* Parses information of static shortcuts from shortcuts.xml
- * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class ShortcutXmlParser {
diff --git a/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java b/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
index dbf7d45..6ab7be4 100644
--- a/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
@@ -45,7 +45,6 @@
import java.io.IOException;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class ColorStateListInflaterCompat {
diff --git a/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java b/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java
index 0dcd6b7..ed22996 100644
--- a/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ComplexColorCompat.java
@@ -48,7 +48,6 @@
* <li>A simple color represented by an {@code int}</li>
* </ol>
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class ComplexColorCompat {
diff --git a/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java b/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java
index df40cb7..ffece79 100644
--- a/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/FontResourcesParserCompat.java
@@ -49,7 +49,6 @@
/**
* Parser for xml type font resources.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class FontResourcesParserCompat {
@@ -82,7 +81,6 @@
private final @FetchStrategy int mStrategy;
private final @Nullable String mSystemFontFamilyName;
- /** @hide */
@RestrictTo(LIBRARY)
public ProviderResourceEntry(@NonNull FontRequest request, @FetchStrategy int strategy,
int timeoutMs, @Nullable String systemFontFamilyName) {
@@ -109,7 +107,6 @@
return mTimeoutMs;
}
- /** @hide */
@RestrictTo(LIBRARY)
public @Nullable String getSystemFontFamilyName() {
return mSystemFontFamilyName;
diff --git a/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java b/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java
index eaa38ac..37fcafd 100644
--- a/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/GradientColorInflaterCompat.java
@@ -49,7 +49,6 @@
import java.util.List;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
final class GradientColorInflaterCompat {
diff --git a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
index 31a2622..c8a8317 100644
--- a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
@@ -470,7 +470,6 @@
* Call {@link #onFontRetrieved(Typeface)} on the handler given, or the Ui Thread if it is
* null.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final void callbackSuccessAsync(final @NonNull Typeface typeface,
@@ -482,7 +481,6 @@
* Call {@link #onFontRetrievalFailed(int)} on the handler given, or the Ui Thread if it is
* null.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final void callbackFailAsync(
@@ -490,7 +488,6 @@
getHandler(handler).post(() -> onFontRetrievalFailed(reason));
}
- /** @hide */
@RestrictTo(LIBRARY)
@NonNull
public static Handler getHandler(@Nullable Handler handler) {
@@ -533,7 +530,6 @@
/**
* Used by TintTypedArray.
*
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java b/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java
index edacea2..4ce9005 100644
--- a/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java
+++ b/core/core/src/main/java/androidx/core/content/res/TypedArrayUtils.java
@@ -43,7 +43,6 @@
* For example, if an private attribute named "abcdefg" in Kitkat has the
* same id value as "android:pathData" in Lollipop, we need to match the attribute's namefirst.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class TypedArrayUtils {
diff --git a/core/core/src/main/java/androidx/core/graphics/Insets.java b/core/core/src/main/java/androidx/core/graphics/Insets.java
index 3387d00..35a5df7 100644
--- a/core/core/src/main/java/androidx/core/graphics/Insets.java
+++ b/core/core/src/main/java/androidx/core/graphics/Insets.java
@@ -171,7 +171,6 @@
}
/**
- * @hide
* @deprecated Use {@link #toCompatInsets(android.graphics.Insets)} instead.
*/
@RequiresApi(api = 29)
diff --git a/core/core/src/main/java/androidx/core/graphics/PathParser.java b/core/core/src/main/java/androidx/core/graphics/PathParser.java
index 2dc2b09..be86323 100644
--- a/core/core/src/main/java/androidx/core/graphics/PathParser.java
+++ b/core/core/src/main/java/androidx/core/graphics/PathParser.java
@@ -21,6 +21,7 @@
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import java.util.ArrayList;
@@ -28,8 +29,8 @@
* This class is a duplicate from the PathParser.java of frameworks/base, with slight
* update on incompatible API like copyOfRange().
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
// TODO: Make this class public
public class PathParser {
private static final String LOGTAG = "PathParser";
@@ -157,7 +158,6 @@
*
* @param target The target path represented in an array of PathDataNode
* @param source The source path represented in an array of PathDataNode
- * @hide
*/
public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
for (int i = 0; i < source.length; i++) {
@@ -340,12 +340,10 @@
public static class PathDataNode {
/**
- * @hide
*/
public char mType;
/**
- * @hide
*/
public float[] mParams;
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
index 89ec82d..63bc7fb 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompat.java
@@ -74,7 +74,6 @@
* Find from internal cache.
*
* @return null if not found.
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -87,7 +86,6 @@
* Find from internal cache.
*
* @return null if not found.
- * @hide
* @deprecated Use {@link #findFromCache(Resources, int, String, int, int)} method
*/
@Nullable
@@ -138,7 +136,6 @@
* Create Typeface from XML resource which root node is font-family.
*
* @return null if failed to create.
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -195,7 +192,6 @@
* Create Typeface from XML resource which root node is font-family.
*
* @return null if failed to create.
- * @hide
* @deprecated Use {@link #createFromResourcesFamilyXml(Context, FamilyResourceEntry,
* Resources, int, String, int, int, ResourcesCompat.FontCallback, Handler, boolean)} method
*/
@@ -213,7 +209,6 @@
/**
* Used by Resources to load a font resource of type font file.
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -231,7 +226,6 @@
/**
* Used by Resources to load a font resource of type font file.
- * @hide
* @deprecated Use {@link #createFromResourcesFontFile(Context, Resources, int, String,
* int, int)} method
*/
@@ -246,7 +240,6 @@
/**
* Create a Typeface from a given FontInfo list and a map that matches them to ByteBuffers.
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -346,7 +339,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@VisibleForTesting
@@ -361,7 +353,6 @@
* RestrictTo(LIBRARY) since it is used by the deprecated
* {@link FontsContractCompat#getFontSync} function.
*
- * @hide
*/
@RestrictTo(LIBRARY)
public static class ResourcesCallbackAdapter extends FontsContractCompat.FontRequestCallback {
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java
index e0c0aaa..0a7a7bb 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi21Impl.java
@@ -47,7 +47,6 @@
/**
* Implementation of the Typeface compat methods for API 21 and above.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(21)
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java
index 12f80a3..7e1b656 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi24Impl.java
@@ -44,7 +44,6 @@
/**
* Implementation of the Typeface compat methods for API 24 and above.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(24)
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java
index 3db83ea..b1353688 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi26Impl.java
@@ -47,7 +47,6 @@
/**
* Implementation of the Typeface compat methods for API 26 and above.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(26)
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java
index 3e3d503..18f4d17 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi28Impl.java
@@ -31,7 +31,6 @@
/**
* Implementation of the Typeface compat methods for API 28 and above.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(28)
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java
index 10123ef..fbd2e17 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatApi29Impl.java
@@ -38,7 +38,6 @@
import java.io.IOException;
import java.io.InputStream;
-/** @hide */
@RestrictTo(LIBRARY_GROUP)
@RequiresApi(29)
public class TypefaceCompatApi29Impl extends TypefaceCompatBaseImpl {
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java
index 2e01d4d..2ed7eb3 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatBaseImpl.java
@@ -39,7 +39,6 @@
/**
* Implementation of the Typeface compat methods for API 14 and above.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
class TypefaceCompatBaseImpl {
diff --git a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
index 85c9e3c..4c74fd1 100644
--- a/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
+++ b/core/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
@@ -51,7 +51,6 @@
/**
* Utility methods for TypefaceCompat.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class TypefaceCompatUtil {
@@ -208,7 +207,6 @@
* {@link FontsContractCompat.FontInfo}.
* @param fonts An array of {@link FontsContractCompat.FontInfo}.
* @return A map from {@link Uri} to {@link ByteBuffer}.
- * @hide
*/
@RestrictTo(LIBRARY)
@NonNull
diff --git a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java
index 2af57d25..90298ca 100644
--- a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java
+++ b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi14.java
@@ -35,7 +35,6 @@
/**
* Helper for creating {@link Typeface}s with exact weight on API 14-20.
* May be used on newer platforms as a fallback method in case private API isn't available.
- * @hide
*/
@RestrictTo(LIBRARY)
final class WeightTypefaceApi14 {
diff --git a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java
index eb5ad22..b07f0ae 100644
--- a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java
+++ b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi21.java
@@ -37,7 +37,6 @@
/**
* Helper for creating {@link Typeface}s with exact weight on API 21-25.
- * @hide
*/
@SuppressLint("SoonBlockedPrivateApi")
@RestrictTo(LIBRARY)
diff --git a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java
index 4a98eda..2a8d51f 100644
--- a/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java
+++ b/core/core/src/main/java/androidx/core/graphics/WeightTypefaceApi26.java
@@ -37,7 +37,6 @@
/**
* Helper for creating {@link Typeface}s with exact weight on API 26-27.
- * @hide
*/
@SuppressLint("SoonBlockedPrivateApi")
@RestrictTo(LIBRARY)
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index 8a8385c..c31083f 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -117,7 +117,6 @@
public static final int TYPE_URI_ADAPTIVE_BITMAP = Icon.TYPE_URI_ADAPTIVE_BITMAP;
/**
- * @hide
*/
@RestrictTo(LIBRARY)
@IntDef({TYPE_UNKNOWN, TYPE_BITMAP, TYPE_RESOURCE, TYPE_DATA, TYPE_URI, TYPE_ADAPTIVE_BITMAP,
@@ -152,7 +151,6 @@
static final String EXTRA_STRING1 = "string1";
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ParcelField(value = 1,
@@ -171,14 +169,12 @@
Object mObj1;
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@ParcelField(value = 2, defaultValue = "null")
public byte[] mData = null;
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -188,7 +184,6 @@
// TYPE_RESOURCE: resId
// TYPE_DATA: data offset
/**
- * @hide
*/
@RestrictTo(LIBRARY)
@ParcelField(value = 4, defaultValue = "0")
@@ -196,14 +191,12 @@
// TYPE_DATA: data length
/**
- * @hide
*/
@RestrictTo(LIBRARY)
@ParcelField(value = 5, defaultValue = "0")
public int mInt2 = 0;
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -214,7 +207,6 @@
@NonParcelField
PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -222,7 +214,6 @@
public String mTintModeStr = null;
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY)
@@ -243,7 +234,6 @@
}
/**
- * @hide
*/
@NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -370,7 +360,6 @@
/**
* Used for VersionedParcelable.
- * @hide
*/
@RestrictTo(LIBRARY)
public IconCompat() {
@@ -446,7 +435,6 @@
* Note: This bitmap may not be available in the future, and it is
* up to the caller to ensure safety if this bitmap is re-used and/or persisted.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
@@ -546,7 +534,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void checkResource(@NonNull Context context) {
@@ -658,7 +645,6 @@
/**
* Create an input stream for bitmap by resolving corresponding content uri.
*
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP)
@@ -706,7 +692,6 @@
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@SuppressWarnings("deprecation")
@@ -988,7 +973,6 @@
/**
* Creates an IconCompat from an Icon.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(23)
@@ -1000,7 +984,6 @@
/**
* Creates an IconCompat from an Icon, or returns null if the given Icon is created from
* resource 0.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(23)
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/TintAwareDrawable.java b/core/core/src/main/java/androidx/core/graphics/drawable/TintAwareDrawable.java
index 4cc224f..8d0a505 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/TintAwareDrawable.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/TintAwareDrawable.java
@@ -28,7 +28,6 @@
* Interface which allows a {@link android.graphics.drawable.Drawable} to receive tinting calls
* from {@code DrawableCompat}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface TintAwareDrawable {
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawable.java b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawable.java
index 270264b..23e79b5 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawable.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/WrappedDrawable.java
@@ -26,7 +26,6 @@
* Interface which allows a {@link android.graphics.drawable.Drawable} to get/set wrapped
* drawables from {@code DrawableCompat}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface WrappedDrawable {
diff --git a/core/core/src/main/java/androidx/core/internal/package-info.java b/core/core/src/main/java/androidx/core/internal/package-info.java
index 743237e..65a385c 100644
--- a/core/core/src/main/java/androidx/core/internal/package-info.java
+++ b/core/core/src/main/java/androidx/core/internal/package-info.java
@@ -1,5 +1,4 @@
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
package androidx.core.internal;
diff --git a/core/core/src/main/java/androidx/core/internal/view/SupportMenu.java b/core/core/src/main/java/androidx/core/internal/view/SupportMenu.java
index b3f70a0..041cc1b 100644
--- a/core/core/src/main/java/androidx/core/internal/view/SupportMenu.java
+++ b/core/core/src/main/java/androidx/core/internal/view/SupportMenu.java
@@ -29,7 +29,6 @@
* elements added in later versions of the framework, are available for all platforms.
*
* @see android.view.Menu
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface SupportMenu extends android.view.Menu {
diff --git a/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java b/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java
index 7b03a35..54c0454 100644
--- a/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java
+++ b/core/core/src/main/java/androidx/core/internal/view/SupportMenuItem.java
@@ -36,7 +36,6 @@
* elements added in later versions of the framework, are available for all platforms.
*
* @see android.view.MenuItem
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface SupportMenuItem extends android.view.MenuItem {
diff --git a/core/core/src/main/java/androidx/core/internal/view/SupportSubMenu.java b/core/core/src/main/java/androidx/core/internal/view/SupportSubMenu.java
index fb8cb46..5b6d378c 100644
--- a/core/core/src/main/java/androidx/core/internal/view/SupportSubMenu.java
+++ b/core/core/src/main/java/androidx/core/internal/view/SupportSubMenu.java
@@ -27,7 +27,6 @@
* elements added in later versions of the framework, are available for all platforms.
*
* @see android.view.SubMenu
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface SupportSubMenu extends SupportMenu, android.view.SubMenu {
diff --git a/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java b/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java
index 4469eac..a547b44 100644
--- a/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java
+++ b/core/core/src/main/java/androidx/core/location/GnssStatusCompat.java
@@ -74,7 +74,6 @@
public static final int CONSTELLATION_IRNSS = GnssStatus.CONSTELLATION_IRNSS;
/**
- * @hide
*/
@RestrictTo(LIBRARY)
@Retention(RetentionPolicy.SOURCE)
diff --git a/core/core/src/main/java/androidx/core/location/GnssStatusWrapper.java b/core/core/src/main/java/androidx/core/location/GnssStatusWrapper.java
index fc59d46..86454f9 100644
--- a/core/core/src/main/java/androidx/core/location/GnssStatusWrapper.java
+++ b/core/core/src/main/java/androidx/core/location/GnssStatusWrapper.java
@@ -27,7 +27,6 @@
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
-/** @hide */
@RestrictTo(LIBRARY)
@RequiresApi(24)
class GnssStatusWrapper extends GnssStatusCompat {
diff --git a/core/core/src/main/java/androidx/core/location/GpsStatusWrapper.java b/core/core/src/main/java/androidx/core/location/GpsStatusWrapper.java
index 3584d6f..55d10da 100644
--- a/core/core/src/main/java/androidx/core/location/GpsStatusWrapper.java
+++ b/core/core/src/main/java/androidx/core/location/GpsStatusWrapper.java
@@ -29,7 +29,6 @@
import java.util.Iterator;
-/** @hide */
@RestrictTo(LIBRARY)
class GpsStatusWrapper extends GnssStatusCompat {
diff --git a/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java b/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
index 3837850..1f6eba5 100644
--- a/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
@@ -53,7 +53,6 @@
*/
public static final long PASSIVE_INTERVAL = LocationRequest.PASSIVE_INTERVAL;
- /** @hide */
@RestrictTo(LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({QUALITY_LOW_POWER, QUALITY_BALANCED_POWER_ACCURACY, QUALITY_HIGH_ACCURACY})
diff --git a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
index 47c9dca..2d29936 100644
--- a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
@@ -51,7 +51,6 @@
*/
@SuppressWarnings("unused")
public final class ConnectivityManagerCompat {
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
diff --git a/core/core/src/main/java/androidx/core/provider/FontRequest.java b/core/core/src/main/java/androidx/core/provider/FontRequest.java
index 31ffd70..5b393d4 100644
--- a/core/core/src/main/java/androidx/core/provider/FontRequest.java
+++ b/core/core/src/main/java/androidx/core/provider/FontRequest.java
@@ -149,7 +149,6 @@
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
*
- * @hide
*/
@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -157,7 +156,6 @@
return mIdentifier;
}
- /** @hide */
@RestrictTo(LIBRARY)
@NonNull
String getId() {
diff --git a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
index 56d92bc..a1c9161 100644
--- a/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
+++ b/core/core/src/main/java/androidx/core/provider/FontsContractCompat.java
@@ -147,7 +147,6 @@
* @return the resulting Typeface if the requested font is in the cache or the request is a
* sync request.
*
- * @hide
*/
@RestrictTo(LIBRARY)
@Nullable
@@ -278,7 +277,6 @@
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
*
- * @hide
*/
// TODO after removing from public API make package private.
@Deprecated
@@ -370,7 +368,6 @@
*/
public static final int STATUS_UNEXPECTED_DATA_PROVIDED = 2;
- /** @hide */
@RestrictTo(LIBRARY)
@IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED})
@Retention(RetentionPolicy.SOURCE)
@@ -382,7 +379,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
**/
// TODO after removing from public API make package private.
@Deprecated
@@ -415,7 +411,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
*/
@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -465,7 +460,6 @@
*/
public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
- /** @hide */
@SuppressWarnings("deprecation")
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
@@ -506,7 +500,6 @@
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
*
- * @hide
*/
@Deprecated // unused
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -517,7 +510,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
**/
@Deprecated // unused
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -526,7 +518,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
**/
@Deprecated // unused
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -536,7 +527,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
**/
@Deprecated // unused
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -559,7 +549,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
**/
@Deprecated // unused
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -580,7 +569,6 @@
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
*
- * @hide
*/
@Deprecated // unused
@RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -596,7 +584,6 @@
/**
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
**/
@Deprecated // unused
@VisibleForTesting
diff --git a/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java b/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java
index 9ccb661..a653239 100644
--- a/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java
+++ b/core/core/src/main/java/androidx/core/provider/SelfDestructiveThread.java
@@ -40,7 +40,6 @@
*
* @deprecated Not being used by any cross library, and should not be used, internal
* implementation detail.
- * @hide
*/
@Deprecated
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/text/HtmlCompat.java b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
index b781ef6..7ae8f24 100644
--- a/core/core/src/main/java/androidx/core/text/HtmlCompat.java
+++ b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
@@ -112,7 +112,6 @@
*/
public static final int FROM_HTML_MODE_COMPACT = Html.FROM_HTML_MODE_COMPACT;
- /** @hide */
@IntDef(value = {
FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH,
FROM_HTML_SEPARATOR_LINE_BREAK_HEADING,
@@ -129,7 +128,6 @@
@interface FromHtmlFlags {
}
- /** @hide */
@IntDef({
TO_HTML_PARAGRAPH_LINES_CONSECUTIVE,
TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
diff --git a/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java b/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
index 613bd1f..af9dd42 100644
--- a/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
+++ b/core/core/src/main/java/androidx/core/text/PrecomputedTextCompat.java
@@ -268,7 +268,6 @@
/**
* Similar to equals but don't compare text direction
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public boolean equalsWithoutTextDirection(@NonNull Params other) {
@@ -504,7 +503,6 @@
/**
* Returns the underlying original text if the text is PrecomputedText.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(28)
diff --git a/core/core/src/main/java/androidx/core/text/util/FindAddress.java b/core/core/src/main/java/androidx/core/text/util/FindAddress.java
index e59fc91..3c00da1 100644
--- a/core/core/src/main/java/androidx/core/text/util/FindAddress.java
+++ b/core/core/src/main/java/androidx/core/text/util/FindAddress.java
@@ -30,7 +30,6 @@
* Support copy of https://cs.chromium.org/chromium/src/android_webview/java/src/org/chromium
* /android_webview/FindAddress.java
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
class FindAddress {
diff --git a/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java b/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java
index 2f7e4f3..24368ba 100644
--- a/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java
+++ b/core/core/src/main/java/androidx/core/text/util/LinkifyCompat.java
@@ -69,7 +69,6 @@
return Integer.compare(b.end, a.end);
};
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef(flag = true, value = { Linkify.WEB_URLS, Linkify.EMAIL_ADDRESSES, Linkify.PHONE_NUMBERS,
Linkify.MAP_ADDRESSES, Linkify.ALL })
diff --git a/core/core/src/main/java/androidx/core/util/DebugUtils.java b/core/core/src/main/java/androidx/core/util/DebugUtils.java
index 7b0d9be..0b9d2bd 100644
--- a/core/core/src/main/java/androidx/core/util/DebugUtils.java
+++ b/core/core/src/main/java/androidx/core/util/DebugUtils.java
@@ -23,7 +23,6 @@
/**
* Helper for accessing features in {@link android.util.DebugUtils}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class DebugUtils {
diff --git a/core/core/src/main/java/androidx/core/util/LogWriter.java b/core/core/src/main/java/androidx/core/util/LogWriter.java
index 75e2942..4d0196b 100644
--- a/core/core/src/main/java/androidx/core/util/LogWriter.java
+++ b/core/core/src/main/java/androidx/core/util/LogWriter.java
@@ -25,7 +25,6 @@
import java.io.Writer;
/**
- * @hide
* @deprecated Copied to use sites. Do not use.
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/util/PatternsCompat.java b/core/core/src/main/java/androidx/core/util/PatternsCompat.java
index 41e0c39..be438ad 100644
--- a/core/core/src/main/java/androidx/core/util/PatternsCompat.java
+++ b/core/core/src/main/java/androidx/core/util/PatternsCompat.java
@@ -297,7 +297,6 @@
* tries to match the URL structure with a relaxed rule for TLDs. If the string does not start
* with http(s):// the TLDs are expected to be one of the known TLDs.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static final Pattern AUTOLINK_WEB_URL = Pattern.compile(
@@ -326,7 +325,6 @@
/**
* Regular expression pattern to match email addresses. It excludes double quoted local parts
* and the special characters #&~!^`{}/=$*?| that are included in RFC5321.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static final Pattern AUTOLINK_EMAIL_ADDRESS = Pattern.compile("(" + WORD_BOUNDARY +
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index 3461d12..a975b9e 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -29,7 +29,6 @@
* Simple static methods to be called at the start of your own methods to verify
* correct arguments and state.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public final class Preconditions {
diff --git a/core/core/src/main/java/androidx/core/util/TimeUtils.java b/core/core/src/main/java/androidx/core/util/TimeUtils.java
index 206d372..f06687f 100644
--- a/core/core/src/main/java/androidx/core/util/TimeUtils.java
+++ b/core/core/src/main/java/androidx/core/util/TimeUtils.java
@@ -25,11 +25,9 @@
/**
* Helper for accessing features in {@link android.util.TimeUtils}.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class TimeUtils {
- /** @hide Field length that can hold 999 days of time */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static final int HUNDRED_DAY_FIELD_LEN = 19;
@@ -148,7 +146,6 @@
return pos + 1;
}
- /** @hide Just for debugging; not internationalized. */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static void formatDuration(long duration, StringBuilder builder) {
synchronized (sFormatSync) {
@@ -157,7 +154,6 @@
}
}
- /** @hide Just for debugging; not internationalized. */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static void formatDuration(long duration, PrintWriter pw, int fieldLen) {
synchronized (sFormatSync) {
@@ -166,13 +162,11 @@
}
}
- /** @hide Just for debugging; not internationalized. */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static void formatDuration(long duration, PrintWriter pw) {
formatDuration(duration, pw, 0);
}
- /** @hide Just for debugging; not internationalized. */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static void formatDuration(long time, long now, PrintWriter pw) {
if (time == 0) {
diff --git a/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java b/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
index 61f55ae..8306bd8 100644
--- a/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
+++ b/core/core/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
@@ -145,7 +145,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public AccessibilityDelegateCompat(@NonNull AccessibilityDelegate originalDelegate) {
diff --git a/core/core/src/main/java/androidx/core/view/ActionProvider.java b/core/core/src/main/java/androidx/core/view/ActionProvider.java
index dff98c6..341375b 100644
--- a/core/core/src/main/java/androidx/core/view/ActionProvider.java
+++ b/core/core/src/main/java/androidx/core/view/ActionProvider.java
@@ -273,7 +273,6 @@
* Notify the system that the visibility of an action view's sub-UI such as an anchored popup
* has changed. This will affect how other system visibility notifications occur.
*
- * @hide Pending future API approval
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void subUiVisibilityChanged(boolean isVisible) {
@@ -283,7 +282,6 @@
}
/**
- * @hide Internal use only
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void setSubUiVisibilityListener(@Nullable SubUiVisibilityListener listener) {
@@ -306,7 +304,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void reset() {
@@ -315,7 +312,6 @@
}
/**
- * @hide Internal use only
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface SubUiVisibilityListener {
diff --git a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
index 5b02d20..e32353e 100644
--- a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
@@ -49,7 +49,6 @@
* Specifies the UI through which content is being inserted. Future versions of Android may
* support additional values.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@IntDef(value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD, SOURCE_DRAG_AND_DROP,
@@ -98,7 +97,6 @@
/**
* Returns the symbolic name of the given source.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@NonNull
@@ -117,7 +115,6 @@
/**
* Flags to configure the insertion behavior.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@IntDef(flag = true, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
@@ -134,7 +131,6 @@
/**
* Returns the symbolic names of the set flags or {@code "0"} if no flags are set.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@NonNull
diff --git a/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java b/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java
index 2e6469b..c4c8e5a 100644
--- a/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/DragAndDropPermissionsCompat.java
@@ -46,7 +46,6 @@
mDragAndDropPermissions = dragAndDropPermissions;
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
public static DragAndDropPermissionsCompat request(@NonNull Activity activity,
diff --git a/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java b/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java
index 0126ce2..5605d46 100644
--- a/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java
+++ b/core/core/src/main/java/androidx/core/view/KeyEventDispatcher.java
@@ -39,7 +39,6 @@
* To use this, implement {@link Component} and call the dispatch methods at appropriate times.
*
* This must be used for some core compatibility features to function fully.
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class KeyEventDispatcher {
diff --git a/core/core/src/main/java/androidx/core/view/PointerIconCompat.java b/core/core/src/main/java/androidx/core/view/PointerIconCompat.java
index 5fb42bd..f47836b 100644
--- a/core/core/src/main/java/androidx/core/view/PointerIconCompat.java
+++ b/core/core/src/main/java/androidx/core/view/PointerIconCompat.java
@@ -112,7 +112,6 @@
}
/**
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/core/core/src/main/java/androidx/core/view/ViewCompat.java b/core/core/src/main/java/androidx/core/view/ViewCompat.java
index c135593..856e06d 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -107,20 +107,17 @@
public class ViewCompat {
private static final String TAG = "ViewCompat";
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({View.FOCUS_LEFT, View.FOCUS_UP, View.FOCUS_RIGHT, View.FOCUS_DOWN,
View.FOCUS_FORWARD, View.FOCUS_BACKWARD})
@Retention(RetentionPolicy.SOURCE)
public @interface FocusDirection {}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({View.FOCUS_LEFT, View.FOCUS_UP, View.FOCUS_RIGHT, View.FOCUS_DOWN})
@Retention(RetentionPolicy.SOURCE)
public @interface FocusRealDirection {}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({View.FOCUS_FORWARD, View.FOCUS_BACKWARD})
@Retention(RetentionPolicy.SOURCE)
@@ -405,7 +402,6 @@
public static final int MEASURED_STATE_TOO_SMALL = 0x01000000;
/**
- * @hide
*/
@IntDef(value = {SCROLL_AXIS_NONE, SCROLL_AXIS_HORIZONTAL, SCROLL_AXIS_VERTICAL}, flag = true)
@Retention(RetentionPolicy.SOURCE)
@@ -428,7 +424,6 @@
public static final int SCROLL_AXIS_VERTICAL = 1 << 1;
/**
- * @hide
*/
@IntDef({TYPE_TOUCH, TYPE_NON_TOUCH})
@Retention(RetentionPolicy.SOURCE)
@@ -446,7 +441,6 @@
*/
public static final int TYPE_NON_TOUCH = 1;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java
index f50849a..aff92af 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsAnimationCompat.java
@@ -351,7 +351,6 @@
public static final int DISPATCH_MODE_CONTINUE_ON_SUBTREE = 1;
WindowInsets mDispachedInsets;
- /** @hide */
@IntDef(value = {
DISPATCH_MODE_STOP,
DISPATCH_MODE_CONTINUE_ON_SUBTREE
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
index e15e1dec..8f1c28b 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
@@ -2013,7 +2013,6 @@
/**
* @return All inset types combined.
- * @hide
*/
@InsetsType
@RestrictTo(LIBRARY_GROUP)
@@ -2048,7 +2047,6 @@
}
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {STATUS_BARS, NAVIGATION_BARS, CAPTION_BAR, IME, WINDOW_DECOR,
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
index bff7344..733521d 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
@@ -152,7 +152,6 @@
/**
* Determines the behavior of system bars when hiding them by calling {@link #hide}.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java
index a0c5997..543b36a 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java
@@ -40,7 +40,6 @@
private final int mClickableSpanActionId;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static final String SPAN_ID = "ACCESSIBILITY_CLICKABLE_SPAN_ID";
@@ -48,7 +47,6 @@
/**
* @param originalClickableSpanId The id of the span this one replaces
* @param nodeInfoCompat The nodeInfoCompat to be associated with this span.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public AccessibilityClickableSpanCompat(int originalClickableSpanId,
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index 7b38d7d..3157094 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -282,7 +282,6 @@
*/
public static final int TYPES_ALL_MASK = 0xFFFFFFFF;
- /** @hide */
@IntDef(
flag = true,
value = {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 7272616..768e166 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -659,7 +659,6 @@
private final Class<? extends CommandArguments> mViewCommandArgumentClass;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected final AccessibilityViewCommand mCommand;
@@ -680,7 +679,6 @@
* @param actionId The action id.
* @param label The action label.
* @param command The command performed when the service requests the action
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public AccessibilityActionCompat(int actionId, CharSequence label,
@@ -742,7 +740,6 @@
* @return If the action was handled.
* @param view View to act upon.
* @param arguments Optional action arguments.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public boolean perform(View view, Bundle arguments) {
@@ -766,7 +763,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public AccessibilityActionCompat createReplacementAction(CharSequence label,
@@ -1330,8 +1326,6 @@
/**
* androidx.customview.widget.ExploreByTouchHelper.HOST_ID = -1;
- *
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public int mParentVirtualDescendantId = NO_ID;
@@ -2891,7 +2885,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void addSpansToExtras(CharSequence text, View view) {
@@ -2928,7 +2921,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static ClickableSpan[] getClickableSpans(CharSequence text) {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java
index 9890290..b020b83 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityViewCommand.java
@@ -47,7 +47,6 @@
Bundle mBundle;
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void setBundle(@Nullable Bundle bundle) {
diff --git a/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java b/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java
index 034a660..1af86ae 100644
--- a/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java
+++ b/core/core/src/main/java/androidx/core/widget/AutoSizeableTextView.java
@@ -33,12 +33,10 @@
* implies that AppCompat shadows the platform's auto-size attributes. See
* {@link androidx.resourceinspection.processor} for more details and a full mapping of attributes.
*
- * @hide Internal use only
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface AutoSizeableTextView {
/**
- * @hide
* @deprecated do not use, overlarge scope and missing annotations
*/
@Deprecated
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index 279e003..dd19e2d 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -124,13 +124,11 @@
private final Rect mTempRect = new Rect();
private OverScroller mScroller;
- /** @hide */
@RestrictTo(LIBRARY)
@VisibleForTesting
@NonNull
public EdgeEffect mEdgeGlowTop;
- /** @hide */
@RestrictTo(LIBRARY)
@VisibleForTesting
@NonNull
@@ -1827,7 +1825,6 @@
/**
* <p>The scroll range of a scroll view is the overall height of all of its
* children.</p>
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -1852,35 +1849,30 @@
return scrollRange;
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
public int computeVerticalScrollOffset() {
return Math.max(0, super.computeVerticalScrollOffset());
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
public int computeVerticalScrollExtent() {
return super.computeVerticalScrollExtent();
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
public int computeHorizontalScrollRange() {
return super.computeHorizontalScrollRange();
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
public int computeHorizontalScrollOffset() {
return super.computeHorizontalScrollOffset();
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
public int computeHorizontalScrollExtent() {
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 02c7d6a..915d0af2 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -92,7 +92,6 @@
*/
public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
@Retention(RetentionPolicy.SOURCE)
@@ -514,7 +513,6 @@
/**
* @see #setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
@@ -537,7 +535,6 @@
/**
* @see #setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
index 336fcc3..50a82bd 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
@@ -43,7 +43,6 @@
* Default implementation inserting content into editable {@link TextView} components. This class
* handles insertion of text (plain text, styled text, HTML, etc) but not images or other content.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class TextViewOnReceiveContentListener implements OnReceiveContentListener {
diff --git a/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java b/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
index fe28197..83c0c60 100644
--- a/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
+++ b/core/core/src/main/java/androidx/core/widget/TintableCheckedTextView.java
@@ -34,7 +34,6 @@
* implies that AppCompat shadows the platform's check mark tint attributes. See
* {@link androidx.resourceinspection.processor} for more details and a full mapping of attributes.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface TintableCheckedTextView {
diff --git a/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java b/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java
index 6adbb4c..abda77b 100644
--- a/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java
+++ b/core/core/src/main/java/androidx/core/widget/TintableImageSourceView.java
@@ -33,7 +33,6 @@
* implies that AppCompat shadows the platform's image tint attributes. See
* {@link androidx.resourceinspection.processor} for more details and a full mapping of attributes.
*
- * @hide Internal use only
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface TintableImageSourceView {
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
index b946a89..28520c0 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
@@ -23,11 +23,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.RangingSessionCallback
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"MutableBareField", "ParcelNotFinal", "CallbackMethodName"})
public interface IRangingSessionCallback extends android.os.IInterface
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
index 5008f91..736e2f8 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
@@ -23,10 +23,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
- /**
- * @hide
+/**
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"MutableBareField", "ParcelNotFinal"})
public interface IUwb extends android.os.IInterface
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
index d19d1eb..6319af2 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
@@ -23,11 +23,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.UwbClient
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"MutableBareField", "ParcelNotFinal", "ExecutorRegistration"})
public interface IUwbClient extends android.os.IInterface
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
index 6da98f1..f53e525 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
@@ -21,11 +21,12 @@
import android.os.Parcel;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.RangingCapabilities
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class RangingCapabilities implements android.os.Parcelable
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
index 5b02460..e27e913 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
@@ -21,11 +21,12 @@
import android.os.Parcel;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.RangingMeasurement
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class RangingMeasurement implements android.os.Parcelable
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
index 966830c..52d466c 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
@@ -22,11 +22,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.RangingParameters
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class RangingParameters implements android.os.Parcelable
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
index 1173b81..afd75ca 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
@@ -22,11 +22,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.RangingPosition
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class RangingPosition implements android.os.Parcelable
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
index e076cd1..dc33256 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
@@ -22,11 +22,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.UwbAddress
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class UwbAddress implements android.os.Parcelable
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
index 24d001a..ec8f41e 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
@@ -21,11 +21,12 @@
import android.os.Parcel;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.UwbComplexChannel
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class UwbComplexChannel implements android.os.Parcelable
{
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
index 054aad7..834ed923 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
@@ -22,11 +22,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
/** Gms Reference: com.google.android.gms.nearby.uwb.UwbDevice
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
public class UwbDevice implements android.os.Parcelable
{
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
index dfcb546..8b39052 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
@@ -20,6 +20,8 @@
import android.util.Log
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.exceptions.CreateCredentialCancellationException
+import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.domerrors.AbortError
import androidx.credentials.exceptions.domerrors.ConstraintError
import androidx.credentials.exceptions.domerrors.DataError
@@ -33,7 +35,6 @@
import androidx.credentials.exceptions.domerrors.TimeoutError
import androidx.credentials.exceptions.domerrors.UnknownError
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
-import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException
import com.google.android.gms.auth.api.identity.BeginSignInRequest
import com.google.android.gms.auth.api.identity.SignInCredential
import com.google.android.gms.fido.common.Transport
@@ -261,18 +262,30 @@
*/
fun publicKeyCredentialResponseContainsError(
cred: PublicKeyCredential
- ): CreatePublicKeyCredentialException? {
+ ): CreateCredentialException? {
val authenticatorResponse: AuthenticatorResponse = cred.response
if (authenticatorResponse is AuthenticatorErrorResponse) {
val code = authenticatorResponse.errorCode
var exceptionError = orderedErrorCodeToExceptions[code]
var msg = authenticatorResponse.errorMessage
- val exception: CreatePublicKeyCredentialDomException
+ val exception: CreateCredentialException
if (exceptionError == null) {
exception = CreatePublicKeyCredentialDomException(
UnknownError(), "unknown fido gms exception - $msg"
)
- } else { exception = CreatePublicKeyCredentialDomException(exceptionError, msg) }
+ } else {
+ // This fix is quite fragile because it relies on that the fido module
+ // does not change its error message, but is the only viable solution
+ // because there's no other differentiator.
+ if (code == ErrorCode.CONSTRAINT_ERR &&
+ msg?.contains("Unable to get sync account") == true
+ ) {
+ exception = CreateCredentialCancellationException(
+ "Passkey registration was cancelled by the user.")
+ } else {
+ exception = CreatePublicKeyCredentialDomException(exceptionError, msg)
+ }
+ }
return exception
}
return null
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
index e72f298..13bd3d7 100644
--- a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.os.Bundle
import androidx.datastore.core.handlers.NoOpCorruptionHandler
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.test.core.app.ApplicationProvider
import androidx.testing.TestMessageProto.FooProto
import com.google.common.truth.Truth.assertThat
@@ -100,8 +101,6 @@
private lateinit var dataStoreContext: CoroutineContext
private lateinit var dataStoreScope: TestScope
- private val TAG = "MPDS test"
-
private val protoSerializer: Serializer<FooProto> = ProtoSerializer<FooProto>(
FooProto.getDefaultInstance(),
ExtensionRegistryLite.getEmptyRegistry()
@@ -114,7 +113,7 @@
return data
}
- internal fun createDataStore(
+ private fun createDataStore(
bundle: Bundle,
scope: TestScope
): DataStoreImpl<FooProto> {
@@ -162,7 +161,7 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
- it.let { WRITE_TEXT(it) }
+ WRITE_TEXT(it)
}
}
}
@@ -207,7 +206,7 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
waitForSignal()
- it.let { WRITE_BOOLEAN(it) }
+ WRITE_BOOLEAN(it)
}
}
}
@@ -242,7 +241,7 @@
val write = async(newSingleThreadContext("blockedWriter")) {
dataStore.updateData {
condition.await()
- it.let { WRITE_BOOLEAN(it) }
+ WRITE_BOOLEAN(it)
}
}
@@ -269,7 +268,7 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
waitForSignal()
- it.let { WRITE_TEXT(it) }
+ WRITE_TEXT(it)
}
}
}
@@ -331,14 +330,14 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
- it.let { INCREMENT_INTEGER(it) }
+ INCREMENT_INTEGER(it)
}
waitForSignal()
val write = async {
store.updateData {
- it.let { WRITE_BOOLEAN(it) }
+ WRITE_BOOLEAN(it)
}
}
waitForSignal()
@@ -391,7 +390,7 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
- it.let { WRITE_TEXT(it) }
+ WRITE_TEXT(it)
}
}
}
@@ -412,7 +411,7 @@
val write = localScope.async {
dataStore.updateData {
blockWrite.await()
- it.let { WRITE_BOOLEAN(it) }
+ WRITE_BOOLEAN(it)
}
}
@@ -443,7 +442,7 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
- it.let { WRITE_TEXT(it) }
+ WRITE_TEXT(it)
}
}
}
@@ -490,23 +489,8 @@
override fun runTest() = runBlocking<Unit> {
store.updateData {
- it.let { WRITE_TEXT(it) }
+ WRITE_TEXT(it)
}
}
}
-
- /**
- * A corruption handler that attempts to replace the on-disk data with data from produceNewData.
- *
- * TODO(zhiyuanwang): replace with androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
- */
- private class ReplaceFileCorruptionHandler<T>(
- private val produceNewData: (CorruptionException) -> T
- ) : CorruptionHandler<T> {
-
- @Throws(IOException::class)
- override suspend fun handleCorruption(ex: CorruptionException): T {
- return produceNewData(ex)
- }
- }
}
\ No newline at end of file
diff --git a/development/update-verification-metadata.sh b/development/update-verification-metadata.sh
index bff975b..90720f2 100755
--- a/development/update-verification-metadata.sh
+++ b/development/update-verification-metadata.sh
@@ -17,7 +17,7 @@
echo "regenerating verification metadata and keyring"
# regenerate metadata
# Need to run a clean build, https://github.com/gradle/gradle/issues/19228
- runGradle --stacktrace --write-verification-metadata pgp,sha256 --export-keys --dry-run --clean bOS
+ runGradle --stacktrace --write-verification-metadata pgp,sha256 --export-keys --dry-run --clean bOS :docs-kmp:zipCombinedKmpDocs
# update verification metadata file
# also remove 'version=' lines, https://github.com/gradle/gradle/issues/20192
diff --git a/development/upload_mac_metrics_to_skia/.gitignore b/development/upload_mac_metrics_to_skia/.gitignore
new file mode 100644
index 0000000..16e340a
--- /dev/null
+++ b/development/upload_mac_metrics_to_skia/.gitignore
@@ -0,0 +1 @@
+download_staging/*
\ No newline at end of file
diff --git a/development/upload_mac_metrics_to_skia/upload_mac_metrics_to_skia.py b/development/upload_mac_metrics_to_skia/upload_mac_metrics_to_skia.py
new file mode 100644
index 0000000..fb9316d
--- /dev/null
+++ b/development/upload_mac_metrics_to_skia/upload_mac_metrics_to_skia.py
@@ -0,0 +1,84 @@
+"""
+Helper script to upload metrics files from a given build ID to skia perf. Must be run on gLinux
+because it relies on the 'fetch_artifact' tool.
+"""
+
+
+import subprocess
+import sys
+import os
+import shutil
+import requests
+import json
+
+FETCH_ARTIFACT = "/google/data/ro/projects/android/fetch_artifact"
+
+
+def fetch_artifacts(build_id, branch, target, output_dir, file_path):
+ subprocess.run(
+ [
+ FETCH_ARTIFACT,
+ "--bid",
+ build_id,
+ "--branch",
+ branch,
+ "--target",
+ target,
+ file_path,
+ ],
+ cwd=output_dir,
+ capture_output=True,
+ check=True
+ )
+
+
+def prep_staging_dir(staging_path):
+ current_dir = os.path.dirname(os.path.realpath(__file__))
+ staging_dir = current_dir + staging_path
+ if os.path.isdir(staging_dir):
+ shutil.rmtree(staging_dir)
+ os.makedirs(staging_dir, exist_ok=True)
+ return staging_dir
+
+
+def post_json_to_endpoint(contents, endpoint):
+ requests.post(endpoint, json=contents)
+
+
+def mutate_metrics(contents, build_id, branch):
+ contents["git_hash"] = build_id
+ contents["key"]["branch"] = branch
+
+
+def upload_metrics(download_staging_dir, build_id, branch, endpoint):
+ for file_name in os.listdir(download_staging_dir):
+ full_path = os.path.join(download_staging_dir, file_name)
+ with open(full_path) as src_file:
+ contents = json.load(src_file)
+ mutate_metrics(contents, build_id, branch)
+ post_json_to_endpoint(contents, endpoint)
+
+
+def main(args):
+ if (len(args) < 2):
+ raise Exception("Please provide a build ID")
+ if (len(args) < 3):
+ raise Exception("Please provide an endpoint")
+ build_id = args[1]
+ endpoint = args[2]
+ branch = "androidx-main"
+ target = "androidx_multiplatform_mac_host_tests"
+ file_path = "librarymetrics/**/*.json"
+ download_staging_dir = prep_staging_dir("/download_staging")
+ fetch_artifacts(
+ build_id,
+ branch,
+ target,
+ download_staging_dir,
+ file_path
+ )
+ upload_metrics(download_staging_dir, build_id, branch, endpoint)
+
+
+if __name__ == "__main__":
+ main(sys.argv)
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 2b5013a..b901079 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -18,6 +18,7 @@
docs(project(":ads:ads-identifier-testing"))
kmpDocs(project(":annotation:annotation"))
docs(project(":annotation:annotation-experimental"))
+ docs(project(":appactions:interaction:interaction-capabilities-communication"))
docs(project(":appactions:interaction:interaction-capabilities-core"))
docs(project(":appactions:interaction:interaction-capabilities-productivity"))
docs(project(":appactions:interaction:interaction-capabilities-safety"))
diff --git a/docs/api_guidelines/compat.md b/docs/api_guidelines/compat.md
index e13a434..147b4b4 100644
--- a/docs/api_guidelines/compat.md
+++ b/docs/api_guidelines/compat.md
@@ -68,19 +68,34 @@
}
```
-##### Preventing invalid casting in verification fixes
+##### Preventing invalid casting {#compat-casting}
Even when a call to a new API is moved to a version-specific class, a class
-verification failure is still possible if the method returns a new type. The new
-type will be seen as `Object` on lower API levels, so casting the returned value
-outside of the version-specific class to anything other than `Object` or the
-same new type will fail.
+verification failure is still possible when referencing types introduced in new
+APIs.
+
+When a type does not exist on a device, the verifier treats the type as
+`Object`. This is a problem if the new type is implicitly cast to a different
+type which does exist on the device.
+
+In general, if `A extends B`, using an `A` as a `B` without an explicit cast is
+fine. However, if `A` was introduced at a later API level than `B`, on devices
+below that API level, `A` will be seen as `Object`. An `Object` cannot be used
+as a `B` without an explicit cast. However, adding an explicit cast to `B` won't
+fix this, because the compiler will see the cast as redundant (as it normally
+would be). So, implicit casts between types introduced at different API levels
+should be moved out to version-specific static inner classes, as described
+[above](#compat-sdk).
+
+The `ImplicitCastClassVerificationFailure` lint check detects and provides
+autofixes for instances of invalid implicit casts.
For instance, the following would **not** be valid, because it implicitly casts
an `AdaptiveIconDrawable` (new in API level 26, `Object` on lower API levels) to
-`Drawable`. Instead, the method inside of `Api26Impl` should return `Drawable`.
+`Drawable`. Instead, the method inside of `Api26Impl` could return `Drawable`,
+or the cast could be moved into a version-specific static inner class.
-```java {.good}
+```java {.bad}
private Drawable methodReturnsDrawable() {
if (Build.VERSION.SDK_INT >= 26) {
// Implicitly casts the returned AdaptiveIconDrawable to Drawable
@@ -96,7 +111,95 @@
@DoNotInline
static AdaptiveIconDrawable createAdaptiveIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) {
return new AdaptiveIconDrawable(backgroundDrawable, foregroundDrawable);
- }
+ }
+}
+```
+
+The version-specific static inner class solution would look like this:
+
+```java {.good}
+private Drawable methodReturnsDrawable() {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return Api26Impl.castToDrawable(Api26Impl.createAdaptiveIconDrawable(null, null));
+ } else {
+ return null;
+ }
+}
+
+@RequiresApi(26)
+static class Api26Impl {
+ // Returns AdaptiveIconDrawable, introduced in API level 26
+ @DoNotInline
+ static AdaptiveIconDrawable createAdaptiveIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) {
+ return new AdaptiveIconDrawable(backgroundDrawable, foregroundDrawable);
+ }
+
+ // Method which performs the implicit cast from AdaptiveIconDrawable to Drawable
+ @DoNotInline
+ static Drawable castToDrawable(AdaptiveIconDrawable adaptiveIconDrawable) {
+ return adaptiveIconDrawable;
+ }
+}
+```
+
+The following would also **not** be valid, because it implicitly casts a
+`Notification.MessagingStyle` (new in API level 24, `Object` on lower API
+levels) to `Notification.Style`. Instead, `Api24Impl` could have a `setBuilder`
+method which takes `Notification.MessagingStyle` as a parameter, or the cast
+could be moved into a version-specific static inner class.
+
+```java {.bad}
+public void methodUsesStyle(Notification.MessagingStyle style, Notification.Builder builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Api16Impl.setBuilder(
+ // Implicitly casts the style to Notification.Style (added in API level 16)
+ // when it is a Notification.MessagingStyle (added in API level 24)
+ style, builder
+ );
+ }
+}
+
+@RequiresApi(16)
+static class Api16Impl {
+ private Api16Impl() { }
+
+ @DoNotInline
+ static void setBuilder(Notification.Style style, Notification.Builder builder) {
+ style.setBuilder(builder);
+ }
+}
+```
+
+The version-specific static inner class solution would look like this:
+
+```java {.good}
+public void methodUsesStyle(Notification.MessagingStyle style, Notification.Builder builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Api16Impl.setBuilder(
+ Api24Impl.castToStyle(style), builder
+ );
+ }
+}
+
+@RequiresApi(16)
+static class Api16Impl {
+ private Api16Impl() { }
+
+ @DoNotInline
+ static void setBuilder(Notification.Style style, Notification.Builder builder) {
+ style.setBuilder(builder);
+ }
+}
+
+@RequiresApi(24)
+static class Api24Impl {
+ private Api24Impl() { }
+
+ // Performs the implicit cast from Notification.MessagingStyle to Notification.Style
+ @DoNotInline
+ static Notification.Style castToStyle(Notification.MessagingStyle messagingStyle) {
+ return messagingStyle;
+ }
}
```
diff --git a/docs/branching.md b/docs/branching.md
index dd23ec5..8f321b7 100644
--- a/docs/branching.md
+++ b/docs/branching.md
@@ -2,7 +2,7 @@
[TOC]
-## Single Development Branch [`aosp/androidx-main`]
+## Single development branch [`aosp/androidx-main`]
All feature development occurs in the public main development branch of the
Android Open Source Project: `androidx-main`. This branch serves as the central
@@ -10,7 +10,7 @@
and `beta` version work -- development, builds, and releases -- will be done
ONLY in this branch.
-## Release Branches [`aosp/androidx-\<feature\>-release`]
+## Release branches [`aosp/androidx-\<feature\>-release`]
Release branches are used for stabilitization of a library and support of a
previous stable release. With one development branch, this is how AndroidX
diff --git a/docs/onboarding.md b/docs/onboarding.md
index 358fdbd..75bfbf7 100644
--- a/docs/onboarding.md
+++ b/docs/onboarding.md
@@ -676,14 +676,17 @@
#### Missing external dependency
-If Gradle cannot resolve a dependency listed in your `build.gradle`, you may
-need to import the corresponding artifact into one of the prebuilts
-repositories. These repositories are located under `prebuilts/androidx`. Our
-workflow does not automatically download artifacts from the internet to
-facilitate reproducible builds even if remote artifacts are changed.
+If Gradle cannot resolve a dependency listed in your `build.gradle`:
-We use a script to download dependencies, you can learn more about it
-[here](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:development/importMaven/README.md).
+* You will probably want to import the missing artifact via
+ [importMaven.sh](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:development/importMaven/README.md)
+
+ * We store artifacts in the prebuilts repositories under
+ `prebuilts/androidx` to facilitate reproducible builds even if remote
+ artifacts are changed.
+
+* You may need to [establish trust for](#dependency-verification) the new
+ artifact
##### Importing dependencies in `libs.versions.toml`
@@ -733,12 +736,10 @@
#### Dependency verification
-If the new dependency you are importing is unsigned, or is signed with a new,
+If you import a new dependency that is either unsigned or is signed with a new,
unrecognized key, then you will need to add new dependency verification metadata
-to indicate to Gradle that this new dependency is trusted. Instructions for how
-to do this are currently in the
-[README](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/gradle/README.md)
-in the development subfolder
+to indicate to Gradle that this new dependency is trusted. See the instructions
+[here](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/gradle/README.md)
#### Updating an existing dependency
diff --git a/docs/principles.md b/docs/principles.md
index a2d2a42..96adce4 100644
--- a/docs/principles.md
+++ b/docs/principles.md
@@ -110,17 +110,11 @@
### 13. Examples of modern development
- Where possible, targeting the latest languages, OS features, and tools. All
- new libraries should be written in Kotlin first. Existing libraries
- implemented in Java should add Kotlin extension libraries to improve the
- interoperability of the Java APIs from Kotlin. New libraries written in Java
- require a significant business reason on why a dependency in Kotlin cannot
- be taken. The following is the order of preference, with each lower tier
- requiring a business reason:
- 1. Implemented in Kotlin that compiles to Java 8 bytecode
- 2. Implemented in Java 8, with `-ktx` extensions for Kotlin
- interoperability
- 3. Implemented in Java 7, with `-ktx` extensions for Kotlin
- interoperability
+ new libraries should be written in Kotlin, compile using the latest stable
+ Android SDK, and assume that clients are using the latest stable versions of
+ Android Studio, Gradle, and AGP. See the
+ [AndroidX API Guidelines](/company/teams/androidx/api_guidelines/index.md#dependencies-kotlin)
+ for more details, including the use of Java sources and `-ktx` artifacts.
### 14. High quality APIs and ownership
diff --git a/docs/testing.md b/docs/testing.md
index 656b760..3b0d40a 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -224,34 +224,49 @@
```shell
# Run instrumentation tests on a connected device
-./gradlew <project-name>:connectedAndroidTest --info --daemon
+./gradlew <project-name>:connectedAndroidTest --info
+
+# Run instrumentation tests in Firebase Test Lab (remote)
+./gradlew <project-name>:ftlnexus4api21
+./gradlew <project-name>:ftlpixel2api26
+./gradlew <project-name>:ftlpixel2api28
+./gradlew <project-name>:ftlpixel2api30
+./gradlew <project-name>:ftlpixel2api33
# Run local unit tests
-./gradlew <project-name>:test --info --daemon
+./gradlew <project-name>:test
```
-substituting the Gradle project name (ex. `core`).
+substituting the Gradle project name (ex. `:core:core`).
-To run all integration tests in the specific project and test class you're
-working on, run
+To run a specific instrumentation test in a given project, run
```shell
-./gradlew <project-name>:connectedAndroidTest --info --daemon \
+# Run instrumentation tests on a connected device
+./gradlew <project-name>:connectedAndroidTest --info \
-Pandroid.testInstrumentationRunnerArguments.class=<fully-qualified-class>[\#testName]
+
+# Run instrumentation tests on in Firebase Test Lab (remote)
+./gradlew <project-name>:ftlpixel2api30 --className=<fully-qualified-class>
```
substituting the Gradle project name (ex. `viewpager`) and fully-qualified class
name (ex. `androidx.viewpager.widget.ViewPagerTest`) of your test file,
optionally followed by `\#testName` if you want to execute a single test in that
-file. Substitute `test` for `connectedAndroidTest` to run local unit tests.
+file
-If you see some weird compilation errors such as below, run `./gradlew clean`
-first:
+If you want to run a specific unit test, you can do it using
+[`--tests` filtering](https://docs.gradle.org/current/userguide/java_testing.html#test_filtering)
+```shell
-```
-Unknown source file : UNEXPECTED TOP-LEVEL EXCEPTION:
-Unknown source file : com.android.dex.DexException: Multiple dex files define Landroid/content/pm/ParceledListSlice$1;
-```
+# Run a test for an Android library on a connected device
+
+./gradlew <project-name>:test --tests androidx.core.view.DisplayCompatTest
+
+# Run a test for a JVM library
+
+./gradlew <project-name>:testDebugUnitTest --tests
+androidx.core.view.DisplayCompatTest ```
## Test apps {#testapps}
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
index 5cee0da..33d97f7 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -18,7 +18,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21"/>
<application
- android:label="Emoji Picker Sample App (Customized)" >
+ android:label="Emoji Picker Sample App (Customized)"
+ android:supportsRtl="true">
<activity android:name=".MainActivity" android:exported="true"
android:theme="@style/MyPinkTheme">
<!-- Handle Google app icon launch. -->
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
index dd4ff10..8a007c5 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
@@ -31,6 +31,7 @@
import androidx.test.screenshot.AndroidXScreenshotTestRule
import androidx.test.screenshot.assertAgainstGolden
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -94,6 +95,7 @@
dumpAndAssertAgainstGolden("multiple_draw")
}
+ @Ignore
@Test
fun testClear() {
setAndWait(GRINNING_FACE)
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
index b724ba9..c3bfbf8 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
@@ -48,6 +48,8 @@
currentProperty = "$osVersion|$appVersion"
emojiPickerCacheDir =
File(getDeviceProtectedStorageContext(context).cacheDir, EMOJI_PICKER_FOLDER)
+ if (!emojiPickerCacheDir.exists())
+ emojiPickerCacheDir.mkdir()
}
/** Get cache for a given file name, or write to a new file using the [defaultValue] factory. */
diff --git a/exifinterface/exifinterface/build.gradle b/exifinterface/exifinterface/build.gradle
index 2db0519..b6bcaab 100644
--- a/exifinterface/exifinterface/build.gradle
+++ b/exifinterface/exifinterface/build.gradle
@@ -9,6 +9,7 @@
implementation("androidx.annotation:annotation:1.2.0")
androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testExtTruth)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index dec5d83..0955535 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -18,10 +18,13 @@
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -64,6 +67,7 @@
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Objects;
import java.util.Random;
@@ -88,6 +92,8 @@
private static final String JPEG_WITH_EXIF_BYTE_ORDER_II = "jpeg_with_exif_byte_order_ii.jpg";
private static final String JPEG_WITH_EXIF_BYTE_ORDER_MM = "jpeg_with_exif_byte_order_mm.jpg";
private static final String JPEG_WITH_EXIF_INVALID_OFFSET = "jpeg_with_exif_invalid_offset.jpg";
+ private static final String JPEG_WITH_EXIF_FULL_APP1_SEGMENT =
+ "jpeg_with_exif_full_app1_segment.jpg";
private static final String DNG_WITH_EXIF_WITH_XMP = "dng_with_exif_with_xmp.dng";
private static final String JPEG_WITH_EXIF_WITH_XMP = "jpeg_with_exif_with_xmp.jpg";
@@ -110,6 +116,7 @@
R.raw.jpeg_with_exif_byte_order_ii,
R.raw.jpeg_with_exif_byte_order_mm,
R.raw.jpeg_with_exif_invalid_offset,
+ R.raw.jpeg_with_exif_full_app1_segment,
R.raw.dng_with_exif_with_xmp,
R.raw.jpeg_with_exif_with_xmp,
R.raw.png_with_exif_byte_order_ii,
@@ -126,6 +133,7 @@
JPEG_WITH_EXIF_BYTE_ORDER_II,
JPEG_WITH_EXIF_BYTE_ORDER_MM,
JPEG_WITH_EXIF_INVALID_OFFSET,
+ JPEG_WITH_EXIF_FULL_APP1_SEGMENT,
DNG_WITH_EXIF_WITH_XMP,
JPEG_WITH_EXIF_WITH_XMP,
PNG_WITH_EXIF_BYTE_ORDER_II,
@@ -466,6 +474,28 @@
writeToFilesWithExif(JPEG_WITH_EXIF_INVALID_OFFSET, R.array.jpeg_with_exif_invalid_offset);
}
+ // https://issuetracker.google.com/263747161
+ @Test
+ @LargeTest
+ public void testJpegWithFullApp1Segment() throws Throwable {
+ File srcFile = getFileFromExternalDir(JPEG_WITH_EXIF_FULL_APP1_SEGMENT);
+ File imageFile = clone(srcFile);
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ // Add a really long string that makes the Exif data too large for the JPEG APP1 segment.
+ char[] longStringChars = new char[500];
+ Arrays.fill(longStringChars, 'a');
+ String longString = new String(longStringChars);
+ exifInterface.setAttribute(ExifInterface.TAG_MAKE, longString);
+
+ IOException expected = assertThrows(IOException.class,
+ exifInterface::saveAttributes);
+ assertThat(expected)
+ .hasCauseThat()
+ .hasMessageThat()
+ .contains("exceeds the max size of a JPEG APP1 segment");
+ assertBitmapsEquivalent(srcFile, imageFile);
+ }
+
@Test
@LargeTest
public void testDngWithExifAndXmp() throws Throwable {
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_full_app1_segment.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_full_app1_segment.jpg
new file mode 100644
index 0000000..1dbbc23
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_full_app1_segment.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 9361fc0..e7c8d08 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -7483,6 +7483,11 @@
switch (mMimeType) {
case IMAGE_TYPE_JPEG:
+ if (totalSize > 0xFFFF) {
+ throw new IllegalStateException(
+ "Size of exif data (" + totalSize + " bytes) exceeds the max size of a "
+ + "JPEG APP1 segment (65536 bytes)");
+ }
// Write JPEG specific data (APP1 size, APP1 identifier)
dataOutputStream.writeUnsignedShort(totalSize);
dataOutputStream.write(IDENTIFIER_EXIF_APP1);
@@ -8015,10 +8020,18 @@
}
public void writeUnsignedShort(int val) throws IOException {
+ if (val > 0xFFFF) {
+ throw new IllegalArgumentException("val is larger than the maximum value of a "
+ + "16-bit unsigned integer");
+ }
writeShort((short) val);
}
public void writeUnsignedInt(long val) throws IOException {
+ if (val > 0xFFFF_FFFFL) {
+ throw new IllegalArgumentException("val is larger than the maximum value of a "
+ + "32-bit unsigned integer");
+ }
writeInt((int) val);
}
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 5dfc13d..3e1efd5 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -38,12 +38,8 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
-import leakcanary.DetectLeaksAfterTestSuccess
-import leakcanary.SkipLeakDetection
-import leakcanary.TestDescriptionHolder
import org.junit.Rule
import org.junit.Test
-import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@LargeTest
@@ -51,16 +47,17 @@
class DialogFragmentTest {
@Suppress("DEPRECATION")
+ @get:Rule
val activityTestRule =
androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ // TODO(b/270722758): Add back in leak detection rule chain once leak addressed by platform
// Detect leaks BEFORE and AFTER activity is destroyed
+ /*
@get:Rule
val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
.around(activityTestRule)
-
- @get:Rule
- val testDescriptionHolderRule = TestDescriptionHolder
+ */
@Test
fun testDialogFragmentShows() {
@@ -100,8 +97,6 @@
.isTrue()
}
- // TODO(b/270722758): remove annotation once issue addressed by LeakCanary/platform
- @SkipLeakDetection("Skip leak detection until platform ViewRootImpl leak addressed")
@Test
fun testDialogFragmentDismiss() {
val fragment = TestDialogFragment()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
index dd7a197..e312fa9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
@@ -32,10 +32,12 @@
import androidx.fragment.test.R
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelStore
+import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.waitForExecution
+import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
@@ -682,38 +684,44 @@
@Test
fun animationListenersAreCalled() {
- waitForAnimationReady()
- val fm = activityRule.activity.supportFragmentManager
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fm = withActivity {
+ setContentView(R.layout.simple_container)
+ supportFragmentManager
+ }
- // Add first fragment
- val fragment1 = AnimationListenerFragment()
- fragment1.forceRunOnHwLayer = false
- fragment1.repeat = true
- fm.beginTransaction()
- .add(R.id.fragmentContainer, fragment1)
- .commit()
- activityRule.waitForExecution()
+ // Add first fragment
+ val fragment1 = AnimationListenerFragment()
+ fragment1.forceRunOnHwLayer = false
+ fragment1.repeat = true
+ withActivity {
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragment1)
+ .commit()
+ fm.executePendingTransactions()
+ }
- // Replace first fragment with second fragment with a fade in/out animation
- val fragment2 = AnimationListenerFragment()
- fragment2.forceRunOnHwLayer = true
- fragment2.repeat = false
- fm.beginTransaction()
- .setCustomAnimations(
- android.R.anim.fade_in, android.R.anim.fade_out,
- android.R.anim.fade_in, android.R.anim.fade_out
- )
- .replace(R.id.fragmentContainer, fragment2)
- .addToBackStack(null)
- .commit()
- activityRule.waitForExecution()
+ // Replace first fragment with second fragment with a fade in/out animation
+ val fragment2 = AnimationListenerFragment()
+ fragment2.forceRunOnHwLayer = true
+ fragment2.repeat = false
+ withActivity {
+ fm.beginTransaction()
+ .setCustomAnimations(
+ android.R.anim.fade_in, android.R.anim.fade_out,
+ android.R.anim.fade_in, android.R.anim.fade_out
+ )
+ .replace(R.id.fragmentContainer, fragment2)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ }
- // Wait for animation to finish
- assertThat(fragment1.exitLatch.await(2, TimeUnit.SECONDS)).isTrue()
- assertThat(fragment2.enterLatch.await(2, TimeUnit.SECONDS)).isTrue()
+ // Wait for animation to finish
+ assertThat(fragment1.exitLatch.await(2, TimeUnit.SECONDS)).isTrue()
+ assertThat(fragment2.enterLatch.await(2, TimeUnit.SECONDS)).isTrue()
- // Check if all animation listener callbacks have been called
- activityRule.runOnUiThread {
+ // Check if all animation listener callbacks have been called
assertThat(fragment1.exitStartCount).isEqualTo(1)
assertThat(fragment1.exitRepeatCount).isEqualTo(1)
assertThat(fragment1.exitEndCount).isEqualTo(1)
@@ -729,17 +737,16 @@
assertThat(fragment2.exitStartCount).isEqualTo(0)
assertThat(fragment2.exitRepeatCount).isEqualTo(0)
assertThat(fragment2.exitEndCount).isEqualTo(0)
- }
- fragment1.resetCounts()
- fragment2.resetCounts()
- // Now pop the transaction
- activityRule.popBackStackImmediate()
+ fragment1.resetCounts()
+ fragment2.resetCounts()
- assertThat(fragment2.exitLatch.await(2, TimeUnit.SECONDS)).isTrue()
- assertThat(fragment1.enterLatch.await(2, TimeUnit.SECONDS)).isTrue()
+ // Now pop the transaction
+ withActivity { fm.popBackStackImmediate() }
- activityRule.runOnUiThread {
+ assertThat(fragment2.exitLatch.await(2, TimeUnit.SECONDS)).isTrue()
+ assertThat(fragment1.enterLatch.await(2, TimeUnit.SECONDS)).isTrue()
+
assertThat(fragment2.exitStartCount).isEqualTo(1)
assertThat(fragment2.exitRepeatCount).isEqualTo(0)
assertThat(fragment2.exitEndCount).isEqualTo(1)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0a6e37b..2383b5d9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,7 +28,7 @@
cmake = "3.22.1"
dagger = "2.44"
dexmaker = "2.28.3"
-dokka = "1.7.20"
+dokka = "1.8.10-dev-203"
espresso = "3.5.1"
espressoDevice = "1.0.0-alpha03"
grpc = "1.52.0"
@@ -144,9 +144,12 @@
junit = { module = "junit:junit", version = "4.13.2" }
gcmNetworkManager = { module = "com.google.android.gms:play-services-gcm", version = "17.0.0" }
googleCompileTesting = { module = "com.google.testing.compile:compile-testing", version = "0.18" }
-grpcBinder = { module = "io.grpc:grpc-binder", version.ref = "grpc" }
grpcAndroid = { module = "io.grpc:grpc-android", version.ref = "grpc" }
+grpcBinder = { module = "io.grpc:grpc-binder", version.ref = "grpc" }
+grpcProtobufCompiler = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" }
+grpcProtobufLite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grpc" }
grpcStub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
+grpcTesting = { module = "io.grpc:grpc-testing", version.ref = "grpc" }
gson = { module = "com.google.code.gson:gson", version = "2.9.0" }
guava = { module = "com.google.guava:guava", version.ref = "guavaJre" }
guavaAndroid = { module = "com.google.guava:guava", version = "31.1-android" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index ea9eb2d..048d8fc 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- To regenerate this file, run development/update-verification-metadata.sh -->
-
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.2.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
@@ -30,6 +29,9 @@
<trust group="^androidx($|([.].*))" regex="true" reason="not signed yet"/>
<trust group="^com[.]android($|([.].*))" regex="true" reason="b/215430394"/>
</trusted-artifacts>
+ <ignored-keys>
+ <ignored-key id="3d12ca2ac19f3181" reason="Key couldn't be downloaded from any key server"/>
+ </ignored-keys>
<trusted-keys>
<trusted-key id="00089ee8c3afa95a854d0f1df800dd0933ecf7f7" group="com.google.guava"/>
<trusted-key id="019082bc00e0324e2aef4cf00d3b328562a119a7" group="org.openjdk.jmh"/>
@@ -94,6 +96,7 @@
<trusted-key id="24d04176586361fda94ee0315f7786df73e61f56" group="com.google.devtools.ksp"/>
<trusted-key id="26063b04869f7d235ccc057447586a1b75ef0de5" group="com.squareup.wire"/>
<trusted-key id="263923711ef4fe3f3f0c28af11509ed50ec155e6" group="org.reactivestreams"/>
+ <trusted-key id="28118c070cb22a0175a2e8d43d12ca2ac19f3181" group="com.fasterxml.jackson.core" name="jackson-databind" version="2.12.7.1"/>
<trusted-key id="2a4f55d9cda5877731fbe7466eff5ef5523052d4" group="com.github.tschuchortdev"/>
<trusted-key id="2bab4466b44f54f8f99bbbdd5ed22f661bbf0acc" group="com.almworks.sqlite4java"/>
<trusted-key id="2bcbdd0f23ea1cafcc11d4860374cf2e8dd1bdfd">
@@ -434,6 +437,14 @@
<sha256 value="8a31a27a776ba55974936b897be76edca1cf871d9ae75537e593a7f1b83380e9" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.8.10.tar.gz"/>
</artifact>
</component>
+ <component group="com.fasterxml.jackson.core" name="jackson-databind" version="2.12.7.1">
+ <artifact name="jackson-databind-2.12.7.1.jar">
+ <sha256 value="3f504cac405ce066d5665ff69541484d5322f35ac7a7ec6104cf86a01008e02d" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+ </artifact>
+ <artifact name="jackson-databind-2.12.7.1.module">
+ <sha256 value="d34c2b230198d8b2d39a2ad14710d6c92271c07848c432a40e1d471594a229a6" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+ </artifact>
+ </component>
<component group="com.github.gundy" name="semver4j" version="0.16.4">
<artifact name="semver4j-0.16.4-nodeps.jar">
<sha256 value="3f59eca516374ccd4fd3551625bf50f8a4b191f700508f7ce4866460a6128af0" origin="Generated by Gradle"/>
@@ -448,12 +459,12 @@
<sha256 value="cd6db17a11a31ede794ccbd1df0e4d9750f640234731f21cff885a9997277e81" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
- <component group="com.google.ads.interactivemedia.v3" name="interactivemedia" version="3.29.0" androidx:reason="Unsigned. Used by media3.">
+ <component group="com.google.ads.interactivemedia.v3" name="interactivemedia" version="3.29.0">
<artifact name="interactivemedia-3.29.0.jar">
- <sha256 value="4d9b94444b8eea1637435c8f0598ee9f86c05ade73b1e8911dc16494013c379a" origin="Generated by Gradle"/>
+ <sha256 value="4d9b94444b8eea1637435c8f0598ee9f86c05ade73b1e8911dc16494013c379a" origin="Generated by Gradle" reason="Unsigned. Used by media3."/>
</artifact>
<artifact name="interactivemedia-3.29.0.pom">
- <sha256 value="9fb18fd29b9dfe2e7ed5fe98a3be433a4c3cc4ea8f47f2b444155c39b4afddf5" origin="Generated by Gradle"/>
+ <sha256 value="9fb18fd29b9dfe2e7ed5fe98a3be433a4c3cc4ea8f47f2b444155c39b4afddf5" origin="Generated by Gradle" reason="Unsigned. Used by media3."/>
</artifact>
</component>
<component group="com.google.android.apps.common.testing.accessibility.framework" name="accessibility-test-framework" version="2.1">
@@ -474,10 +485,10 @@
</component>
<component group="com.google.android.libraries.identity.googleid" name="googleid" version="0.0.2">
<artifact name="googleid-0.0.2.aar">
- <sha256 value="ae69047587928df1ff82ce71e9f2ec40a777270f04f4a6d1f66241944961b682" origin="Generated by Gradle"/>
+ <sha256 value="ae69047587928df1ff82ce71e9f2ec40a777270f04f4a6d1f66241944961b682" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="googleid-0.0.2.pom">
- <sha256 value="100a2c6db1ec4aab9545ead93be094c9728ecc671fba8353648d04ef405f30c8" origin="Generated by Gradle"/>
+ <sha256 value="100a2c6db1ec4aab9545ead93be094c9728ecc671fba8353648d04ef405f30c8" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.google.android.odml" name="image" version="1.0.0-beta1">
@@ -655,14 +666,6 @@
<sha256 value="31ce606f4e9518936299bb0d27c978fa61e185fd1de7c9874fe959a53e34a685" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
- <component group="org.chromium.net" name="cronet-api" version="72.3626.96" androidx:reason="Unsigned. Used by media3.">
- <artifact name="cronet-api-72.3626.96.aar">
- <sha256 value="6542c0377f00f38e2c707cc3ade1607843f572342cc798b38b78897f7d8ec248" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="cronet-api-72.3626.96.pom">
- <sha256 value="8d25d21f7f2aca27dc10638ad417bbb08a189310274bc322aa620aafe7e82c92" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="org.apache" name="apache" version="15">
<artifact name="apache-15.pom">
<pgp value="6bdaca2c0493cca133b372d09c4f7e9d98b1cc53"/>
@@ -706,6 +709,14 @@
<sha256 value="186fd460ee13150e31188703a2c871bf86e20332636f3ede4ab959cd5568da78" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.chromium.net" name="cronet-api" version="72.3626.96">
+ <artifact name="cronet-api-72.3626.96.aar">
+ <sha256 value="6542c0377f00f38e2c707cc3ade1607843f572342cc798b38b78897f7d8ec248" origin="Generated by Gradle" reason="Unsigned. Used by media3."/>
+ </artifact>
+ <artifact name="cronet-api-72.3626.96.pom">
+ <sha256 value="8d25d21f7f2aca27dc10638ad417bbb08a189310274bc322aa620aafe7e82c92" origin="Generated by Gradle" reason="Unsigned. Used by media3."/>
+ </artifact>
+ </component>
<component group="org.codehaus.mojo" name="mojo-parent" version="40">
<artifact name="mojo-parent-40.pom">
<pgp value="d433f9c895710db8ab087fa6b7c3b43d18eaa8b7"/>
@@ -716,6 +727,70 @@
<pgp value="720746177725a89207a7075bfd5dea07fcb690a8"/>
</artifact>
</component>
+ <component group="org.jetbrains.dokka" name="all-modules-page-plugin" version="1.8.10-dev-203">
+ <artifact name="all-modules-page-plugin-1.8.10-dev-203.jar">
+ <sha256 value="d750c52b0f2ad66ff5bdee04ecc6ba50b9eb7e02720abec455aff33635112524" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="all-modules-page-plugin-1.8.10-dev-203.module">
+ <sha256 value="317e08d873f54310e1fcbfe5a4509367c2d3c26a65cf6f01937b700ed5db8b4e" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="android-documentation-plugin" version="1.8.10-dev-203">
+ <artifact name="android-documentation-plugin-1.8.10-dev-203.jar">
+ <sha256 value="388125760e133d6cf80d8277990532a8037ba29c9beb57f8daf4f268f0e9a6ed" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="android-documentation-plugin-1.8.10-dev-203.module">
+ <sha256 value="5653f9361ed738ddff84ff5a4a2aef26e5983113ec7cac6a7e9d14d51b7f0905" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="dokka-analysis" version="1.8.10-dev-203">
+ <artifact name="dokka-analysis-1.8.10-dev-203.jar">
+ <sha256 value="f92acffcf55a8ecd1ca1d86224fe9c0477c2efc2f2a80a3541250620dab31c3f" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="dokka-analysis-1.8.10-dev-203.module">
+ <sha256 value="0ca9f55d947ec7c03a1169a67f8ec328c135efb359621a80a633aca53d5ba278" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="dokka-base" version="1.8.10-dev-203">
+ <artifact name="dokka-base-1.8.10-dev-203.jar">
+ <sha256 value="87a7737029f32a602bd5e04146e5919b46be2eadbacf5ad7bd151f78d6caf1ff" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="dokka-base-1.8.10-dev-203.module">
+ <sha256 value="842b421bd24ac2a154dd6b10f358eb31ed5d498340b75898c68e88ff72f14714" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="dokka-cli" version="1.8.10-dev-203">
+ <artifact name="dokka-cli-1.8.10-dev-203.jar">
+ <sha256 value="aac1f51acff796df791343aab7e52c7d1828c14811c63aeb17d8b4f2815f1433" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="dokka-cli-1.8.10-dev-203.pom">
+ <sha256 value="18e6494b8198025538bc36f4ed81cbcb61c72c6d2d9fdb0162d0981404f65bac" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="kotlin-analysis-compiler" version="1.8.10-dev-203">
+ <artifact name="kotlin-analysis-compiler-1.8.10-dev-203.jar">
+ <sha256 value="a220e6566c17fdf57e66bc76678876dcb5a1605a00107561588209df84700e2e" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="kotlin-analysis-compiler-1.8.10-dev-203.pom">
+ <sha256 value="11479e6eea4f5d6758e5993e3240ceeb9ac2f19de043cdacba2a10c39d1ce6b4" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="kotlin-analysis-intellij" version="1.8.10-dev-203">
+ <artifact name="kotlin-analysis-intellij-1.8.10-dev-203.jar">
+ <sha256 value="7e8fa87cfe4aa342844e571cd6d71274e4ef0694f90c64527b534603cd2a8545" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="kotlin-analysis-intellij-1.8.10-dev-203.pom">
+ <sha256 value="16bb57be30478e0a2f9ec0923aa4ca362c55c511282c01ad7d92d7dc1f8823c3" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
+ <component group="org.jetbrains.dokka" name="templating-plugin" version="1.8.10-dev-203">
+ <artifact name="templating-plugin-1.8.10-dev-203.jar">
+ <sha256 value="30b12682ffe08a886f8a78ee1df307c36a23dec5764c580665924d7fac9ba2d9" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="templating-plugin-1.8.10-dev-203.module">
+ <sha256 value="0eadb5d9052cf9cd3e2da9681c8de8367a272b524b4915c3b11eb87e789655b6" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.3.71">
<artifact name="kotlin-reflect-1.3.71.pom">
<sha256 value="4df94aaeee8d900be431386e31ef44e82a66e57c3ae30866aec2875aff01fe70" origin="Generated by Gradle"/>
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
index 44fcc10..dd4c20a 100644
--- a/graphics/graphics-core/build.gradle
+++ b/graphics/graphics-core/build.gradle
@@ -62,6 +62,7 @@
androidx {
name = "Android Graphics Core"
type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.GRAPHICS_CORE
inceptionYear = "2021"
description = "Leverage graphics facilities across multiple Android platform releases"
}
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 16ac246..b53ff80 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -749,7 +749,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
fun testUsageFlagContainsFrontBufferUsage() {
- val usageFlags = GLFrontBufferedRenderer.obtainHardwareBufferUsageFlags()
+ val usageFlags = FrontBufferUtils.obtainHardwareBufferUsageFlags()
if (UsageFlagsVerificationHelper.isSupported(HardwareBuffer.USAGE_FRONT_BUFFER)) {
assertNotEquals(0, usageFlags and HardwareBuffer.USAGE_FRONT_BUFFER)
} else {
@@ -760,7 +760,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
fun testUsageFlagContainsComposerOverlay() {
- val usageFlags = GLFrontBufferedRenderer.obtainHardwareBufferUsageFlags()
+ val usageFlags = FrontBufferUtils.obtainHardwareBufferUsageFlags()
if (UsageFlagsVerificationHelper.isSupported(HardwareBuffer.USAGE_COMPOSER_OVERLAY)) {
assertNotEquals(
0,
@@ -775,11 +775,11 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testBaseFlags() {
assertNotEquals(
- 0, GLFrontBufferedRenderer.BaseFlags and
+ 0, FrontBufferUtils.BaseFlags and
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
)
assertNotEquals(
- 0, GLFrontBufferedRenderer.BaseFlags and
+ 0, FrontBufferUtils.BaseFlags and
HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
)
}
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
index 68fccb0..f29aee4 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -19,6 +19,7 @@
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.ColorSpace
+import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.SurfaceTexture
import android.hardware.HardwareBuffer
@@ -816,7 +817,7 @@
val teardownLatch = CountDownLatch(1)
val glRenderer = GLRenderer().apply { start() }
var frameBuffer: FrameBuffer? = null
- var status: Boolean? = false
+ var status = false
var supportsFence = false
val callbacks = object : FrameBufferRenderer.RenderCallback {
@@ -862,7 +863,7 @@
frameBuffer: FrameBuffer,
syncFenceCompat: SyncFenceCompat?
) {
- status = syncFenceCompat?.await(3000)
+ status = syncFenceCompat?.await(3000) ?: true
renderLatch.countDown()
}
}
@@ -877,10 +878,7 @@
try {
assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
if (supportsFence) {
- assert(status != null)
- status?.let {
- assertTrue(it)
- }
+ assert(status)
}
hardwareBuffer = frameBuffer?.hardwareBuffer
@@ -908,6 +906,140 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ fun testQuadTextureRenderer() {
+ val width = 10
+ val height = 10
+ val renderLatch = CountDownLatch(1)
+ val teardownLatch = CountDownLatch(1)
+ val glRenderer = GLRenderer().apply { start() }
+ var frameBuffer: FrameBuffer? = null
+ var status = false
+ var supportsFence = false
+ val frameHandlerThread = HandlerThread("frameAvailable").apply { start() }
+ val frameHandler = Handler(frameHandlerThread.looper)
+ val callbacks = object : FrameBufferRenderer.RenderCallback {
+
+ private val mOrthoMatrix = FloatArray(16)
+
+ override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
+ FrameBuffer(
+ egl,
+ HardwareBuffer.create(
+ width,
+ height,
+ HardwareBuffer.RGBA_8888,
+ 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ )
+ ).also { frameBuffer = it }
+
+ override fun onDraw(eglManager: EGLManager) {
+ val texId = genTexture()
+ val frameAvailableLatch = CountDownLatch(1)
+ val surfaceTexture = SurfaceTexture(texId).apply {
+ setDefaultBufferSize(width, height)
+ setOnFrameAvailableListener({
+ frameAvailableLatch.countDown()
+ }, frameHandler)
+ }
+
+ val surface = Surface(surfaceTexture)
+ val canvas = surface.lockCanvas(null)
+ canvas.save()
+ // GL is flipped vertically from Android's canvas so flip the canvas here
+ canvas.scale(1f, -1f, width / 2f, height / 2f)
+ val paint = Paint()
+ // top left
+ canvas.drawRect(0f, 0f, width / 2f, height / 2f,
+ paint.apply { color = Color.RED })
+ // top right
+ canvas.drawRect(width / 2f, 0f, width.toFloat(), height / 2f,
+ paint.apply { color = Color.BLUE })
+ // bottom left
+ canvas.drawRect(0f, height / 2f, width / 2f, height.toFloat(),
+ paint.apply { color = Color.YELLOW })
+ // bottom right
+ canvas.drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(),
+ paint.apply { color = Color.GREEN })
+ canvas.restore()
+ surface.unlockCanvasAndPost(canvas)
+
+ assertTrue(frameAvailableLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ GLES20.glViewport(0, 0, width, height)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ width.toFloat(),
+ 0f,
+ height.toFloat(),
+ -1f,
+ 1f
+ )
+ val quadRenderer = QuadTextureRenderer().apply {
+ setSurfaceTexture(surfaceTexture)
+ }
+ quadRenderer.draw(
+ mOrthoMatrix,
+ width.toFloat(),
+ height.toFloat()
+ )
+ supportsFence = eglManager.supportsNativeAndroidFence()
+ quadRenderer.release()
+ surface.release()
+ surfaceTexture.release()
+ deleteTexture(texId)
+ }
+
+ override fun onDrawComplete(
+ frameBuffer: FrameBuffer,
+ syncFenceCompat: SyncFenceCompat?
+ ) {
+ status = syncFenceCompat?.await(3000) ?: true
+ renderLatch.countDown()
+ }
+ }
+
+ glRenderer.createRenderTarget(
+ width,
+ height,
+ FrameBufferRenderer(callbacks)
+ ).requestRender()
+
+ var hardwareBuffer: HardwareBuffer? = null
+ try {
+ assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+ if (supportsFence) {
+ assertTrue(status)
+ }
+
+ hardwareBuffer = frameBuffer?.hardwareBuffer
+ if (hardwareBuffer != null) {
+ val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+ // Copy to non hardware bitmap to be able to sample pixels
+ val bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace)
+ ?.copy(Bitmap.Config.ARGB_8888, false)
+ if (bitmap != null) {
+ bitmap.verifyQuadrants(Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN)
+ } else {
+ fail("Unable to obtain Bitmap from hardware buffer")
+ }
+ } else {
+ fail("Unable to obtain hardwarebuffer from FrameBuffer")
+ }
+ } finally {
+ hardwareBuffer?.close()
+ glRenderer.stop(true) {
+ teardownLatch.countDown()
+ }
+ frameHandlerThread.quit()
+ assertTrue(teardownLatch.await(3000, TimeUnit.MILLISECONDS))
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testFrameBufferRendererWithSyncFence() {
val width = 10
@@ -1078,4 +1210,43 @@
}
return true
}
+
+ private fun Bitmap.verifyQuadrants(
+ topLeft: Int,
+ topRight: Int,
+ bottomLeft: Int,
+ bottomRight: Int
+ ) {
+ assertEquals(topLeft, getPixel(0, 0))
+ assertEquals(topLeft, getPixel(width / 2 - 1, 0))
+ assertEquals(topLeft, getPixel(width / 2 - 1, height / 2 - 1))
+ assertEquals(topLeft, getPixel(0, height / 2 - 1))
+
+ assertEquals(topRight, getPixel(width / 2 + 1, 0))
+ assertEquals(topRight, getPixel(width - 1, 0))
+ assertEquals(topRight, getPixel(width - 1, height / 2 - 1))
+ assertEquals(topRight, getPixel(width / 2 + 1, height / 2 - 1))
+
+ assertEquals(bottomLeft, getPixel(0, height / 2 + 1))
+ assertEquals(bottomLeft, getPixel(width / 2 - 1, height / 2 + 1))
+ assertEquals(bottomLeft, getPixel(width / 2 - 1, height - 1))
+ assertEquals(bottomLeft, getPixel(0, height - 1))
+
+ assertEquals(bottomRight, getPixel(width / 2 + 1, height / 2 + 1))
+ assertEquals(bottomRight, getPixel(width - 1, height / 2 + 1))
+ assertEquals(bottomRight, getPixel(width - 1, height - 1))
+ assertEquals(bottomRight, getPixel(width / 2 + 1, height - 1))
+ }
+
+ private fun genTexture(): Int {
+ val buffer = IntArray(1)
+ GLES20.glGenTextures(1, buffer, 0)
+ return buffer[0]
+ }
+
+ private fun deleteTexture(texId: Int) {
+ val buffer = IntArray(1)
+ buffer[0] = texId
+ GLES20.glDeleteTextures(1, buffer, 0)
+ }
}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
index 38050ea..09d8470 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
@@ -20,7 +20,7 @@
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.graphics.lowlatency.FrontBufferSyncStrategy
-import androidx.graphics.lowlatency.GLFrontBufferedRenderer
+import androidx.graphics.lowlatency.FrontBufferUtils
import androidx.graphics.opengl.egl.EGLConfigAttributes
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
@@ -37,7 +37,7 @@
@SmallTest
@RequiresApi(Build.VERSION_CODES.Q)
class SyncStrategyTest {
- private val mUsageFlags = GLFrontBufferedRenderer.obtainHardwareBufferUsageFlags()
+ private val mUsageFlags = FrontBufferUtils.obtainHardwareBufferUsageFlags()
@RequiresApi(Build.VERSION_CODES.O)
@Test
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt
new file mode 100644
index 0000000..415fa3b
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.graphics.lowlatency
+
+import android.annotation.SuppressLint
+import android.hardware.HardwareBuffer
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+internal class FrontBufferUtils private constructor() {
+
+ companion object {
+
+ internal const val TAG = "FrontBufferUtils"
+
+ // Leverage the same value as HardwareBuffer.USAGE_COMPOSER_OVERLAY.
+ // While this constant was introduced in the SDK in the Android T release, it has
+ // been available within the NDK as part of
+ // AHardwareBuffer_UsageFlags#AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY for quite some time.
+ // This flag is required for usage of ASurfaceTransaction#setBuffer
+ // Use a separate constant with the same value to avoid SDK warnings of accessing the
+ // newly added constant in the SDK.
+ // See:
+ // developer.android.com/ndk/reference/group/a-hardware-buffer#ahardwarebuffer_usageflags
+ private const val USAGE_COMPOSER_OVERLAY: Long = 2048L
+
+ /**
+ * Flags that are expected to be supported on all [HardwareBuffer] instances
+ */
+ internal const val BaseFlags =
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or
+ HardwareBuffer.USAGE_GPU_COLOR_OUTPUT or
+ USAGE_COMPOSER_OVERLAY
+
+ internal fun obtainHardwareBufferUsageFlags(): Long =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ UsageFlagsVerificationHelper.obtainUsageFlagsV33()
+ } else {
+ BaseFlags
+ }
+ }
+}
+
+/**
+ * Helper class to avoid class verification failures
+ */
+@RequiresApi(Build.VERSION_CODES.Q)
+internal class UsageFlagsVerificationHelper private constructor() {
+ companion object {
+
+ /**
+ * Helper method to determine if a particular HardwareBuffer usage flag is supported.
+ * Even though the FRONT_BUFFER_USAGE and COMPOSER_OVERLAY flags are introduced in
+ * Android T, not all devices may support this flag. So we conduct a capability query
+ * with a sample 1x1 HardwareBuffer with the provided flag to see if it is compatible
+ */
+ // Suppressing WrongConstant warnings as we are leveraging a constant with the same value
+ // as HardwareBuffer.USAGE_COMPOSER_OVERLAY to avoid SDK checks as the constant has been
+ // supported in the NDK for several platform releases.
+ // See:
+ // developer.android.com/ndk/reference/group/a-hardware-buffer#ahardwarebuffer_usageflags
+ @SuppressLint("WrongConstant")
+ @RequiresApi(Build.VERSION_CODES.Q)
+ @androidx.annotation.DoNotInline
+ internal fun isSupported(flag: Long): Boolean =
+ HardwareBuffer.isSupported(
+ 1, // width
+ 1, // height
+ HardwareBuffer.RGBA_8888, // format
+ 1, // layers
+ FrontBufferUtils.BaseFlags or flag
+ )
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ @androidx.annotation.DoNotInline
+ fun obtainUsageFlagsV33(): Long {
+ // First verify if the front buffer usage flag is supported along with the
+ // "usage composer overlay" flag that was introduced in API level 33
+ return if (isSupported(HardwareBuffer.USAGE_FRONT_BUFFER)) {
+ FrontBufferUtils.BaseFlags or HardwareBuffer.USAGE_FRONT_BUFFER
+ } else {
+ FrontBufferUtils.BaseFlags
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 829e606..db7462f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -294,7 +294,7 @@
mGLRenderer = renderer
- mHardwareBufferUsageFlags = obtainHardwareBufferUsageFlags()
+ mHardwareBufferUsageFlags = FrontBufferUtils.obtainHardwareBufferUsageFlags()
mFrontBufferSyncStrategy = FrontBufferSyncStrategy(mHardwareBufferUsageFlags)
}
@@ -324,7 +324,7 @@
bufferWidth,
bufferHeight,
format = HardwareBuffer.RGBA_8888,
- usage = BaseFlags,
+ usage = FrontBufferUtils.BaseFlags,
maxPoolSize = 4
)
@@ -675,32 +675,6 @@
companion object {
internal const val TAG = "GLFrontBufferedRenderer"
-
- // Leverage the same value as HardwareBuffer.USAGE_COMPOSER_OVERLAY.
- // While this constant was introduced in the SDK in the Android T release, it has
- // been available within the NDK as part of
- // AHardwareBuffer_UsageFlags#AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY for quite some time.
- // This flag is required for usage of ASurfaceTransaction#setBuffer
- // Use a separate constant with the same value to avoid SDK warnings of accessing the
- // newly added constant in the SDK.
- // See:
- // developer.android.com/ndk/reference/group/a-hardware-buffer#ahardwarebuffer_usageflags
- private const val USAGE_COMPOSER_OVERLAY: Long = 2048L
-
- /**
- * Flags that are expected to be supported on all [HardwareBuffer] instances
- */
- internal const val BaseFlags =
- HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or
- HardwareBuffer.USAGE_GPU_COLOR_OUTPUT or
- USAGE_COMPOSER_OVERLAY
-
- internal fun obtainHardwareBufferUsageFlags(): Long =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- UsageFlagsVerificationHelper.obtainUsageFlagsV33()
- } else {
- BaseFlags
- }
}
@JvmDefaultWithCompatibility
@@ -874,48 +848,4 @@
// Default implementation is a no-op
}
}
-}
-
-/**
- * Helper class to avoid class verification failures
- */
-@RequiresApi(Build.VERSION_CODES.Q)
-internal class UsageFlagsVerificationHelper private constructor() {
- companion object {
-
- /**
- * Helper method to determine if a particular HardwareBuffer usage flag is supported.
- * Even though the FRONT_BUFFER_USAGE and COMPOSER_OVERLAY flags are introduced in
- * Android T, not all devices may support this flag. So we conduct a capability query
- * with a sample 1x1 HardwareBuffer with the provided flag to see if it is compatible
- */
- // Suppressing WrongConstant warnings as we are leveraging a constant with the same value
- // as HardwareBuffer.USAGE_COMPOSER_OVERLAY to avoid SDK checks as the constant has been
- // supported in the NDK for several platform releases.
- // See:
- // developer.android.com/ndk/reference/group/a-hardware-buffer#ahardwarebuffer_usageflags
- @SuppressLint("WrongConstant")
- @RequiresApi(Build.VERSION_CODES.Q)
- @androidx.annotation.DoNotInline
- internal fun isSupported(flag: Long): Boolean =
- HardwareBuffer.isSupported(
- 1, // width
- 1, // height
- HardwareBuffer.RGBA_8888, // format
- 1, // layers
- GLFrontBufferedRenderer.BaseFlags or flag
- )
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- @androidx.annotation.DoNotInline
- fun obtainUsageFlagsV33(): Long {
- // First verify if the front buffer usage flag is supported along with the
- // "usage composer overlay" flag that was introduced in API level
- return if (isSupported(HardwareBuffer.USAGE_FRONT_BUFFER)) {
- GLFrontBufferedRenderer.BaseFlags or HardwareBuffer.USAGE_FRONT_BUFFER
- } else {
- GLFrontBufferedRenderer.BaseFlags
- }
- }
- }
}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt
new file mode 100644
index 0000000..c45e203
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt
@@ -0,0 +1,331 @@
+/*
+ * 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.graphics.opengl
+
+import android.graphics.SurfaceTexture
+import android.opengl.GLES11Ext
+import android.opengl.GLES20
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+import java.nio.ShortBuffer
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal class QuadTextureRenderer {
+
+ private var mSurfaceTexture: SurfaceTexture? = null
+
+ /**
+ * Array used to store 4 vertices of x and y coordinates
+ */
+ private val mQuadCoords = FloatArray(8)
+
+ /**
+ * Transform to apply to the corresponding texture source
+ */
+ private val mTextureTransform = FloatArray(16)
+
+ /**
+ * Handle to the quad position attribute
+ */
+ private var mQuadPositionHandle = -1
+
+ /**
+ * Handle to the texture coordinate attribute
+ */
+ private var mTexPositionHandle = -1
+
+ /**
+ * Handle to the texture sampler uniform
+ */
+ private var mTextureUniformHandle: Int = -1
+
+ /**
+ * Handle to the MVP matrix uniform
+ */
+ private var mViewProjectionMatrixHandle: Int = -1
+
+ /**
+ * Handle to texture transform matrix
+ */
+ private var mTextureTransformHandle: Int = -1
+
+ /**
+ * GL Program used for rendering a quad with a texture
+ */
+ private var mProgram: Int = -1
+
+ /**
+ * Handle to the vertex shader
+ */
+ private var mVertexShader = -1
+
+ /**
+ * Handle to the fragment shader
+ */
+ private var mFragmentShader = -1
+
+ /**
+ * Flag to indicate the resources associated with the shaders/texture has been
+ * released. If this is true all subsequent attempts to draw should be ignored
+ */
+ private var mIsReleased = false
+
+ /**
+ * FloatBuffer used to specify quad coordinates
+ */
+ private val mQuadrantCoordinatesBuffer: FloatBuffer =
+ ByteBuffer.allocateDirect(mQuadCoords.size * 4).run {
+ order(ByteOrder.nativeOrder())
+ asFloatBuffer().apply {
+ position(0)
+ }
+ }
+
+ init {
+ mVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VertexShader)
+ mFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FragmentShader)
+ mProgram = GLES20.glCreateProgram()
+
+ GLES20.glAttachShader(mProgram, mVertexShader)
+ GLES20.glAttachShader(mProgram, mFragmentShader)
+ GLES20.glLinkProgram(mProgram)
+ GLES20.glUseProgram(mProgram)
+
+ mQuadPositionHandle = GLES20.glGetAttribLocation(mProgram, aPosition)
+ mTexPositionHandle = GLES20.glGetAttribLocation(mProgram, aTexCoord)
+
+ mTextureUniformHandle = GLES20.glGetUniformLocation(mProgram, uTexture)
+ mViewProjectionMatrixHandle = GLES20.glGetUniformLocation(mProgram, uVPMatrix)
+ mTextureTransformHandle = GLES20.glGetUniformLocation(mProgram, tVPMatrix)
+
+ // Enable blend
+ GLES20.glEnable(GLES20.GL_BLEND)
+ // Uses to prevent transparent area to turn in black
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA)
+ }
+
+ fun release() {
+ if (!mIsReleased) {
+ if (mVertexShader != -1) {
+ GLES20.glDeleteShader(mVertexShader)
+ mVertexShader = -1
+ }
+
+ if (mFragmentShader != -1) {
+ GLES20.glDeleteShader(mFragmentShader)
+ mFragmentShader = -1
+ }
+
+ if (mProgram != -1) {
+ GLES20.glDeleteProgram(mProgram)
+ mProgram = -1
+ }
+
+ mIsReleased = true
+ }
+ }
+
+ private fun configureQuad(width: Float, height: Float): FloatBuffer =
+ mQuadrantCoordinatesBuffer.apply {
+ put(mQuadCoords.apply {
+ this[0] = 0f // top left
+ this[1] = height
+ this[2] = 0f // bottom left
+ this[3] = 0f
+ this[4] = width // top right
+ this[5] = 0f
+ this[6] = width // bottom right
+ this[7] = height
+ })
+ position(0)
+ }
+
+ internal fun setSurfaceTexture(surfaceTexture: SurfaceTexture) {
+ mSurfaceTexture = surfaceTexture
+ }
+
+ fun draw(
+ mvpMatrix: FloatArray,
+ width: Float,
+ height: Float
+ ) {
+ if (mIsReleased) {
+ Log.w(TAG, "Attempt to render when TextureRenderer has been released")
+ return
+ }
+
+ val textureSource = mSurfaceTexture
+ if (textureSource == null) {
+ Log.w(TAG, "Attempt to render without texture source")
+ return
+ }
+
+ GLES20.glUseProgram(mProgram)
+ textureSource.updateTexImage()
+
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_LINEAR)
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR)
+
+ GLES20.glUniform1i(mTextureUniformHandle, 0)
+
+ GLES20.glUniformMatrix4fv(
+ mViewProjectionMatrixHandle,
+ 1,
+ false,
+ mvpMatrix,
+ 0)
+
+ GLES20.glUniformMatrix4fv(
+ mTextureTransformHandle,
+ 1,
+ false,
+ mTextureTransform.apply {
+ textureSource.getTransformMatrix(this)
+ },
+ 0
+ )
+
+ GLES20.glVertexAttribPointer(
+ mQuadPositionHandle,
+ CoordsPerVertex,
+ GLES20.GL_FLOAT,
+ false,
+ VertexStride,
+ configureQuad(width, height)
+ )
+
+ GLES20.glVertexAttribPointer(
+ mTexPositionHandle,
+ CoordsPerVertex,
+ GLES20.GL_FLOAT,
+ false,
+ VertexStride,
+ TextureCoordinatesBuffer
+ )
+
+ GLES20.glEnableVertexAttribArray(mQuadPositionHandle)
+ GLES20.glEnableVertexAttribArray(mTexPositionHandle)
+
+ GLES20.glDrawElements(
+ GLES20.GL_TRIANGLES,
+ DrawOrder.size,
+ GLES20.GL_UNSIGNED_SHORT,
+ DrawOrderBuffer
+ )
+
+ GLES20.glDisableVertexAttribArray(mQuadPositionHandle)
+ GLES20.glDisableVertexAttribArray(mTexPositionHandle)
+ }
+
+ companion object {
+
+ private val TAG = "TextureRenderer"
+
+ internal const val uVPMatrix = "uVPMatrix"
+ internal const val tVPMatrix = "tVPMatrix"
+ internal const val aPosition = "aPosition"
+ internal const val aTexCoord = "aTexCoord"
+
+ private const val vTexCoord = "vTexCoord"
+ internal const val uTexture = "uTexture"
+
+ internal const val VertexShader =
+ """
+ uniform mat4 $uVPMatrix;
+ uniform mat4 $tVPMatrix;
+ attribute vec4 $aPosition;
+ attribute vec2 $aTexCoord;
+ varying vec2 $vTexCoord;
+
+ void main(void)
+ {
+ gl_Position = $uVPMatrix * $aPosition;
+ $vTexCoord = vec2($tVPMatrix * vec4($aTexCoord, 1.0, 1.0));
+ }
+ """
+
+ internal const val FragmentShader =
+ """
+ #extension GL_OES_EGL_image_external : require
+ precision highp float;
+
+ uniform samplerExternalOES $uTexture;
+
+ varying vec2 $vTexCoord;
+
+ void main(void){
+ gl_FragColor = texture2D($uTexture, $vTexCoord);
+ }
+ """
+
+ internal const val CoordsPerVertex = 2
+ internal const val VertexStride = 4 * CoordsPerVertex
+
+ private val TextureCoordinates = floatArrayOf(
+ // x, y
+ 0.0f, 1.0f, // top left
+ 0.0f, 0.0f, // bottom left
+ 1.0f, 0.0f, // bottom right
+ 1.0f, 1.0f, // top right
+ )
+
+ /**
+ * FloatBuffer used to specify the texture coordinates
+ */
+ private val TextureCoordinatesBuffer: FloatBuffer =
+ ByteBuffer.allocateDirect(TextureCoordinates.size * 4).run {
+ order(ByteOrder.nativeOrder())
+ asFloatBuffer().apply {
+ put(TextureCoordinates)
+ position(0)
+ }
+ }
+
+ private val DrawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)
+
+ /**
+ * Convert short array to short buffer
+ */
+ private val DrawOrderBuffer: ShortBuffer =
+ ByteBuffer.allocateDirect(DrawOrder.size * 2).run {
+ order(ByteOrder.nativeOrder())
+ asShortBuffer().apply {
+ put(DrawOrder)
+ position(0)
+ }
+ }
+
+ fun checkError(msg: String) {
+ val error = GLES20.glGetError()
+ if (error != GLES20.GL_NO_ERROR) {
+ Log.v(TAG, "GLError $msg: $error")
+ }
+ }
+
+ internal fun loadShader(type: Int, shaderCode: String): Int =
+ GLES20.glCreateShader(type).also { shader ->
+ GLES20.glShaderSource(shader, shaderCode)
+ GLES20.glCompileShader(shader)
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index 7dbd44f..c54a44a 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -39,6 +39,7 @@
androidx {
name = "Android Graphics Shapes"
type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.GRAPHICS_SHAPES
inceptionYear = "2022"
description = "create and render rounded polygonal shapes"
}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
index 4b3f682..2ab4db1 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/Availability.kt
@@ -26,7 +26,6 @@
public interface Availability {
public val id: Int
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public fun toProto(): DataProto.Availability =
DataProto.Availability.newBuilder()
@@ -34,7 +33,6 @@
.build()
public companion object {
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
public fun fromProto(proto: DataProto.Availability): Availability =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ComparisonType.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ComparisonType.kt
index 99291bf..ed977cd 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ComparisonType.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ComparisonType.kt
@@ -39,7 +39,6 @@
override fun toString(): String = name
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun toProto(): DataProto.ComparisonType =
when (this) {
@@ -81,7 +80,6 @@
public val VALUES: List<ComparisonType> =
listOf(GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL)
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
internal fun fromProto(proto: DataProto.ComparisonType): ComparisonType =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataTypeAvailability.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataTypeAvailability.kt
index bbbf76c..cc8e023 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataTypeAvailability.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataTypeAvailability.kt
@@ -39,7 +39,6 @@
override fun hashCode(): Int = id
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public override fun toProto(): DataProto.Availability =
DataProto.Availability.newBuilder()
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseEndReason.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseEndReason.kt
index 8003c72..b0ac3ad 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseEndReason.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseEndReason.kt
@@ -17,6 +17,7 @@
package androidx.health.services.client.data
import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
import androidx.health.services.client.proto.DataProto
import androidx.health.services.client.ExerciseClient
import kotlin.annotation.AnnotationRetention.SOURCE
@@ -24,7 +25,6 @@
/**
* The reason why an exercise has been ended for [ExerciseState] used in [ExerciseStateInfo].
*
- * @hide
*/
@Retention(SOURCE)
@IntDef(
@@ -36,6 +36,7 @@
ExerciseEndReason.AUTO_END_SUPERSEDED,
ExerciseEndReason.AUTO_END_PREPARE_EXPIRED
)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public annotation class ExerciseEndReason {
public companion object {
@@ -86,7 +87,6 @@
else -> DataProto.ExerciseEndReason.EXERCISE_END_REASON_UNKNOWN
}
- /** @hide */
@ExerciseEndReason
internal fun fromProto(proto: DataProto.ExerciseEndReason): Int =
when (proto) {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseGoalType.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseGoalType.kt
index ab28fd9..0cd2a4d 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseGoalType.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseGoalType.kt
@@ -35,7 +35,6 @@
override fun hashCode(): Int = id
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun toProto(): DataProto.ExerciseGoalType =
DataProto.ExerciseGoalType.forNumber(id) ?: EXERCISE_GOAL_TYPE_UNKNOWN
@@ -59,7 +58,6 @@
@JvmStatic
public fun fromId(id: Int): ExerciseGoalType? = VALUES.firstOrNull { it.id == id }
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
internal fun fromProto(proto: DataProto.ExerciseGoalType): ExerciseGoalType? =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseState.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseState.kt
index ef7649f..40ead51 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseState.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseState.kt
@@ -66,7 +66,6 @@
override fun hashCode(): Int = id
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun toProto(): DataProto.ExerciseState =
DataProto.ExerciseState.forNumber(id) ?: DataProto.ExerciseState.EXERCISE_STATE_UNKNOWN
@@ -255,7 +254,6 @@
@JvmStatic
public fun fromId(id: Int): ExerciseState? = VALUES.firstOrNull { it.id == id }
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmStatic
public fun fromProto(proto: DataProto.ExerciseState): ExerciseState? = fromId(proto.number)
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseStateInfo.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseStateInfo.kt
index 28408bf..64bd5b5 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseStateInfo.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseStateInfo.kt
@@ -72,7 +72,6 @@
/**
* Gets the [ExerciseEndReason] from the current [ExerciseState].
*
- * @hide
*/
@ExerciseEndReason
internal fun getEndReasonFromState(exerciseState: ExerciseState): Int =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTrackedStatus.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTrackedStatus.kt
index 7665eb6..e02b7f8 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTrackedStatus.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTrackedStatus.kt
@@ -24,7 +24,6 @@
/**
* Status representing if an exercise is being tracked and which app owns the exercise.
*
- * @hide
*/
@Retention(AnnotationRetention.SOURCE)
@IntDef(
@@ -32,6 +31,7 @@
ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS,
ExerciseTrackedStatus.NO_EXERCISE_IN_PROGRESS
)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public annotation class ExerciseTrackedStatus {
public companion object {
@@ -44,13 +44,11 @@
/** There is not currently any exercise in progress owned by any app. */
public const val NO_EXERCISE_IN_PROGRESS: Int = 3
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun @receiver:ExerciseTrackedStatus
Int.toProto(): DataProto.ExerciseTrackedStatus =
DataProto.ExerciseTrackedStatus.forNumber(this) ?: EXERCISE_TRACKED_STATUS_UNKNOWN
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@ExerciseTrackedStatus
@Suppress("WrongConstant")
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseType.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseType.kt
index 5f9affbe..0d2b3c3 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseType.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseType.kt
@@ -45,7 +45,6 @@
return id
}
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public fun toProto(): DataProto.ExerciseType =
DataProto.ExerciseType.forNumber(id) ?: DataProto.ExerciseType.EXERCISE_TYPE_UNKNOWN
@@ -282,7 +281,6 @@
return exerciseType ?: UNKNOWN
}
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public fun fromProto(proto: DataProto.ExerciseType): ExerciseType = fromId(proto.number)
}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseUpdate.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseUpdate.kt
index a5b8ea1..104dd19 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseUpdate.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseUpdate.kt
@@ -71,7 +71,6 @@
*/
public val startTime: Instant? = null,
) {
- /** @hide */
@RestrictTo(Scope.LIBRARY)
public constructor(
proto: DataProto.ExerciseUpdate
@@ -123,7 +122,6 @@
public val activeDuration: Duration,
) {
- /** @hide */
@RestrictTo(Scope.LIBRARY)
internal fun toProto(): DataProto.ExerciseUpdate.ActiveDurationCheckpoint =
DataProto.ExerciseUpdate.ActiveDurationCheckpoint.newBuilder()
@@ -153,7 +151,6 @@
}
internal companion object {
- /** @hide */
@RestrictTo(Scope.LIBRARY)
internal fun fromProto(
proto: DataProto.ExerciseUpdate.ActiveDurationCheckpoint
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/HeartRateAccuracy.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/HeartRateAccuracy.kt
index 3756fbe..56c0aaa 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/HeartRateAccuracy.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/HeartRateAccuracy.kt
@@ -44,7 +44,6 @@
override fun toString(): String = name
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun toProto(): SensorStatusProto =
SensorStatusProto.forNumber(id) ?: SensorStatusProto.HR_ACCURACY_SENSOR_STATUS_UNKNOWN
@@ -91,7 +90,6 @@
ACCURACY_HIGH,
)
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public fun fromProto(proto: SensorStatusProto): SensorStatus =
VALUES.firstOrNull { it.id == proto.number } ?: UNKNOWN
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationAvailability.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationAvailability.kt
index 55c7bda..a9370f0a 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationAvailability.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationAvailability.kt
@@ -39,7 +39,6 @@
override fun hashCode(): Int = id
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public override fun toProto(): DataProto.Availability =
DataProto.Availability.newBuilder()
@@ -83,7 +82,6 @@
@JvmStatic
public fun fromId(id: Int): LocationAvailability? = VALUES.firstOrNull { it.id == id }
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun fromProto(proto: LocationAvailabilityProto): LocationAvailability =
fromId(proto.number) ?: UNKNOWN
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
index 72c8603..bfb1d9a 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
@@ -74,7 +74,6 @@
/**
* The frequency at which passive goals should be triggered.
*
- * @hide
*/
@Retention(AnnotationRetention.SOURCE)
@IntDef(
@@ -96,14 +95,12 @@
*/
const val REPEATED: Int = 2
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun @receiver:TriggerFrequency
Int.toProto(): PassiveGoalProto.TriggerFrequency =
PassiveGoalProto.TriggerFrequency.forNumber(this)
?: PassiveGoalProto.TriggerFrequency.TRIGGER_FREQUENCY_UNKNOWN
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TriggerFrequency
@Suppress("WrongConstant")
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ProtoParcelable.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ProtoParcelable.kt
index 8ef1bea..10274fc 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ProtoParcelable.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ProtoParcelable.kt
@@ -28,7 +28,6 @@
* Provided [proto] represents everything important to subclasses, they need not implement [equals]
* and [hashCode].
*
- * @hide
*/
@Suppress("ParcelCreator", "ParcelNotFinal")
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -68,7 +67,6 @@
/**
* Constructs and returns a [Creator] based on the provided [parser] accepting a [ByteArray]
* .
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public inline fun <reified U : ProtoParcelable<*>> newCreator(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/UserActivityState.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/UserActivityState.kt
index cc049c5..5df182a 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/UserActivityState.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/UserActivityState.kt
@@ -34,7 +34,6 @@
override fun toString(): String = name
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal fun toProto(): UserActivityStateProto =
UserActivityStateProto.forNumber(id) ?: UserActivityStateProto.USER_ACTIVITY_STATE_UNKNOWN
@@ -63,7 +62,6 @@
public val USER_ACTIVITY_ASLEEP: UserActivityState =
UserActivityState(3, "USER_ACTIVITY_ASLEEP")
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmField
public val VALUES: List<UserActivityState> =
@@ -74,7 +72,6 @@
USER_ACTIVITY_ASLEEP,
)
- /** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public fun fromProto(proto: UserActivityStateProto): UserActivityState =
VALUES.firstOrNull { it.id == proto.number } ?: USER_ACTIVITY_UNKNOWN
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/IpcConstants.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/IpcConstants.kt
index 48fae70..a82a2fe 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/IpcConstants.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/IpcConstants.kt
@@ -1,10 +1,12 @@
package androidx.health.services.client.impl
+import androidx.annotation.RestrictTo
+
/**
* Collection of constants used for IPC.
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public object IpcConstants {
public const val SERVICE_PACKAGE_NAME: String = "com.google.android.wearable.healthservices"
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/MeasureCallbackStub.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/MeasureCallbackStub.kt
index 18f97db..6e674e1 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/MeasureCallbackStub.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/MeasureCallbackStub.kt
@@ -36,7 +36,6 @@
/**
* A stub implementation for IMeasureCallback.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class MeasureCallbackStub
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
index bfd3d63..faa7b30 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
@@ -55,7 +55,6 @@
/**
* [ExerciseClient] implementation that is backed by Health Services.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class ServiceBackedExerciseClient(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt
index f7f7f76..7ebf6f7 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt
@@ -44,7 +44,6 @@
/**
* [MeasureClient] implementation that is backed by Health Services.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt
index 391922f..f06ddb6 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt
@@ -47,7 +47,6 @@
/**
* [PassiveMonitoringClient] implementation that is backed by Health Services.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/MeasureCallbackEvent.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/MeasureCallbackEvent.kt
index 30c1849..d3d42a0 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/MeasureCallbackEvent.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/MeasureCallbackEvent.kt
@@ -10,7 +10,6 @@
/**
* An event representing a `MeasureCallback` invocation.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class MeasureCallbackEvent(public override val proto: ListenerProto) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveCallbackEvent.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveCallbackEvent.kt
index 337dcee..5ae88fc 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveCallbackEvent.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveCallbackEvent.kt
@@ -9,7 +9,6 @@
/**
* An event representing a `PassiveMonitoringCallback` invocation.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PassiveCallbackEvent(public override val proto: EventProto) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveListenerEvent.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveListenerEvent.kt
index cc216a8..5a01c66 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveListenerEvent.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/event/PassiveListenerEvent.kt
@@ -30,7 +30,6 @@
/**
* An event representing a [PassiveListenerCallback] or [PassiveListenerService] invocation.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class PassiveListenerEvent(public override val proto: EventProto) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
index 4d449e9..b99e192 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
@@ -26,7 +26,6 @@
/**
* A callback for ipc invocations dealing with [ExerciseInfo].
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ExerciseInfoCallback(private val resultFuture: SettableFuture<ExerciseInfo>) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/HsConnectionManager.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/HsConnectionManager.kt
index 7c19ca2..d1439d2 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/HsConnectionManager.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/HsConnectionManager.kt
@@ -28,7 +28,6 @@
/**
* Utility to return an instance of connection manager.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public object HsConnectionManager {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
index 8ba8279..d0163f8 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
@@ -25,7 +25,6 @@
/**
* A generic callback for ipc invocations.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal open class StatusCallback(private val resultFuture: SettableFuture<Void?>) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/AutoPauseAndResumeConfigRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/AutoPauseAndResumeConfigRequest.kt
index 5219307..996d032 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/AutoPauseAndResumeConfigRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/AutoPauseAndResumeConfigRequest.kt
@@ -24,7 +24,6 @@
/**
* Request for enabling/disabling auto pause/resume.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class AutoPauseAndResumeConfigRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt
index 6179e7b..085363b 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for updating batching mode of an exercise.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class BatchingModeConfigRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/CapabilitiesRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/CapabilitiesRequest.kt
index 875a79b..3477090 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/CapabilitiesRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/CapabilitiesRequest.kt
@@ -24,7 +24,6 @@
/**
* Request for capabilities.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class CapabilitiesRequest(public val packageName: String) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/ExerciseGoalRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/ExerciseGoalRequest.kt
index 4838af7..664a955 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/ExerciseGoalRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/ExerciseGoalRequest.kt
@@ -24,7 +24,6 @@
/**
* Request for adding a [ExerciseGoal] to an exercise.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public data class ExerciseGoalRequest(val packageName: String, val exerciseGoal: ExerciseGoal<*>) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/FlushRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/FlushRequest.kt
index 86c53bf..a9ee606 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/FlushRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/FlushRequest.kt
@@ -24,7 +24,6 @@
/**
* Request to flush data metrics.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class FlushRequest(public val packageName: String) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureRegistrationRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureRegistrationRequest.kt
index 4cc8131..6fd3e84 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureRegistrationRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureRegistrationRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for measure registration.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class MeasureRegistrationRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureUnregistrationRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureUnregistrationRequest.kt
index 6c07a1f..e1027d7 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureUnregistrationRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/MeasureUnregistrationRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for measure unregistration.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class MeasureUnregistrationRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerCallbackRegistrationRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerCallbackRegistrationRequest.kt
index 40303af..92062da 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerCallbackRegistrationRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerCallbackRegistrationRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for background registration.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class PassiveListenerCallbackRegistrationRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerServiceRegistrationRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerServiceRegistrationRequest.kt
index b280b85..d162f2d 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerServiceRegistrationRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PassiveListenerServiceRegistrationRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for background registration.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class PassiveListenerServiceRegistrationRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PrepareExerciseRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PrepareExerciseRequest.kt
index 42a659c..def39f8 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PrepareExerciseRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/PrepareExerciseRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for preparing for an exercise.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PrepareExerciseRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/StartExerciseRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/StartExerciseRequest.kt
index 617ce951..ea36ce4 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/StartExerciseRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/StartExerciseRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for starting an exercise.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class StartExerciseRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
index 8416212..596413b 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
@@ -25,7 +25,6 @@
/**
* Request for updating exercise type configuration in an [ExerciseTypeConfig].
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
class UpdateExerciseTypeConfigRequest(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseCapabilitiesResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseCapabilitiesResponse.kt
index c7ce7be..82910bc 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseCapabilitiesResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseCapabilitiesResponse.kt
@@ -26,7 +26,6 @@
* Response containing the [ExerciseCapabilities] of the Health Services exercise client on the
* device.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ExerciseCapabilitiesResponse(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseInfoResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseInfoResponse.kt
index b814697..2a0936f8 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseInfoResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseInfoResponse.kt
@@ -25,7 +25,6 @@
/**
* Response containing [ExerciseInfo] when changed.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ExerciseInfoResponse(public val exerciseInfo: ExerciseInfo) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseLapSummaryResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseLapSummaryResponse.kt
index c8e24f8..8621ad2 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseLapSummaryResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseLapSummaryResponse.kt
@@ -25,7 +25,6 @@
/**
* Response containing [ExerciseLapSummary] when it's updated.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ExerciseLapSummaryResponse(public val exerciseLapSummary: ExerciseLapSummary) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseUpdateResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseUpdateResponse.kt
index 377053e..da1817b 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseUpdateResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/ExerciseUpdateResponse.kt
@@ -25,7 +25,6 @@
/**
* Response containing [ExerciseUpdate] when it's updated.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class ExerciseUpdateResponse(public val exerciseUpdate: ExerciseUpdate) :
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/HealthEventResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/HealthEventResponse.kt
index 600f26e..e8496b7 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/HealthEventResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/HealthEventResponse.kt
@@ -24,12 +24,10 @@
/**
* Response containing a [HealthEvent].
*
- * @hide
*/
internal class HealthEventResponse(public val healthEvent: HealthEvent) :
ProtoParcelable<ResponsesProto.HealthEventResponse>() {
- /** @hide */
public constructor(
proto: ResponsesProto.HealthEventResponse
) : this(HealthEvent(proto.healthEvent))
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/MeasureCapabilitiesResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/MeasureCapabilitiesResponse.kt
index c715092..245b7b0 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/MeasureCapabilitiesResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/MeasureCapabilitiesResponse.kt
@@ -25,7 +25,6 @@
/**
* Response containing the [MeasureCapabilities] of the device.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class MeasureCapabilitiesResponse(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringCapabilitiesResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringCapabilitiesResponse.kt
index dd38942..e2164f2 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringCapabilitiesResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringCapabilitiesResponse.kt
@@ -25,7 +25,6 @@
/**
* Response containing the [PassiveMonitoringCapabilities] of the device.
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PassiveMonitoringCapabilitiesResponse(
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringGoalResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringGoalResponse.kt
index 186b9c4..013149e 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringGoalResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringGoalResponse.kt
@@ -25,13 +25,11 @@
/**
* Response containing an achieved [PassiveGoal].
*
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PassiveMonitoringGoalResponse(public val passiveGoal: PassiveGoal) :
ProtoParcelable<ResponsesProto.PassiveMonitoringGoalResponse>() {
- /** @hide */
public constructor(
proto: ResponsesProto.PassiveMonitoringGoalResponse
) : this(PassiveGoal(proto.goal))
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringUpdateResponse.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringUpdateResponse.kt
index 0deaf87..27f9b603 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringUpdateResponse.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/response/PassiveMonitoringUpdateResponse.kt
@@ -17,6 +17,7 @@
package androidx.health.services.client.impl.response
import android.os.Parcelable
+import androidx.annotation.RestrictTo
import androidx.health.services.client.data.PassiveMonitoringUpdate
import androidx.health.services.client.data.ProtoParcelable
import androidx.health.services.client.proto.ResponsesProto
@@ -24,13 +25,12 @@
/**
* Response containing [PassiveMonitoringUpdate].
*
- * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PassiveMonitoringUpdateResponse(
public val passiveMonitoringUpdate: PassiveMonitoringUpdate
) : ProtoParcelable<ResponsesProto.PassiveMonitoringUpdateResponse>() {
- /** @hide */
public constructor(
proto: ResponsesProto.PassiveMonitoringUpdateResponse
) : this(PassiveMonitoringUpdate(proto.update))
diff --git a/input/input-motionprediction/api/1.0.0-beta01.txt b/input/input-motionprediction/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
diff --git a/input/input-motionprediction/api/current.txt b/input/input-motionprediction/api/current.txt
index 6611119..b0eef8e 100644
--- a/input/input-motionprediction/api/current.txt
+++ b/input/input-motionprediction/api/current.txt
@@ -1,8 +1,7 @@
// Signature format: 4.0
package androidx.input.motionprediction {
- public interface MotionEventPredictor extends java.lang.AutoCloseable {
- method public void close();
+ public interface MotionEventPredictor {
method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
method public android.view.MotionEvent? predict();
method public void record(android.view.MotionEvent);
diff --git a/input/input-motionprediction/api/public_plus_experimental_1.0.0-beta01.txt b/input/input-motionprediction/api/public_plus_experimental_1.0.0-beta01.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/public_plus_experimental_1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
diff --git a/input/input-motionprediction/api/public_plus_experimental_current.txt b/input/input-motionprediction/api/public_plus_experimental_current.txt
index 6611119..b0eef8e 100644
--- a/input/input-motionprediction/api/public_plus_experimental_current.txt
+++ b/input/input-motionprediction/api/public_plus_experimental_current.txt
@@ -1,8 +1,7 @@
// Signature format: 4.0
package androidx.input.motionprediction {
- public interface MotionEventPredictor extends java.lang.AutoCloseable {
- method public void close();
+ public interface MotionEventPredictor {
method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
method public android.view.MotionEvent? predict();
method public void record(android.view.MotionEvent);
diff --git a/input/input-motionprediction/api/res-1.0.0-beta01.txt b/input/input-motionprediction/api/res-1.0.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/input/input-motionprediction/api/res-1.0.0-beta01.txt
diff --git a/input/input-motionprediction/api/restricted_1.0.0-beta01.txt b/input/input-motionprediction/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..b0eef8e
--- /dev/null
+++ b/input/input-motionprediction/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,11 @@
+// Signature format: 4.0
+package androidx.input.motionprediction {
+
+ public interface MotionEventPredictor {
+ method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
+ method public android.view.MotionEvent? predict();
+ method public void record(android.view.MotionEvent);
+ }
+
+}
+
diff --git a/input/input-motionprediction/api/restricted_current.txt b/input/input-motionprediction/api/restricted_current.txt
index 6611119..b0eef8e 100644
--- a/input/input-motionprediction/api/restricted_current.txt
+++ b/input/input-motionprediction/api/restricted_current.txt
@@ -1,8 +1,7 @@
// Signature format: 4.0
package androidx.input.motionprediction {
- public interface MotionEventPredictor extends java.lang.AutoCloseable {
- method public void close();
+ public interface MotionEventPredictor {
method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
method public android.view.MotionEvent? predict();
method public void record(android.view.MotionEvent);
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
index 39e1c14..e976419 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
@@ -30,16 +30,16 @@
* {@link #newInstance(android.view.View)}; put the motion events you receive into it with
* {@link #record(android.view.MotionEvent)}, and call {@link #predict()} to retrieve the
* predicted {@link android.view.MotionEvent} that would occur at the moment the next frame is
- * rendered on the display. Once no more predictions are needed, call {@link #close()} to stop it
- * and clean up resources.
+ * rendered on the display.
*/
-public interface MotionEventPredictor extends AutoCloseable {
+public interface MotionEventPredictor {
/**
* Record a user's movement to the predictor. You should call this for every
* {@link android.view.MotionEvent} that is received by the associated
* {@link android.view.View}.
* @param event the {@link android.view.MotionEvent} the associated view received and that
* needs to be recorded.
+ * @throws IllegalArgumentException if an inconsistent MotionEvent stream is sent.
*/
void record(@NonNull MotionEvent event);
@@ -52,18 +52,11 @@
MotionEvent predict();
/**
- * Notify the predictor that no more predictions are needed. Any subsequent call to
- * {@link #predict()} will return null.
- */
- @Override
- void close();
-
- /**
* Create a new motion predictor associated to a specific {@link android.view.View}
* @param view the view to associated to this predictor
* @return the new predictor instance
*/
static @NonNull MotionEventPredictor newInstance(@NonNull View view) {
- return new KalmanMotionEventPredictor();
+ return new KalmanMotionEventPredictor(view.getContext());
}
}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
index 91bec15..1b7d73b 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
@@ -18,46 +18,36 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import android.content.Context;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.input.motionprediction.MotionEventPredictor;
+import androidx.input.motionprediction.utils.PredictionEstimator;
/**
*/
@RestrictTo(LIBRARY)
public class KalmanMotionEventPredictor implements MotionEventPredictor {
- private MultiPointerPredictor mMultiPointerPredictor = new MultiPointerPredictor();
+ private final MultiPointerPredictor mMultiPointerPredictor = new MultiPointerPredictor();
+ private final PredictionEstimator mPredictionEstimator;
- public KalmanMotionEventPredictor() {
- // 1 may seem arbitrary, but this basically tells the predictor to
- // just predict the next MotionEvent.
- // This will need to change as we want to build a prediction depending
- // on the expected time that the frame will arrive to the screen.
- mMultiPointerPredictor.setPredictionTarget(1);
+ public KalmanMotionEventPredictor(@NonNull Context context) {
+ mPredictionEstimator = new PredictionEstimator(context);
}
@Override
public void record(@NonNull MotionEvent event) {
- if (mMultiPointerPredictor == null) {
- return;
- }
+ mPredictionEstimator.record(event);
mMultiPointerPredictor.onTouchEvent(event);
}
@Nullable
@Override
public MotionEvent predict() {
- if (mMultiPointerPredictor == null) {
- return null;
- }
- return mMultiPointerPredictor.predict();
- }
-
- @Override
- public void close() {
- mMultiPointerPredictor = null;
+ final int predictionTimeDelta = mPredictionEstimator.estimate();
+ return mMultiPointerPredictor.predict(predictionTimeDelta);
}
}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
index 87a35e6..3a8ff60 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
@@ -30,13 +30,6 @@
*/
@RestrictTo(LIBRARY)
public interface KalmanPredictor {
-
- /** Gets the current prediction target */
- int getPredictionTarget();
-
- /** Sets the current prediction target */
- void setPredictionTarget(int predictionTargetMillis);
-
/** Sets the report rate */
void setReportRate(int reportRateMs);
@@ -45,5 +38,5 @@
/** @return null if not possible to make a prediction. */
@Nullable
- MotionEvent predict();
+ MotionEvent predict(int predictionTargetMillis);
}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
index cb6c3a9..3f02764 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
@@ -36,29 +36,11 @@
private static final boolean DEBUG_PREDICTION = Log.isLoggable(TAG, Log.DEBUG);
private final SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
- private int mPredictionTargetMs = 0;
private int mReportRateMs = 0;
public MultiPointerPredictor() {}
@Override
- public int getPredictionTarget() {
- return mPredictionTargetMs;
- }
-
- @Override
- public void setPredictionTarget(int predictionTargetMillis) {
- if (predictionTargetMillis < 0) {
- predictionTargetMillis = 0;
- }
- mPredictionTargetMs = predictionTargetMillis;
-
- for (int i = 0; i < mPredictorMap.size(); ++i) {
- mPredictorMap.valueAt(i).setPredictionTarget(predictionTargetMillis);
- }
- }
-
- @Override
public void setReportRate(int reportRateMs) {
if (reportRateMs <= 0) {
throw new IllegalArgumentException(
@@ -78,7 +60,6 @@
int pointerId = event.getPointerId(actionIndex);
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
SinglePointerPredictor predictor = new SinglePointerPredictor();
- predictor.setPredictionTarget(mPredictionTargetMs);
if (mReportRateMs > 0) {
predictor.setReportRate(mReportRateMs);
}
@@ -113,7 +94,7 @@
/** Support eventTime */
@Override
- public @Nullable MotionEvent predict() {
+ public @Nullable MotionEvent predict(int predictionTargetMs) {
final int pointerCount = mPredictorMap.size();
// Shortcut for likely case where only zero or one pointer is on the screen
// this logic exists only to make sure logic when one pointer is on screen then
@@ -128,7 +109,7 @@
}
if (pointerCount == 1) {
SinglePointerPredictor predictor = mPredictorMap.valueAt(0);
- MotionEvent predictedEv = predictor.predict();
+ MotionEvent predictedEv = predictor.predict(predictionTargetMs);
if (DEBUG_PREDICTION) {
Log.d(TAG, "predict() -> MotionEvent: " + predictedEv);
}
@@ -141,7 +122,7 @@
for (int i = 0; i < pointerCount; ++i) {
pointerIds[i] = mPredictorMap.keyAt(i);
SinglePointerPredictor predictor = mPredictorMap.valueAt(i);
- singlePointerEvents[i] = predictor.predict();
+ singlePointerEvents[i] = predictor.predict(predictionTargetMs);
// If predictor consumer expect more sample, generate sample where position and
// pressure are constant
singlePointerEvents[i] = predictor.appendPredictedEvent(singlePointerEvents[i]);
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
index 0ba5bc6..74dfc7c 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
@@ -60,9 +60,6 @@
// Minimum number of Kalman filter samples needed for predicting the next point
private static final int MIN_KALMAN_FILTER_ITERATIONS = 4;
- // Target time in milliseconds to predict.
- private float mPredictionTargetMs = 0.0f;
-
// The Kalman filter is tuned to smooth noise while maintaining fast reaction to direction
// changes. The stronger the filter, the smoother the prediction result will be, at the
// cost of possible prediction errors.
@@ -148,23 +145,6 @@
}
@Override
- public int getPredictionTarget() {
- // Prediction target should always be an int, so no precision lost in the cast
- return (int) mPredictionTargetMs;
- }
-
- @Override
- public void setPredictionTarget(int predictionTargetMillis) {
- if (predictionTargetMillis < 0) {
- predictionTargetMillis = 0;
- }
- mPredictionTargetMs = predictionTargetMillis;
- if (mReportRates == null) {
- mExpectedPredictionSampleSize = (int) Math.ceil(mPredictionTargetMs / mReportRateMs);
- }
- }
-
- @Override
public void setReportRate(int reportRateMs) {
if (reportRateMs <= 0) {
throw new IllegalArgumentException(
@@ -172,8 +152,6 @@
}
mReportRateMs = reportRateMs;
mReportRates = null;
-
- mExpectedPredictionSampleSize = (int) Math.ceil(mPredictionTargetMs / mReportRateMs);
}
@Override
@@ -205,7 +183,11 @@
}
@Override
- public @Nullable MotionEvent predict() {
+ public @Nullable MotionEvent predict(int predictionTargetMs) {
+ if (mReportRates == null) {
+ mExpectedPredictionSampleSize = (int) Math.ceil(predictionTargetMs / mReportRateMs);
+ }
+
if (mExpectedPredictionSampleSize == -1
&& mKalman.getNumIterations() < MIN_KALMAN_FILTER_ITERATIONS) {
return null;
@@ -236,7 +218,7 @@
// Project physical state of the pen into the future.
int predictionTargetInSamples =
- (int) Math.ceil(mPredictionTargetMs / mReportRateMs * confidenceFactor);
+ (int) Math.ceil(predictionTargetMs / mReportRateMs * confidenceFactor);
// Normally this should always be false as confidenceFactor should be less than 1.0
if (mExpectedPredictionSampleSize != -1
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java
new file mode 100644
index 0000000..3ed9a1b
--- /dev/null
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/utils/PredictionEstimator.java
@@ -0,0 +1,140 @@
+/*
+ * 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.input.motionprediction.utils;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.SystemClock;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ */
+@SuppressWarnings("deprecation")
+@RestrictTo(LIBRARY)
+public class PredictionEstimator {
+ private static final int MAX_PREDICTION_MS = 32;
+ private static final int LEGACY_FRAME_TIME_MS = 16;
+ private static final int MS_IN_A_SECOND = 1000;
+
+ private long mLastEventTime = -1;
+ private final float mFrameTimeMs;
+
+ public PredictionEstimator(@NonNull Context context) {
+ mFrameTimeMs = getFastestFrameTimeMs(context);
+ }
+
+ /** Records the needed information from the event to calculate the prediction. */
+ public void record(@NonNull MotionEvent event) {
+ mLastEventTime = event.getEventTime();
+ }
+
+ /** Return the estimated amount of prediction needed. */
+ public int estimate() {
+ if (mLastEventTime <= 0) {
+ return (int) mFrameTimeMs;
+ }
+ // The amount of prediction is the estimated amount of time it will take to land the
+ // information on the screen from now, plus the time since the last recorded MotionEvent
+ int estimatedMs = (int) (SystemClock.uptimeMillis() - mLastEventTime + mFrameTimeMs);
+ return Math.min(MAX_PREDICTION_MS, estimatedMs);
+ }
+
+ private Display getDisplayForContext(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Api30Impl.getDisplayForContext(context);
+ }
+ return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ }
+
+ private float getFastestFrameTimeMs(Context context) {
+ Display defaultDisplay = getDisplayForContext(context);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return Api23Impl.getFastestFrameTimeMs(defaultDisplay);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return Api21Impl.getFastestFrameTimeMs(defaultDisplay);
+ } else {
+ return LEGACY_FRAME_TIME_MS;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ static class Api21Impl {
+ private Api21Impl() {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static float getFastestFrameTimeMs(Display display) {
+ float[] refreshRates = display.getSupportedRefreshRates();
+ float largestRefreshRate = refreshRates[0];
+
+ for (int c = 1; c < refreshRates.length; c++) {
+ if (refreshRates[c] > largestRefreshRate) {
+ largestRefreshRate = refreshRates[c];
+ }
+ }
+
+ return MS_IN_A_SECOND / largestRefreshRate;
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ static class Api23Impl {
+ private Api23Impl() {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static float getFastestFrameTimeMs(Display display) {
+ Display.Mode[] displayModes = display.getSupportedModes();
+ float largestRefreshRate = displayModes[0].getRefreshRate();
+
+ for (int c = 1; c < displayModes.length; c++) {
+ float currentRefreshRate = displayModes[c].getRefreshRate();
+ if (currentRefreshRate > largestRefreshRate) {
+ largestRefreshRate = currentRefreshRate;
+ }
+ }
+
+ return MS_IN_A_SECOND / largestRefreshRate;
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ static class Api30Impl {
+ private Api30Impl() {
+ // Not instantiable
+ }
+
+ @DoNotInline
+ static Display getDisplayForContext(Context context) {
+ return context.getDisplay();
+ }
+ }
+}
diff --git a/libraryversions.toml b/libraryversions.toml
index 88e6113..07010bd 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -63,7 +63,8 @@
FUTURES = "1.2.0-alpha01"
GLANCE = "1.0.0-alpha06"
GLANCE_TEMPLATE = "1.0.0-alpha01"
-GRAPHICS = "1.0.0-alpha03"
+GRAPHICS_CORE = "1.0.0-alpha03"
+GRAPHICS_SHAPES = "1.0.0-alpha01"
GRAPHICS_FILTERS = "1.0.0-alpha01"
GRIDLAYOUT = "1.1.0-alpha01"
HEALTH_CONNECT = "1.0.0-alpha11"
@@ -71,7 +72,7 @@
HEIFWRITER = "1.1.0-alpha02"
HILT = "1.1.0-alpha02"
HILT_NAVIGATION_COMPOSE = "1.1.0-alpha02"
-INPUT_MOTIONPREDICTION = "1.0.0-alpha03"
+INPUT_MOTIONPREDICTION = "1.0.0-beta01"
INSPECTION = "1.0.0"
INTERPOLATOR = "1.1.0-alpha01"
JAVASCRIPTENGINE = "1.0.0-alpha04"
@@ -207,7 +208,7 @@
FRAGMENT = { group = "androidx.fragment", atomicGroupVersion = "versions.FRAGMENT" }
GLANCE = { group = "androidx.glance", atomicGroupVersion = "versions.GLANCE" }
GLANCE_TEMPLATE = { group = "androidx.template", atomicGroupVersion = "versions.GLANCE_TEMPLATE" }
-GRAPHICS = { group = "androidx.graphics", atomicGroupVersion = "versions.GRAPHICS" }
+GRAPHICS = { group = "androidx.graphics"}
GRAPHICS_FILTERS = { group = "androidx.graphics.filters", atomicGroupVersion = "versions.GRAPHICS_FILTERS" }
GRIDLAYOUT = { group = "androidx.gridlayout", atomicGroupVersion = "versions.GRIDLAYOUT" }
HEALTH = { group = "androidx.health" }
@@ -248,7 +249,6 @@
ROOM = { group = "androidx.room", atomicGroupVersion = "versions.ROOM" }
SAVEDSTATE = { group = "androidx.savedstate", atomicGroupVersion = "versions.SAVEDSTATE" }
SECURITY = { group = "androidx.security" }
-SHAPES = { group = "androidx.graphics.shapes", atomicGroupVersion = "versions.GRAPHICS" }
SHARETARGET = { group = "androidx.sharetarget", atomicGroupVersion = "versions.SHARETARGET" }
SLICE = { group = "androidx.slice" }
SLIDINGPANELAYOUT = { group = "androidx.slidingpanelayout", atomicGroupVersion = "versions.SLIDINGPANELAYOUT" }
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/MediaDescriptionCompat.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/MediaDescriptionCompat.aidl
new file mode 100644
index 0000000..587736c
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/MediaDescriptionCompat.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2017, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media;
+@JavaOnlyStableParcelable
+parcelable MediaDescriptionCompat;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/MediaMetadataCompat.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/MediaMetadataCompat.aidl
new file mode 100644
index 0000000..d6d7965
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/MediaMetadataCompat.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media;
+@JavaOnlyStableParcelable
+parcelable MediaMetadataCompat;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/RatingCompat.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/RatingCompat.aidl
new file mode 100644
index 0000000..e08b9e1
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/RatingCompat.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media;
+@JavaOnlyStableParcelable
+parcelable RatingCompat;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/IMediaControllerCallback.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/IMediaControllerCallback.aidl
new file mode 100644
index 0000000..628871f
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/IMediaControllerCallback.aidl
@@ -0,0 +1,49 @@
+/* Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+/* @hide */
+interface IMediaControllerCallback {
+ oneway void onEvent(String event, in android.os.Bundle extras);
+ oneway void onSessionDestroyed();
+ oneway void onPlaybackStateChanged(in android.support.v4.media.session.PlaybackStateCompat state);
+ oneway void onMetadataChanged(in android.support.v4.media.MediaMetadataCompat metadata);
+ oneway void onQueueChanged(in List<android.support.v4.media.session.MediaSessionCompat.QueueItem> queue);
+ oneway void onQueueTitleChanged(CharSequence title);
+ oneway void onExtrasChanged(in android.os.Bundle extras);
+ oneway void onVolumeInfoChanged(in android.support.v4.media.session.ParcelableVolumeInfo info);
+ oneway void onRepeatModeChanged(int repeatMode);
+ oneway void onShuffleModeChangedRemoved(boolean enabled);
+ oneway void onCaptioningEnabledChanged(boolean enabled);
+ oneway void onShuffleModeChanged(int shuffleMode);
+ oneway void onSessionReady();
+}
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/IMediaSession.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/IMediaSession.aidl
new file mode 100644
index 0000000..0a33361
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/IMediaSession.aidl
@@ -0,0 +1,87 @@
+/* Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+/* @hide */
+interface IMediaSession {
+ void sendCommand(String command, in android.os.Bundle args, in android.support.v4.media.session.MediaSessionCompat.ResultReceiverWrapper cb) = 0;
+ boolean sendMediaButton(in android.view.KeyEvent mediaButton) = 1;
+ void registerCallbackListener(in android.support.v4.media.session.IMediaControllerCallback cb) = 2;
+ void unregisterCallbackListener(in android.support.v4.media.session.IMediaControllerCallback cb) = 3;
+ boolean isTransportControlEnabled() = 4;
+ String getPackageName() = 5;
+ String getTag() = 6;
+ android.app.PendingIntent getLaunchPendingIntent() = 7;
+ long getFlags() = 8;
+ android.support.v4.media.session.ParcelableVolumeInfo getVolumeAttributes() = 9;
+ void adjustVolume(int direction, int flags, String packageName) = 10;
+ void setVolumeTo(int value, int flags, String packageName) = 11;
+ android.support.v4.media.MediaMetadataCompat getMetadata() = 26;
+ android.support.v4.media.session.PlaybackStateCompat getPlaybackState() = 27;
+ List<android.support.v4.media.session.MediaSessionCompat.QueueItem> getQueue() = 28;
+ CharSequence getQueueTitle() = 29;
+ android.os.Bundle getExtras() = 30;
+ int getRatingType() = 31;
+ boolean isCaptioningEnabled() = 44;
+ int getRepeatMode() = 36;
+ boolean isShuffleModeEnabledRemoved() = 37;
+ int getShuffleMode() = 46;
+ void addQueueItem(in android.support.v4.media.MediaDescriptionCompat description) = 40;
+ void addQueueItemAt(in android.support.v4.media.MediaDescriptionCompat description, int index) = 41;
+ void removeQueueItem(in android.support.v4.media.MediaDescriptionCompat description) = 42;
+ void removeQueueItemAt(int index) = 43;
+ android.os.Bundle getSessionInfo() = 49;
+ void prepare() = 32;
+ void prepareFromMediaId(String uri, in android.os.Bundle extras) = 33;
+ void prepareFromSearch(String string, in android.os.Bundle extras) = 34;
+ void prepareFromUri(in android.net.Uri uri, in android.os.Bundle extras) = 35;
+ void play() = 12;
+ void playFromMediaId(String uri, in android.os.Bundle extras) = 13;
+ void playFromSearch(String string, in android.os.Bundle extras) = 14;
+ void playFromUri(in android.net.Uri uri, in android.os.Bundle extras) = 15;
+ void skipToQueueItem(long id) = 16;
+ void pause() = 17;
+ void stop() = 18;
+ void next() = 19;
+ void previous() = 20;
+ void fastForward() = 21;
+ void rewind() = 22;
+ void seekTo(long pos) = 23;
+ void rate(in android.support.v4.media.RatingCompat rating) = 24;
+ void rateWithExtras(in android.support.v4.media.RatingCompat rating, in android.os.Bundle extras) = 50;
+ void setPlaybackSpeed(float speed) = 48;
+ void setCaptioningEnabled(boolean enabled) = 45;
+ void setRepeatMode(int repeatMode) = 38;
+ void setShuffleModeEnabledRemoved(boolean shuffleMode) = 39;
+ void setShuffleMode(int shuffleMode) = 47;
+ void sendCustomAction(String action, in android.os.Bundle args) = 25;
+}
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.QueueItem.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.QueueItem.aidl
new file mode 100644
index 0000000..375b6d7
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.QueueItem.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+@JavaOnlyStableParcelable
+parcelable MediaSessionCompat.QueueItem;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.ResultReceiverWrapper.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.ResultReceiverWrapper.aidl
new file mode 100644
index 0000000..62d1ec0
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.ResultReceiverWrapper.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+@JavaOnlyStableParcelable
+parcelable MediaSessionCompat.ResultReceiverWrapper;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.Token.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.Token.aidl
new file mode 100644
index 0000000..490d4d0
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/MediaSessionCompat.Token.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+@JavaOnlyStableParcelable
+parcelable MediaSessionCompat.Token;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/ParcelableVolumeInfo.aidl
new file mode 100644
index 0000000..16d3fe0
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/ParcelableVolumeInfo.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+@JavaOnlyStableParcelable
+parcelable ParcelableVolumeInfo;
diff --git a/media/media/api/aidlRelease/current/android/support/v4/media/session/PlaybackStateCompat.aidl b/media/media/api/aidlRelease/current/android/support/v4/media/session/PlaybackStateCompat.aidl
new file mode 100644
index 0000000..682e2ac
--- /dev/null
+++ b/media/media/api/aidlRelease/current/android/support/v4/media/session/PlaybackStateCompat.aidl
@@ -0,0 +1,35 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.v4.media.session;
+@JavaOnlyStableParcelable
+parcelable PlaybackStateCompat;
diff --git a/media/media/build.gradle b/media/media/build.gradle
index 91a6177..0787b8d 100644
--- a/media/media/build.gradle
+++ b/media/media/build.gradle
@@ -19,6 +19,7 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("androidx.stableaidl")
}
dependencies {
@@ -46,6 +47,10 @@
buildTypes.all {
consumerProguardFiles "proguard-rules.pro"
+
+ stableAidl {
+ version 1
+ }
}
namespace "androidx.media"
}
diff --git a/media/media/src/main/aidl/android/support/v4/media/RatingCompat.aidl b/media/media/src/main/aidl/android/support/v4/media/RatingCompat.aidl
deleted file mode 100644
index 223fd5c..0000000
--- a/media/media/src/main/aidl/android/support/v4/media/RatingCompat.aidl
+++ /dev/null
@@ -1,18 +0,0 @@
-/* Copyright 2014, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT 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 android.support.v4.media;
-
-parcelable RatingCompat;
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/MediaSessionCompat.aidl b/media/media/src/main/aidl/android/support/v4/media/session/MediaSessionCompat.aidl
deleted file mode 100644
index d0c2f6f..0000000
--- a/media/media/src/main/aidl/android/support/v4/media/session/MediaSessionCompat.aidl
+++ /dev/null
@@ -1,20 +0,0 @@
-/* Copyright 2014, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT 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 android.support.v4.media.session;
-
-parcelable MediaSessionCompat.Token;
-parcelable MediaSessionCompat.QueueItem;
-parcelable MediaSessionCompat.ResultReceiverWrapper;
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl b/media/media/src/main/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl
deleted file mode 100644
index 3d4ef59..0000000
--- a/media/media/src/main/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl
+++ /dev/null
@@ -1,18 +0,0 @@
-/* Copyright 2014, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT 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 android.support.v4.media.session;
-
-parcelable PlaybackStateCompat;
diff --git a/media/media/src/main/aidl/android/support/v4/media/MediaDescriptionCompat.aidl b/media/media/src/main/stableAidl/android/support/v4/media/MediaDescriptionCompat.aidl
similarity index 91%
rename from media/media/src/main/aidl/android/support/v4/media/MediaDescriptionCompat.aidl
rename to media/media/src/main/stableAidl/android/support/v4/media/MediaDescriptionCompat.aidl
index f002cdd..c9c0d6d 100644
--- a/media/media/src/main/aidl/android/support/v4/media/MediaDescriptionCompat.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/MediaDescriptionCompat.aidl
@@ -15,4 +15,4 @@
package android.support.v4.media;
-parcelable MediaDescriptionCompat;
+@JavaOnlyStableParcelable parcelable MediaDescriptionCompat;
diff --git a/media/media/src/main/aidl/android/support/v4/media/MediaMetadataCompat.aidl b/media/media/src/main/stableAidl/android/support/v4/media/MediaMetadataCompat.aidl
similarity index 91%
copy from media/media/src/main/aidl/android/support/v4/media/MediaMetadataCompat.aidl
copy to media/media/src/main/stableAidl/android/support/v4/media/MediaMetadataCompat.aidl
index 6d36b97..0c69e05 100644
--- a/media/media/src/main/aidl/android/support/v4/media/MediaMetadataCompat.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/MediaMetadataCompat.aidl
@@ -15,4 +15,4 @@
package android.support.v4.media;
-parcelable MediaMetadataCompat;
+@JavaOnlyStableParcelable parcelable MediaMetadataCompat;
diff --git a/media/media/src/main/aidl/android/support/v4/media/MediaMetadataCompat.aidl b/media/media/src/main/stableAidl/android/support/v4/media/RatingCompat.aidl
similarity index 92%
rename from media/media/src/main/aidl/android/support/v4/media/MediaMetadataCompat.aidl
rename to media/media/src/main/stableAidl/android/support/v4/media/RatingCompat.aidl
index 6d36b97..37f054d 100644
--- a/media/media/src/main/aidl/android/support/v4/media/MediaMetadataCompat.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/RatingCompat.aidl
@@ -15,4 +15,4 @@
package android.support.v4.media;
-parcelable MediaMetadataCompat;
+@JavaOnlyStableParcelable parcelable RatingCompat;
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/IMediaControllerCallback.aidl b/media/media/src/main/stableAidl/android/support/v4/media/session/IMediaControllerCallback.aidl
similarity index 100%
rename from media/media/src/main/aidl/android/support/v4/media/session/IMediaControllerCallback.aidl
rename to media/media/src/main/stableAidl/android/support/v4/media/session/IMediaControllerCallback.aidl
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/IMediaSession.aidl b/media/media/src/main/stableAidl/android/support/v4/media/session/IMediaSession.aidl
similarity index 99%
rename from media/media/src/main/aidl/android/support/v4/media/session/IMediaSession.aidl
rename to media/media/src/main/stableAidl/android/support/v4/media/session/IMediaSession.aidl
index 878ea1c..2b46a0e 100644
--- a/media/media/src/main/aidl/android/support/v4/media/session/IMediaSession.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/session/IMediaSession.aidl
@@ -24,6 +24,7 @@
import android.support.v4.media.session.ParcelableVolumeInfo;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.session.MediaSessionCompat;
+import android.net.Uri;
import android.os.Bundle;
import android.view.KeyEvent;
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/media/media/src/main/stableAidl/android/support/v4/media/session/MediaSessionCompat.aidl
similarity index 75%
copy from media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
copy to media/media/src/main/stableAidl/android/support/v4/media/session/MediaSessionCompat.aidl
index 2e77c4f..25b1b07 100644
--- a/media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/session/MediaSessionCompat.aidl
@@ -15,4 +15,6 @@
package android.support.v4.media.session;
-parcelable ParcelableVolumeInfo;
+@JavaOnlyStableParcelable parcelable MediaSessionCompat.Token;
+@JavaOnlyStableParcelable parcelable MediaSessionCompat.QueueItem;
+@JavaOnlyStableParcelable parcelable MediaSessionCompat.ResultReceiverWrapper;
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/media/media/src/main/stableAidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
similarity index 91%
rename from media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
rename to media/media/src/main/stableAidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
index 2e77c4f..6317531 100644
--- a/media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
@@ -15,4 +15,4 @@
package android.support.v4.media.session;
-parcelable ParcelableVolumeInfo;
+@JavaOnlyStableParcelable parcelable ParcelableVolumeInfo;
diff --git a/media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/media/media/src/main/stableAidl/android/support/v4/media/session/PlaybackStateCompat.aidl
similarity index 91%
copy from media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
copy to media/media/src/main/stableAidl/android/support/v4/media/session/PlaybackStateCompat.aidl
index 2e77c4f..63add46 100644
--- a/media/media/src/main/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl
+++ b/media/media/src/main/stableAidl/android/support/v4/media/session/PlaybackStateCompat.aidl
@@ -15,4 +15,4 @@
package android.support.v4.media.session;
-parcelable ParcelableVolumeInfo;
+@JavaOnlyStableParcelable parcelable PlaybackStateCompat;
diff --git a/media/media/src/main/stableAidlImports/android/app/PendingIntent.aidl b/media/media/src/main/stableAidlImports/android/app/PendingIntent.aidl
new file mode 100644
index 0000000..4212dfa
--- /dev/null
+++ b/media/media/src/main/stableAidlImports/android/app/PendingIntent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+@JavaOnlyStableParcelable parcelable PendingIntent;
diff --git a/media/media/src/main/stableAidlImports/android/content/Intent.aidl b/media/media/src/main/stableAidlImports/android/content/Intent.aidl
new file mode 100644
index 0000000..0c8c241
--- /dev/null
+++ b/media/media/src/main/stableAidlImports/android/content/Intent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+@JavaOnlyStableParcelable parcelable Intent;
diff --git a/media/media/src/main/stableAidlImports/android/net/Uri.aidl b/media/media/src/main/stableAidlImports/android/net/Uri.aidl
new file mode 100644
index 0000000..5ec5a66
--- /dev/null
+++ b/media/media/src/main/stableAidlImports/android/net/Uri.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+@JavaOnlyStableParcelable parcelable Uri;
diff --git a/media/media/src/main/stableAidlImports/android/os/Bundle.aidl b/media/media/src/main/stableAidlImports/android/os/Bundle.aidl
new file mode 100644
index 0000000..9642d31
--- /dev/null
+++ b/media/media/src/main/stableAidlImports/android/os/Bundle.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+@JavaOnlyStableParcelable parcelable Bundle;
diff --git a/media/media/src/main/stableAidlImports/android/view/KeyEvent.aidl b/media/media/src/main/stableAidlImports/android/view/KeyEvent.aidl
new file mode 100644
index 0000000..cfaff66
--- /dev/null
+++ b/media/media/src/main/stableAidlImports/android/view/KeyEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+@JavaOnlyStableParcelable parcelable KeyEvent;
diff --git a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java
index 9b21b8f..726c980 100644
--- a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java
+++ b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java
@@ -464,6 +464,7 @@
mPlayer.reset();
}
+ @Ignore("Test disabled due to flakiness, see b/272342480")
@Test
@LargeTest
public void seekModes() throws Exception {
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderDescriptor.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderDescriptor.java
index c7e4d1f..ee1b64a 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderDescriptor.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProviderDescriptor.java
@@ -42,9 +42,13 @@
final List<MediaRouteDescriptor> mRoutes;
final boolean mSupportsDynamicGroupRoute;
- MediaRouteProviderDescriptor(List<MediaRouteDescriptor> routes,
+ MediaRouteProviderDescriptor(@NonNull List<MediaRouteDescriptor> routes,
boolean supportsDynamicGroupRoute) {
- mRoutes = (routes == null) ? Collections.emptyList() : routes;
+ if (routes.isEmpty()) {
+ mRoutes = Collections.emptyList();
+ } else {
+ mRoutes = Collections.unmodifiableList(new ArrayList<>(routes));
+ }
mSupportsDynamicGroupRoute = supportsDynamicGroupRoute;
}
@@ -108,9 +112,9 @@
return mBundle;
}
mBundle = new Bundle();
- if (mRoutes != null && !mRoutes.isEmpty()) {
+ if (!mRoutes.isEmpty()) {
final int count = mRoutes.size();
- ArrayList<Bundle> routeBundles = new ArrayList<Bundle>(count);
+ ArrayList<Bundle> routeBundles = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
routeBundles.add(mRoutes.get(i).asBundle());
}
@@ -131,12 +135,10 @@
if (bundle == null) {
return null;
}
- List<MediaRouteDescriptor> routes = null;
+ List<MediaRouteDescriptor> routes = new ArrayList<>();
ArrayList<Bundle> routeBundles = bundle.getParcelableArrayList(KEY_ROUTES);
- if (routeBundles != null && !routeBundles.isEmpty()) {
- final int count = routeBundles.size();
- routes = new ArrayList<MediaRouteDescriptor>(count);
- for (int i = 0; i < count; i++) {
+ if (routeBundles != null) {
+ for (int i = 0; i < routeBundles.size(); i++) {
routes.add(MediaRouteDescriptor.fromBundle(routeBundles.get(i)));
}
}
@@ -149,7 +151,7 @@
* Builder for {@link MediaRouteProviderDescriptor}.
*/
public static final class Builder {
- private List<MediaRouteDescriptor> mRoutes;
+ private final List<MediaRouteDescriptor> mRoutes = new ArrayList<>();
private boolean mSupportsDynamicGroupRoute = false;
/**
@@ -166,7 +168,7 @@
if (descriptor == null) {
throw new IllegalArgumentException("descriptor must not be null");
}
- mRoutes = descriptor.mRoutes;
+ mRoutes.addAll(descriptor.getRoutes());
mSupportsDynamicGroupRoute = descriptor.mSupportsDynamicGroupRoute;
}
@@ -179,9 +181,7 @@
throw new IllegalArgumentException("route must not be null");
}
- if (mRoutes == null) {
- mRoutes = new ArrayList<MediaRouteDescriptor>();
- } else if (mRoutes.contains(route)) {
+ if (mRoutes.contains(route)) {
throw new IllegalArgumentException("route descriptor already added");
}
mRoutes.add(route);
@@ -210,10 +210,9 @@
*/
@NonNull
Builder setRoutes(@Nullable Collection<MediaRouteDescriptor> routes) {
- if (routes == null || routes.isEmpty()) {
- mRoutes = null;
- } else {
- mRoutes = new ArrayList<>(routes);
+ mRoutes.clear();
+ if (routes != null) {
+ mRoutes.addAll(routes);
}
return this;
}
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 32fafbf..e6984b1 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,7 +28,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.0.1")
- api("androidx.activity:activity-compose:1.7.0-rc01")
+ api("androidx.activity:activity-compose:1.7.0")
api("androidx.compose.animation:animation:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index 8627741..b0506a9 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -30,7 +30,7 @@
compileOnly(projectOrArtifact(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.0.1")
- implementation projectOrArtifact(":compose:ui:ui-tooling")
+ implementation("androidx.compose.ui:ui-tooling:1.4.0-beta02")
implementation(projectOrArtifact(":navigation:navigation-compose"))
implementation("androidx.compose.material:material:1.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index 5ff82ae..6404e3a 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -1173,6 +1173,7 @@
assertWithMessage("Entry2 should never be resumed").that(entry2Resumed).isFalse()
}
+ @Ignore // b/271634544
@LargeTest
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
index bb6e488..4541c3a 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
@@ -513,6 +513,7 @@
* @see NavOptions.popEnterAnim
* @see NavOptions.popExitAnim
*/
+ @Suppress("DEPRECATION")
@JvmStatic
public fun applyPopAnimationsToPendingTransition(activity: Activity) {
val intent = activity.intent ?: return
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index a91574d..10ec13b 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -199,14 +199,14 @@
method @Deprecated public final void removeWeakLoadStateListener(kotlin.jvm.functions.Function2<? super androidx.paging.LoadType,? super androidx.paging.LoadState,kotlin.Unit> listener);
method @Deprecated public void retry();
method @Deprecated public final java.util.List<T> snapshot();
- property public final androidx.paging.PagedList.Config config;
+ property @Deprecated public final androidx.paging.PagedList.Config config;
property @Deprecated public final androidx.paging.DataSource<?,T> dataSource;
- property public abstract boolean isDetached;
- property public boolean isImmutable;
- property public abstract Object? lastKey;
- property public final int loadedCount;
- property public final int positionOffset;
- property public int size;
+ property @Deprecated public abstract boolean isDetached;
+ property @Deprecated public boolean isImmutable;
+ property @Deprecated public abstract Object? lastKey;
+ property @Deprecated public final int loadedCount;
+ property @Deprecated public final int positionOffset;
+ property @Deprecated public int size;
}
@Deprecated @MainThread public abstract static class PagedList.BoundaryCallback<T> {
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 55408c2..07e6471 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -202,14 +202,14 @@
method @Deprecated public final void removeWeakLoadStateListener(kotlin.jvm.functions.Function2<? super androidx.paging.LoadType,? super androidx.paging.LoadState,kotlin.Unit> listener);
method @Deprecated public void retry();
method @Deprecated public final java.util.List<T> snapshot();
- property public final androidx.paging.PagedList.Config config;
+ property @Deprecated public final androidx.paging.PagedList.Config config;
property @Deprecated public final androidx.paging.DataSource<?,T> dataSource;
- property public abstract boolean isDetached;
- property public boolean isImmutable;
- property public abstract Object? lastKey;
- property public final int loadedCount;
- property public final int positionOffset;
- property public int size;
+ property @Deprecated public abstract boolean isDetached;
+ property @Deprecated public boolean isImmutable;
+ property @Deprecated public abstract Object? lastKey;
+ property @Deprecated public final int loadedCount;
+ property @Deprecated public final int positionOffset;
+ property @Deprecated public int size;
}
@Deprecated @MainThread public abstract static class PagedList.BoundaryCallback<T> {
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index a91574d..10ec13b 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -199,14 +199,14 @@
method @Deprecated public final void removeWeakLoadStateListener(kotlin.jvm.functions.Function2<? super androidx.paging.LoadType,? super androidx.paging.LoadState,kotlin.Unit> listener);
method @Deprecated public void retry();
method @Deprecated public final java.util.List<T> snapshot();
- property public final androidx.paging.PagedList.Config config;
+ property @Deprecated public final androidx.paging.PagedList.Config config;
property @Deprecated public final androidx.paging.DataSource<?,T> dataSource;
- property public abstract boolean isDetached;
- property public boolean isImmutable;
- property public abstract Object? lastKey;
- property public final int loadedCount;
- property public final int positionOffset;
- property public int size;
+ property @Deprecated public abstract boolean isDetached;
+ property @Deprecated public boolean isImmutable;
+ property @Deprecated public abstract Object? lastKey;
+ property @Deprecated public final int loadedCount;
+ property @Deprecated public final int positionOffset;
+ property @Deprecated public int size;
}
@Deprecated @MainThread public abstract static class PagedList.BoundaryCallback<T> {
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/GarbageCollectionTestHelper.kt b/paging/paging-common/src/test/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
index de6cf9a..0f92723 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
@@ -21,6 +21,7 @@
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
+import kotlin.random.Random
import kotlin.reflect.KClass
import kotlin.time.Duration.Companion.seconds
@@ -41,7 +42,7 @@
thread {
val leak: ArrayList<ByteArray> = ArrayList()
do {
- val arraySize = (Math.random() * 1000).toInt()
+ val arraySize = Random.nextInt(1000)
leak.add(ByteArray(arraySize))
System.gc()
} while (continueTriggeringGc.get())
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
index 7af01a1..dc2c9fb 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
@@ -30,6 +30,7 @@
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import kotlin.random.Random
import kotlin.test.assertFailsWith
@Suppress("DEPRECATION")
@@ -501,8 +502,8 @@
Item(
names[it % 10],
it,
- Math.random() * 1000,
- (Math.random() * 200).toInt().toString() + " fake st."
+ Random.nextDouble(1000.0),
+ Random.nextInt(200).toString() + " fake st."
)
}.sortedWith(ITEM_COMPARATOR)
}
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
index e2d59c1..bf9b097 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -26,6 +26,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.random.Random
import kotlin.test.assertFailsWith
@RunWith(JUnit4::class)
@@ -426,8 +427,8 @@
Item(
names[it % 10],
it,
- Math.random() * 1000,
- (Math.random() * 200).toInt().toString() + " fake st."
+ Random.nextDouble(1000.0),
+ Random.nextInt(200).toString() + " fake st."
)
}.sortedWith(ITEM_COMPARATOR)
diff --git a/paging/paging-runtime/api/current.txt b/paging/paging-runtime/api/current.txt
index b67fc35..f52790f 100644
--- a/paging/paging-runtime/api/current.txt
+++ b/paging/paging-runtime/api/current.txt
@@ -15,8 +15,8 @@
method @Deprecated public final void removePagedListListener(kotlin.jvm.functions.Function2<? super androidx.paging.PagedList<T>,? super androidx.paging.PagedList<T>,kotlin.Unit> callback);
method @Deprecated public void submitList(androidx.paging.PagedList<T>? pagedList);
method @Deprecated public void submitList(androidx.paging.PagedList<T>? pagedList, Runnable? commitCallback);
- property public androidx.paging.PagedList<T>? currentList;
- property public int itemCount;
+ property @Deprecated public androidx.paging.PagedList<T>? currentList;
+ property @Deprecated public int itemCount;
}
@Deprecated public static interface AsyncPagedListDiffer.PagedListListener<T> {
@@ -97,7 +97,7 @@
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateFooter(androidx.paging.LoadStateAdapter<?> footer);
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateHeader(androidx.paging.LoadStateAdapter<?> header);
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateHeaderAndFooter(androidx.paging.LoadStateAdapter<?> header, androidx.paging.LoadStateAdapter<?> footer);
- property public androidx.paging.PagedList<T>? currentList;
+ property @Deprecated public androidx.paging.PagedList<T>? currentList;
}
public abstract class PagingDataAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
diff --git a/paging/paging-runtime/api/public_plus_experimental_current.txt b/paging/paging-runtime/api/public_plus_experimental_current.txt
index b67fc35..f52790f 100644
--- a/paging/paging-runtime/api/public_plus_experimental_current.txt
+++ b/paging/paging-runtime/api/public_plus_experimental_current.txt
@@ -15,8 +15,8 @@
method @Deprecated public final void removePagedListListener(kotlin.jvm.functions.Function2<? super androidx.paging.PagedList<T>,? super androidx.paging.PagedList<T>,kotlin.Unit> callback);
method @Deprecated public void submitList(androidx.paging.PagedList<T>? pagedList);
method @Deprecated public void submitList(androidx.paging.PagedList<T>? pagedList, Runnable? commitCallback);
- property public androidx.paging.PagedList<T>? currentList;
- property public int itemCount;
+ property @Deprecated public androidx.paging.PagedList<T>? currentList;
+ property @Deprecated public int itemCount;
}
@Deprecated public static interface AsyncPagedListDiffer.PagedListListener<T> {
@@ -97,7 +97,7 @@
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateFooter(androidx.paging.LoadStateAdapter<?> footer);
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateHeader(androidx.paging.LoadStateAdapter<?> header);
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateHeaderAndFooter(androidx.paging.LoadStateAdapter<?> header, androidx.paging.LoadStateAdapter<?> footer);
- property public androidx.paging.PagedList<T>? currentList;
+ property @Deprecated public androidx.paging.PagedList<T>? currentList;
}
public abstract class PagingDataAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
diff --git a/paging/paging-runtime/api/restricted_current.txt b/paging/paging-runtime/api/restricted_current.txt
index b67fc35..f52790f 100644
--- a/paging/paging-runtime/api/restricted_current.txt
+++ b/paging/paging-runtime/api/restricted_current.txt
@@ -15,8 +15,8 @@
method @Deprecated public final void removePagedListListener(kotlin.jvm.functions.Function2<? super androidx.paging.PagedList<T>,? super androidx.paging.PagedList<T>,kotlin.Unit> callback);
method @Deprecated public void submitList(androidx.paging.PagedList<T>? pagedList);
method @Deprecated public void submitList(androidx.paging.PagedList<T>? pagedList, Runnable? commitCallback);
- property public androidx.paging.PagedList<T>? currentList;
- property public int itemCount;
+ property @Deprecated public androidx.paging.PagedList<T>? currentList;
+ property @Deprecated public int itemCount;
}
@Deprecated public static interface AsyncPagedListDiffer.PagedListListener<T> {
@@ -97,7 +97,7 @@
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateFooter(androidx.paging.LoadStateAdapter<?> footer);
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateHeader(androidx.paging.LoadStateAdapter<?> header);
method @Deprecated public final androidx.recyclerview.widget.ConcatAdapter withLoadStateHeaderAndFooter(androidx.paging.LoadStateAdapter<?> header, androidx.paging.LoadStateAdapter<?> footer);
- property public androidx.paging.PagedList<T>? currentList;
+ property @Deprecated public androidx.paging.PagedList<T>? currentList;
}
public abstract class PagingDataAdapter<T, VH extends androidx.recyclerview.widget.RecyclerView.ViewHolder> extends androidx.recyclerview.widget.RecyclerView.Adapter<VH> {
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index d57ea74..59d26ac 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
# Disable docs
androidx.enableDocumentation=false
androidx.playground.snapshotBuildId=9614968
-androidx.playground.metalavaBuildId=9663318
+androidx.playground.metalavaBuildId=9692962
androidx.studio.type=playground
diff --git a/preference/preference/src/main/java/androidx/preference/AndroidResources.java b/preference/preference/src/main/java/androidx/preference/AndroidResources.java
index 18f3003..51d4447 100644
--- a/preference/preference/src/main/java/androidx/preference/AndroidResources.java
+++ b/preference/preference/src/main/java/androidx/preference/AndroidResources.java
@@ -24,7 +24,6 @@
/**
* Utility class for attributes unavailable on older APIs
- * @hide
*/
@RestrictTo(LIBRARY)
@SuppressLint("InlinedApi")
diff --git a/preference/preference/src/main/java/androidx/preference/CheckBoxPreference.java b/preference/preference/src/main/java/androidx/preference/CheckBoxPreference.java
index a0250f3..5d9beef 100644
--- a/preference/preference/src/main/java/androidx/preference/CheckBoxPreference.java
+++ b/preference/preference/src/main/java/androidx/preference/CheckBoxPreference.java
@@ -86,7 +86,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY)
@Override
diff --git a/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java b/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java
index d234000..1b3902b 100644
--- a/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java
+++ b/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java
@@ -96,7 +96,6 @@
return (EditTextPreference) getPreference();
}
- /** @hide */
@RestrictTo(LIBRARY)
@Override
protected boolean needInputMethod() {
diff --git a/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java b/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java
index 020e1b1..0513466 100644
--- a/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java
+++ b/preference/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java
@@ -96,7 +96,6 @@
return (EditTextPreference) getPreference();
}
- /** @hide */
@RestrictTo(LIBRARY)
@Override
protected boolean needInputMethod() {
@@ -113,7 +112,6 @@
mShowRequestTime = pendingShowSoftInputRequest ? SystemClock.currentThreadTimeMillis() : -1;
}
- /** @hide */
@RestrictTo(LIBRARY)
@Override
protected void scheduleShowSoftInput() {
@@ -121,7 +119,6 @@
scheduleShowSoftInputInner();
}
- /** @hide */
@RestrictTo(LIBRARY)
void scheduleShowSoftInputInner() {
if (hasPendingShowSoftInputRequest()) {
diff --git a/preference/preference/src/main/java/androidx/preference/ExpandButton.java b/preference/preference/src/main/java/androidx/preference/ExpandButton.java
index 769758a..44b6fd4 100644
--- a/preference/preference/src/main/java/androidx/preference/ExpandButton.java
+++ b/preference/preference/src/main/java/androidx/preference/ExpandButton.java
@@ -28,7 +28,6 @@
* A {@link Preference} that visually wraps preferences collapsed in a {@link PreferenceGroup},
* and expands those preferences into the group when tapped.
*
- * @hide
*/
final class ExpandButton extends Preference {
private long mId;
diff --git a/preference/preference/src/main/java/androidx/preference/Preference.java b/preference/preference/src/main/java/androidx/preference/Preference.java
index 79d8a2d..3039564 100644
--- a/preference/preference/src/main/java/androidx/preference/Preference.java
+++ b/preference/preference/src/main/java/androidx/preference/Preference.java
@@ -1176,7 +1176,6 @@
/**
* Used by Settings.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void performClick(@NonNull View view) {
@@ -1187,7 +1186,6 @@
* Called when a click should be performed.
*
* Used by Settings.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void performClick() {
@@ -1336,7 +1334,6 @@
*
* Used by Settings.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void onAttachedToHierarchy(@NonNull PreferenceManager preferenceManager, long id) {
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragment.java b/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragment.java
index 31fb389..1a2bc35 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragment.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragment.java
@@ -220,7 +220,6 @@
* focus (ideally in {@link #onBindDialogView(View)}) for the input field in order to
* correctly attach the input method to the field.
*
- * @hide
*/
@RestrictTo(LIBRARY)
protected boolean needInputMethod() {
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragmentCompat.java b/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragmentCompat.java
index 2c96f07..48b3821 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragmentCompat.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragmentCompat.java
@@ -202,7 +202,6 @@
* focus (ideally in {@link #onBindDialogView(View)}) for the input field in order to
* correctly attach the input method to the field.
*
- * @hide
*/
@RestrictTo(LIBRARY)
protected boolean needInputMethod() {
@@ -215,7 +214,6 @@
* Note that starting from Android R, the new WindowInsets API supports showing soft-input
* on-demand, so there is no longer a need to schedule showing soft-input when input connection
* established by the focused editor.</p>
- * @hide
*/
@RestrictTo(LIBRARY)
protected void scheduleShowSoftInput() {
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceFragment.java b/preference/preference/src/main/java/androidx/preference/PreferenceFragment.java
index a61dcc9..ec40d57 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceFragment.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceFragment.java
@@ -515,11 +515,9 @@
onUnbindPreferences();
}
- /** @hide */
@RestrictTo(LIBRARY)
protected void onBindPreferences() {}
- /** @hide */
@RestrictTo(LIBRARY)
protected void onUnbindPreferences() {}
@@ -648,7 +646,6 @@
* A wrapper for getParentFragment which is v17+. Used by the leanback preference library.
*
* @return The {@link android.app.Fragment} to possibly use as a callback
- * @hide
*/
@RestrictTo(LIBRARY)
public android.app.Fragment getCallbackFragment() {
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java b/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
index 97e57f5..0bce96b 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
@@ -541,14 +541,12 @@
/**
* Used by Settings.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void onBindPreferences() {}
/**
* Used by Settings.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void onUnbindPreferences() {}
@@ -685,7 +683,6 @@
* A wrapper for getParentFragment which is v17+. Used by the leanback preference lib.
*
* @return The {@link Fragment} to possibly use as a callback
- * @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceGroup.java b/preference/preference/src/main/java/androidx/preference/PreferenceGroup.java
index 7c30e99..da92130 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceGroup.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceGroup.java
@@ -407,7 +407,6 @@
/**
* Returns true if we're between {@link #onAttached()} and {@link #onPrepareForRemoval()}
*
- * @hide
*/
@RestrictTo(LIBRARY)
public boolean isAttached() {
@@ -421,7 +420,6 @@
*
* @param onExpandButtonClickListener The callback to be invoked
* @see #setInitialExpandedChildrenCount(int)
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void setOnExpandButtonClickListener(
@@ -435,7 +433,6 @@
* Used by Settings.
*
* @return The callback to be invoked when the expand button is clicked.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Nullable
@@ -564,7 +561,6 @@
* Used by Settings.
*
* @see #setInitialExpandedChildrenCount(int)
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public interface OnExpandButtonClickListener {
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceGroupAdapter.java b/preference/preference/src/main/java/androidx/preference/PreferenceGroupAdapter.java
index e9b04d3..7391982 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceGroupAdapter.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceGroupAdapter.java
@@ -44,7 +44,6 @@
*
* Used by Settings.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class PreferenceGroupAdapter extends RecyclerView.Adapter<PreferenceViewHolder>
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceManager.java b/preference/preference/src/main/java/androidx/preference/PreferenceManager.java
index 3f4e92d..d89be35 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceManager.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceManager.java
@@ -98,7 +98,6 @@
/**
* Used by Settings.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public PreferenceManager(@NonNull Context context) {
@@ -204,7 +203,6 @@
* @param rootPreferences Optional existing hierarchy to merge the new
* hierarchies into.
* @return The root hierarchy (if one was not provided, the new hierarchy's root)
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceRecyclerViewAccessibilityDelegate.java b/preference/preference/src/main/java/androidx/preference/PreferenceRecyclerViewAccessibilityDelegate.java
index 8a5f423..e45fcd5 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceRecyclerViewAccessibilityDelegate.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceRecyclerViewAccessibilityDelegate.java
@@ -34,7 +34,6 @@
*
* Used by Leanback.
*
- * @hide
* @deprecated This class is used to set AccessibilityNodeInfo for {@link Preference}. Preference
* class deprecated the API onInitializeAccessibilityNodeInfo.
*/
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceScreen.java b/preference/preference/src/main/java/androidx/preference/PreferenceScreen.java
index 1085608..f2e4429 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceScreen.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceScreen.java
@@ -49,7 +49,6 @@
*
* Used by Settings :)
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public PreferenceScreen(@NonNull Context context, @Nullable AttributeSet attrs) {
diff --git a/preference/preference/src/main/java/androidx/preference/SwitchPreference.java b/preference/preference/src/main/java/androidx/preference/SwitchPreference.java
index 77ecb7a..db02c69 100644
--- a/preference/preference/src/main/java/androidx/preference/SwitchPreference.java
+++ b/preference/preference/src/main/java/androidx/preference/SwitchPreference.java
@@ -192,7 +192,6 @@
}
/**
- * @hide
* @param view
*/
@RestrictTo(LIBRARY)
diff --git a/preference/preference/src/main/java/androidx/preference/SwitchPreferenceCompat.java b/preference/preference/src/main/java/androidx/preference/SwitchPreferenceCompat.java
index b5bea34..8fa40b5 100644
--- a/preference/preference/src/main/java/androidx/preference/SwitchPreferenceCompat.java
+++ b/preference/preference/src/main/java/androidx/preference/SwitchPreferenceCompat.java
@@ -190,7 +190,6 @@
}
/**
- * @hide
* @param view
*/
@RestrictTo(LIBRARY)
diff --git a/preference/preference/src/main/java/androidx/preference/TwoStatePreference.java b/preference/preference/src/main/java/androidx/preference/TwoStatePreference.java
index daf3dbc..9cafbd9 100644
--- a/preference/preference/src/main/java/androidx/preference/TwoStatePreference.java
+++ b/preference/preference/src/main/java/androidx/preference/TwoStatePreference.java
@@ -219,7 +219,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY)
protected void syncSummaryView(@NonNull View view) {
diff --git a/preference/preference/src/main/java/androidx/preference/UnPressableLinearLayout.java b/preference/preference/src/main/java/androidx/preference/UnPressableLinearLayout.java
index c685322..0db689f 100644
--- a/preference/preference/src/main/java/androidx/preference/UnPressableLinearLayout.java
+++ b/preference/preference/src/main/java/androidx/preference/UnPressableLinearLayout.java
@@ -33,7 +33,6 @@
*
* Used by Leanback and Car.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class UnPressableLinearLayout extends LinearLayout {
diff --git a/preference/preference/src/main/java/androidx/preference/internal/PreferenceImageView.java b/preference/preference/src/main/java/androidx/preference/internal/PreferenceImageView.java
index 2e7a162..bb5603d 100644
--- a/preference/preference/src/main/java/androidx/preference/internal/PreferenceImageView.java
+++ b/preference/preference/src/main/java/androidx/preference/internal/PreferenceImageView.java
@@ -34,7 +34,6 @@
*
* Used by Car.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@SuppressLint("AppCompatCustomView")
diff --git a/preference/preference/src/main/java/androidx/preference/internal/package-info.java b/preference/preference/src/main/java/androidx/preference/internal/package-info.java
index 9b46f97..f7e85bf 100644
--- a/preference/preference/src/main/java/androidx/preference/internal/package-info.java
+++ b/preference/preference/src/main/java/androidx/preference/internal/package-info.java
@@ -15,7 +15,6 @@
*/
/**
- * @hide
*/
@RestrictTo(LIBRARY)
package androidx.preference.internal;
diff --git a/privacysandbox/OWNERS b/privacysandbox/OWNERS
index d52e717..0e5347a 100644
--- a/privacysandbox/OWNERS
+++ b/privacysandbox/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 1314839
abz@google.com
ltenorio@google.com
nicoroulet@google.com
diff --git a/privacysandbox/ui/OWNERS b/privacysandbox/ui/OWNERS
index 61d23dd..a322ae5 100644
--- a/privacysandbox/ui/OWNERS
+++ b/privacysandbox/ui/OWNERS
@@ -1,3 +1,5 @@
nator@google.com
akulakov@google.com
-rafaykamran@google.com
\ No newline at end of file
+rafaykamran@google.com
+maco@google.com
+sanjanasunil@google.com
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index da9789e..d720829 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -42,6 +42,7 @@
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -80,6 +81,7 @@
})
}
+ @Ignore // b/271299184
@Test
fun testChangingSandboxedSdkViewLayoutChangesChildLayout() {
val adapter = TestSandboxedUiAdapter(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index fa49f87..2676100 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -101,29 +101,37 @@
databases?.forEach { db ->
DatabaseWriter(db, context.codeLanguage).write(context.processingEnv)
if (db.exportSchema) {
+ val qName = db.element.qualifiedName
+ val filename = "${db.version}.json"
+ val exportToResources =
+ Context.BooleanProcessorOptions.EXPORT_SCHEMA_RESOURCE.getValue(env)
val schemaOutFolderPath = context.schemaOutFolderPath
- if (schemaOutFolderPath == null) {
- context.logger.w(
- Warning.MISSING_SCHEMA_LOCATION, db.element,
- ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY
+ if (exportToResources) {
+ context.logger.w(ProcessorErrors.EXPORTING_SCHEMA_TO_RESOURCES)
+ val schemaFileOutputStream = env.filer.writeResource(
+ filePath = Path.of("schemas", qName, filename),
+ originatingElements = listOf(db.element)
)
- } else {
+ db.exportSchema(schemaFileOutputStream)
+ } else if (schemaOutFolderPath != null) {
val schemaOutFolder = SchemaFileResolver.RESOLVER.getFile(
Path.of(schemaOutFolderPath)
)
if (!schemaOutFolder.exists()) {
schemaOutFolder.mkdirs()
}
- val qName = db.element.qualifiedName
val dbSchemaFolder = File(schemaOutFolder, qName)
if (!dbSchemaFolder.exists()) {
dbSchemaFolder.mkdirs()
}
db.exportSchema(
- File(
- dbSchemaFolder,
- "${db.version}.json"
- )
+ File(dbSchemaFolder, "${db.version}.json")
+ )
+ } else {
+ context.logger.w(
+ warning = Warning.MISSING_SCHEMA_LOCATION,
+ element = db.element,
+ msg = ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY
)
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/log/RLog.kt b/room/room-compiler/src/main/kotlin/androidx/room/log/RLog.kt
index cb6f8f9..e973919 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/log/RLog.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/log/RLog.kt
@@ -74,6 +74,10 @@
printToMessager(messager, WARNING, msg.safeFormat(args), defaultElement)
}
+ fun w(msg: String, vararg args: Any) {
+ printToMessager(messager, WARNING, msg.safeFormat(args), defaultElement)
+ }
+
private data class DiagnosticMessage(
val msg: String,
val element: XElement?,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
index fcca5f5..3221bd5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
@@ -263,7 +263,8 @@
INCREMENTAL("room.incremental", defaultValue = true),
EXPAND_PROJECTION("room.expandProjection", defaultValue = false),
USE_NULL_AWARE_CONVERTER("room.useNullAwareTypeAnalysis", defaultValue = false),
- GENERATE_KOTLIN("room.generateKotlin", defaultValue = false);
+ GENERATE_KOTLIN("room.generateKotlin", defaultValue = false),
+ EXPORT_SCHEMA_RESOURCE("room.exportSchemaResource", defaultValue = false);
/**
* Returns the value of this option passed through the [XProcessingEnv]. If the value
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index b077b98..414e98f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -1147,4 +1147,11 @@
return "The DAO method return type ($typeName) with the nullable type argument " +
"is meaningless because for now Room will never put a null value in a result."
}
+
+ val EXPORTING_SCHEMA_TO_RESOURCES = "Schema export is set to be outputted as a resource" +
+ " (i.e. room.exportSchemaResource is set to true), this means Room will write the current" +
+ " schema version file into the produced JAR. Such flag must only be used for generating" +
+ " the schema file and extracting it from the JAR but not for production builds, otherwise" +
+ " the schema file will end up in the final artifact which is typically not desired. This" +
+ " warning serves as a reminder to use room.exportSchemaResource cautiously."
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
index d65aedb..1bcd843b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -23,6 +23,7 @@
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.migration.bundle.SchemaBundle
import java.io.File
+import java.io.OutputStream
import org.apache.commons.codec.digest.DigestUtils
/**
@@ -125,4 +126,9 @@
}
SchemaBundle.serialize(schemaBundle, file)
}
+
+ fun exportSchema(outputStream: OutputStream) {
+ val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+ SchemaBundle.serialize(schemaBundle, outputStream)
+ }
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
index 68b7ff2..b32301e 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -18,6 +18,7 @@
import COMMON
import androidx.room.DatabaseProcessingStep
+import androidx.room.RoomProcessor
import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
@@ -1475,6 +1476,30 @@
}
@Test
+ fun exportSchemaToJarResources() {
+ val dbSource = Source.java(
+ "foo.bar.MyDb",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Database(entities = {User.class}, version = 1, exportSchema = true)
+ public abstract class MyDb extends RoomDatabase {}
+ """.trimIndent()
+ )
+ val lib = compileFiles(
+ sources = listOf(dbSource, USER),
+ annotationProcessors = listOf(RoomProcessor()),
+ options = mapOf("room.exportSchemaResource" to "true"),
+ includeSystemClasspath = false
+ )
+ assertThat(
+ lib.any { libDir ->
+ libDir.walkTopDown().any { it.endsWith("schemas/foo.bar.MyDb/1.json") }
+ }
+ ).isTrue()
+ }
+
+ @Test
fun jvmNameOnDaoMethod() {
val jvmNameInDaoGetter = Source.kotlin(
"MyDb.kt",
diff --git a/room/room-migration/api/restricted_current.txt b/room/room-migration/api/restricted_current.txt
index 8d5b5f4..8c3e714 100644
--- a/room/room-migration/api/restricted_current.txt
+++ b/room/room-migration/api/restricted_current.txt
@@ -173,6 +173,7 @@
method public int getFormatVersion();
method public boolean isSchemaEqual(androidx.room.migration.bundle.SchemaBundle other);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static final void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.File file) throws java.io.IOException;
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static final void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.OutputStream outputStream) throws java.io.IOException;
property public androidx.room.migration.bundle.DatabaseBundle database;
property public int formatVersion;
field public static final androidx.room.migration.bundle.SchemaBundle.Companion Companion;
@@ -182,6 +183,7 @@
public static final class SchemaBundle.Companion {
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=UnsupportedEncodingException::class) public androidx.room.migration.bundle.SchemaBundle deserialize(java.io.InputStream fis) throws java.io.UnsupportedEncodingException;
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.File file) throws java.io.IOException;
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public void serialize(androidx.room.migration.bundle.SchemaBundle bundle, java.io.OutputStream outputStream) throws java.io.IOException;
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SchemaEquality<T> {
diff --git a/room/room-migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.kt b/room/room-migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.kt
index 16ab264..23e9176 100644
--- a/room/room-migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.kt
+++ b/room/room-migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.kt
@@ -17,7 +17,6 @@
package androidx.room.migration.bundle
import androidx.annotation.RestrictTo
-
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
@@ -28,15 +27,14 @@
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
-
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
+import java.io.OutputStream
import java.io.OutputStreamWriter
import java.io.UnsupportedEncodingException
-import kotlin.jvm.Throws
/**
* Data class that holds the information about a database schema export.
@@ -68,7 +66,7 @@
public fun deserialize(fis: InputStream): SchemaBundle {
InputStreamReader(fis, CHARSET).use { inputStream ->
return GSON.fromJson(inputStream, SchemaBundle::class.javaObjectType)
- ?: throw IllegalStateException("Invalid schema file")
+ ?: throw IllegalStateException("Empty schema file")
}
}
@@ -78,8 +76,14 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@JvmStatic
public fun serialize(bundle: SchemaBundle, file: File) {
- val fos = FileOutputStream(file, false)
- OutputStreamWriter(fos, CHARSET).use { outputStreamWriter ->
+ serialize(bundle, FileOutputStream(file, false))
+ }
+
+ @Throws(IOException::class)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @JvmStatic
+ public fun serialize(bundle: SchemaBundle, outputStream: OutputStream) {
+ OutputStreamWriter(outputStream, CHARSET).use { outputStreamWriter ->
GSON.toJson(bundle, outputStreamWriter)
}
}
diff --git a/room/scripts/attach-async-profiler-to-tests-init-script.gradle b/room/scripts/attach-async-profiler-to-tests-init-script.gradle
index 97303cf..06d8f87 100644
--- a/room/scripts/attach-async-profiler-to-tests-init-script.gradle
+++ b/room/scripts/attach-async-profiler-to-tests-init-script.gradle
@@ -5,7 +5,9 @@
taskGraph.addTaskExecutionGraphListener { graph ->
graph.beforeTask { task ->
if (task instanceof Test) {
- task.jvmArgs(jvmArgs)
+ if (jvmArgs != null) {
+ task.jvmArgs(jvmArgs)
+ }
// this environment variable is used to avoid running profiling tests
// unless we are in a profiling execution
task.environment("ANDROIDX_ROOM_ENABLE_PROFILE_TESTS", "true")
diff --git a/room/scripts/java-kotlin-codegen-comparison.sh b/room/scripts/java-kotlin-codegen-comparison.sh
new file mode 100755
index 0000000..e222937
--- /dev/null
+++ b/room/scripts/java-kotlin-codegen-comparison.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+#
+# 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.
+#
+
+set -eu
+
+JAVA_GEN_TASKS=":room:integration-tests:room-testapp-kotlin:kspWithKspGenJavaDebugAndroidTestKotlin \
+:room:integration-tests:room-testapp-kotlin:compileWithKspGenJavaDebugAndroidTestKotlin \
+:room:integration-tests:room-testapp-kotlin:compileWithKspGenJavaDebugAndroidTestJavaWithJavac"
+KOTLIN_GEN_TASKS=":room:integration-tests:room-testapp-kotlin:kspWithKspGenKotlinDebugAndroidTestKotlin \
+:room:integration-tests:room-testapp-kotlin:compileWithKspGenKotlinDebugAndroidTestKotlin \
+:room:integration-tests:room-testapp-kotlin:compileWithKspGenKotlinDebugAndroidTestJavaWithJavac"
+
+kotlinc -script $(dirname $0)/tasks-comparison.kts -- \
+ -t "java-codegen" $JAVA_GEN_TASKS \
+ -t "kotlin-codegen" $KOTLIN_GEN_TASKS
diff --git a/room/scripts/ksp-kapt-comparison.sh b/room/scripts/ksp-kapt-comparison.sh
index 8de53fa..e10e4fc 100755
--- a/room/scripts/ksp-kapt-comparison.sh
+++ b/room/scripts/ksp-kapt-comparison.sh
@@ -1,97 +1,11 @@
#!/bin/bash
-# This script runs kotlin test app compilation with ksp and kapt repeatedly to measure time spent for each of them.
-# Each build is executed once first (to cache all other tasks), and then N times for just ksp/kapt tasks.
-set -e
-declare -A totals
-declare -A taskTotals
-function log {
- echo $1
-}
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
-PROJECT_DIR="$SCRIPT_DIR/.."
-# move to the project directory
-cd $PROJECT_DIR;
+set -eu
-KSP_TASK=":room:integration-tests:room-testapp-kotlin:kspWithKspGenJavaDebugAndroidTestKotlin"
-KAPT_TASK=":room:integration-tests:room-testapp-kotlin:kaptGenerateStubsWithKaptDebugAndroidTestKotlin \
- :room:integration-tests:room-testapp-kotlin:kaptWithKaptDebugAndroidTestKotlin"
-# parses the given profile file, extracts task durations that we are interested in and adds them to the global tracking
-# usage: parseTimes profileFileURI prefix
-function parseTimes {
- local filePath=$1
- local prefix=$2
- # get the times
- local result=`curl -s $filePath|grep :$prefix -A1`
- local total=0
- local taskName="ERROR-$prefix"
- while read -r line
- do
- if [[ "$line" == *"numeric"* ]]; then
- local taskTime=`echo $line|awk -F'[>s<]' '{print $5*1000}'`
- total=$(($total + $taskTime))
- taskTotals[$taskName]=$((taskTotals[$taskName] + $taskTime))
- elif [[ "$line" == *":"* ]]; then
- taskName=`echo $line|awk -F'[><]' '{print $3}'|awk -F'[:]' '{print $NF}'`
- fi
- done <<< $result
- echo "total time spent for in this run $prefix: $total"
- totals[$prefix]=$((totals[$prefix] + $total))
-}
+KSP_TASKS=":room:integration-tests:room-testapp-kotlin:kspWithKspGenJavaDebugAndroidTestKotlin"
+KAPT_TASKS=":room:integration-tests:room-testapp-kotlin:kaptGenerateStubsWithKaptDebugAndroidTestKotlin \
+:room:integration-tests:room-testapp-kotlin:kaptWithKaptDebugAndroidTestKotlin"
-# Runs the kotlin integration test app with either ksp or kapt then records the duration of tasks.
-# usage: runBuild ksp / runBuild kapt
-function runBuild {
- local type=$1
- local task=""
- if [ "$type" = "ksp" ]; then
- task=$KSP_TASK
- elif [ "$type" = "kapt" ]; then
- task=$KAPT_TASK
- else
- echo "bad arg '$type'"
- exit 1
- fi
- local cmd="./gradlew --init-script \
- $SCRIPT_DIR/rerun-requested-task-init-script.gradle \
- --no-configuration-cache
- --profile $task"
- log "Executing $cmd"
- local profileFile=`$cmd|grep "room"|awk '/profiling report at:/ {print $6}'`
- log "result: $profileFile"
- parseTimes $profileFile $type
-}
-
-# Runs the compilation with kapt and ksp for the given number of times
-# usage: runTest 3 ksp|kapt
-function runTest {
- local limit=$1
- local type=$2
-
- for (( c=1; c<=$limit; c++ ))
- do
- echo "run #$c of $limit"
- runBuild "$type"
- done
-}
-
-function printData {
- local -n data=$1
- echo "$1:"
- for i in "${!data[@]}"
- do
- echo "$i : ${data[$i]} ms"
- done
-}
-
-# build once so all other tasks are cached
-./gradlew --stop
-./gradlew $KSP_TASK
-runTest 10 "ksp"
-
-./gradlew --stop
-./gradlew $KAPT_TASK
-runTest 10 "kapt"
-
-printData totals
-printData taskTotals
+kotlinc -script $(dirname $0)/tasks-comparison.kts -- \
+ -t "ksp" $KSP_TASKS \
+ -t "kapt" $KAPT_TASKS
diff --git a/room/scripts/profile.sh b/room/scripts/profile.sh
index 1e34203..6d9ca30 100755
--- a/room/scripts/profile.sh
+++ b/room/scripts/profile.sh
@@ -144,7 +144,9 @@
$GRADLEW --no-daemon \
--init-script $SCRIPT_DIR/rerun-requested-task-init-script.gradle \
--init-script $SCRIPT_DIR/attach-async-profiler-to-tests-init-script.gradle \
- -p $PROJECT_DIR $GRADLE_ARGS \
+ -p $PROJECT_DIR \
+ --no-configuration-cache \
+ $GRADLE_ARGS \
-Dkotlin.compiler.execution.strategy="in-process" \
$AGENT_PARAMETER_NAME="-agentpath:$AGENT_PATH=start,event=cpu,$AGENT_PARAMS,interval=500000" #sample every .5 ms
}
diff --git a/room/scripts/tasks-comparison.kts b/room/scripts/tasks-comparison.kts
new file mode 100644
index 0000000..b15afd5
--- /dev/null
+++ b/room/scripts/tasks-comparison.kts
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+
+import java.io.BufferedReader
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+val currentDir = File(".").absolutePath
+check(currentDir.endsWith("/frameworks/support/.")) {
+ "Script needs to be executed from '<check-out>/frameworks/support', was '$currentDir'."
+}
+val scriptDir = File(currentDir, "room/scripts")
+
+check(args.size >= 6) { "Expected at least 6 args. See usage instructions."}
+val taskIds = args.count { it == "-t" }
+if (taskIds != 2) {
+ error("Exactly two tags are required per invocation. Found $taskIds")
+}
+
+val firstTagIndex = args.indexOfFirst { it == "-t" } + 1
+val firstTag = args[firstTagIndex]
+val firstTasks = extractTasks(firstTagIndex, args)
+check(firstTasks.isNotEmpty()) { "Task list for a tag must not be empty." }
+
+val secondTagIndex = args.indexOfLast { it == "-t" } + 1
+val secondTag = args[secondTagIndex]
+val secondTasks = extractTasks(secondTagIndex, args)
+check(secondTasks.isNotEmpty()) { "Task list for a tag must not be empty." }
+
+println("Comparing tasks groups!")
+println("First tag: $firstTag")
+println("Task list:\n${firstTasks.joinToString(separator = "\n")}")
+println("Second tag: $secondTag")
+println("Task list\n${secondTasks.joinToString(separator = "\n")}")
+
+cleanBuild(firstTasks)
+val firstResult = profile(firstTag, firstTasks)
+
+cleanBuild(secondTasks)
+val secondResult = profile(secondTag, secondTasks)
+
+crunchNumbers(firstResult)
+crunchNumbers(secondResult)
+
+fun extractTasks(tagIndex: Int, args: Array<String>): List<String> {
+ return buildList {
+ for (i in (tagIndex + 1) until args.size) {
+ if (args[i] == "-t") {
+ break
+ }
+ add(args[i])
+ }
+ }
+}
+
+fun cleanBuild(tasks: List<String>) {
+ println("Running initial build to cook cache...")
+ runCommand("./gradlew --stop")
+ runCommand("./gradlew ${tasks.joinToString(separator = " ")}")
+}
+
+fun profile(
+ tag: String,
+ tasks: List<String>,
+ amount: Int = 10
+): ProfileResult {
+ println("Profiling tasks for '$tag'...")
+ val allRunTimes = List(amount) { runNumber ->
+ val profileCmd = buildString {
+ append("./gradlew ")
+ append("--init-script $scriptDir/rerun-requested-task-init-script.gradle ")
+ append("--no-configuration-cache ")
+ append("--profile ")
+ append(tasks.joinToString(separator = " "))
+ }
+ val reportPath = runCommand(profileCmd, returnOutputStream = true)?.use { stream ->
+ stream.lineSequence().forEach { line ->
+ if (line.startsWith("See the profiling report at:")) {
+ val scheme = "file://"
+ return@use line.substring(
+ line.indexOf(scheme) + scheme.length
+ )
+ }
+ }
+ return@use null
+ }
+ checkNotNull(reportPath) { "Couldn't get report path!" }
+ println("Result at: $reportPath")
+ val taskTimes = mutableMapOf<String, Float>()
+ File(reportPath).bufferedReader().use { reader ->
+ while (true) {
+ val line = reader.readLine()
+ if (line == null) {
+ return@use
+ }
+ tasks.forEach { taskName ->
+ if (line.contains(">$taskName<")) {
+ val timeValue = checkNotNull(reader.readLine())
+ .drop("<td class=\"numeric\">".length)
+ .let { it.substring(0, it.indexOf("s</td>")) }
+ .toFloat()
+ taskTimes[taskName] = taskTimes.getOrDefault(taskName, 0.0f) + timeValue
+ }
+ }
+ }
+ }
+ println("Result of run #${runNumber + 1} of '$tag':")
+ taskTimes.forEach { taskName, time ->
+ println("$time - $taskName")
+ }
+ return@List taskTimes
+ }
+ return ProfileResult(tag, allRunTimes)
+}
+
+fun crunchNumbers(result: ProfileResult) {
+ println("--------------------")
+ println("Summary of profile for '${result.tag}'")
+ println("--------------------")
+ println("Total time (${result.numOfRuns} runs):")
+ println(" Min: ${result.minTotal()}")
+ println(" Avg: ${result.avgTotal()}")
+ println(" Max: ${result.maxTotal()}")
+ println("Per task times:")
+ result.tasks.forEach { taskName ->
+ println(" $taskName")
+ println(buildString {
+ append(" Min: ${result.minTask(taskName)}")
+ append(" Avg: ${result.avgTask(taskName)}")
+ append(" Max: ${result.maxTask(taskName)}")
+ })
+ }
+}
+
+fun runCommand(
+ command: String,
+ workingDir: File = File("."),
+ timeoutAmount: Long = 60,
+ timeoutUnit: TimeUnit = TimeUnit.SECONDS,
+ returnOutputStream: Boolean = false
+): BufferedReader? = runCatching {
+ println("Executing: $command")
+ val proc = ProcessBuilder("\\s".toRegex().split(command))
+ .directory(workingDir)
+ .apply {
+ if (returnOutputStream) {
+ redirectOutput(ProcessBuilder.Redirect.PIPE)
+ } else {
+ redirectOutput(ProcessBuilder.Redirect.INHERIT)
+ }
+ }
+ .redirectError(ProcessBuilder.Redirect.INHERIT)
+ .start()
+ proc.waitFor(timeoutAmount, timeoutUnit)
+ if (proc.exitValue() != 0) {
+ error("Non-zero exit code received: ${proc.exitValue()}")
+ }
+ return if (returnOutputStream) {
+ proc.inputStream.bufferedReader()
+ } else {
+ null
+ }
+}.onFailure { it.printStackTrace() }.getOrNull()
+
+data class ProfileResult(
+ val tag: String,
+ private val taskTimes: List<Map<String, Float>>
+) {
+ val numOfRuns = taskTimes.size
+ val tasks = taskTimes.first().keys
+
+ fun minTotal(): Float = taskTimes.minOf { it.values.sum() }
+
+ fun avgTotal(): Float = taskTimes.map { it.values.sum() }.sum() / taskTimes.size
+
+ fun maxTotal(): Float = taskTimes.maxOf { it.values.sum() }
+
+ fun minTask(name: String): Float = taskTimes.minOf { it.getValue(name) }
+
+ fun avgTask(name: String): Float = taskTimes.map { it.getValue(name) }.sum() / taskTimes.size
+
+ fun maxTask(name: String): Float = taskTimes.maxOf { it.getValue(name) }
+}
\ No newline at end of file
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index a605a56..60e8d9d 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -21,7 +21,7 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.arch.core:core-common:2.1.0")
- implementation(project(":lifecycle:lifecycle-common"))
+ implementation("androidx.lifecycle:lifecycle-common:2.6.0")
api(libs.kotlinStdlib)
androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime"))
diff --git a/settings.gradle b/settings.gradle
index 1a4896a..b9bb09d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -365,6 +365,7 @@
includeProject(":annotation:annotation-experimental-lint")
includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
includeProject(":annotation:annotation-sampled")
+includeProject(":appactions:interaction:interaction-capabilities-communication", [BuildType.MAIN])
includeProject(":appactions:interaction:interaction-capabilities-core", [BuildType.MAIN])
includeProject(":appactions:interaction:interaction-capabilities-productivity", [BuildType.MAIN])
includeProject(":appactions:interaction:interaction-capabilities-safety", [BuildType.MAIN])
diff --git a/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/ScreenshotTestRule.kt b/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/ScreenshotTestRule.kt
index dbd7817..132b215 100644
--- a/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/ScreenshotTestRule.kt
+++ b/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/ScreenshotTestRule.kt
@@ -348,7 +348,8 @@
private fun getDeviceModel(): String {
var model = Build.MODEL.lowercase()
- arrayOf("phone", "x86_64", "x86", "x64", "gms").forEach {
+ model = model.replace("sdk_gphone64_", "emulator")
+ arrayOf("phone", "x86_64", "x86", "x64", "gms", "arm64").forEach {
model = model.replace(it, "")
}
return model.trim().replace(" ", "_")
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index f581e35..cb51b971 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -490,7 +490,7 @@
fun getLineEllipsisCount(lineIndex: Int): Int = layout.getEllipsisCount(lineIndex)
- fun getLineForVertical(vertical: Int): Int = layout.getLineForVertical(topPadding + vertical)
+ fun getLineForVertical(vertical: Int): Int = layout.getLineForVertical(vertical - topPadding)
fun getOffsetForHorizontal(line: Int, horizontal: Float): Int {
return layout.getOffsetForHorizontal(line, horizontal + -1 * getHorizontalPadding(line))
diff --git a/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java b/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
index e04fdbc..217c891 100644
--- a/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
+++ b/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
@@ -35,7 +35,6 @@
/**
- * @hide
*/
// This is instantiated in androidx.fragment.app.FragmentTransition
@SuppressWarnings("unused")
diff --git a/transition/transition/src/main/java/androidx/transition/Slide.java b/transition/transition/src/main/java/androidx/transition/Slide.java
index c707b97..ae54f98 100644
--- a/transition/transition/src/main/java/androidx/transition/Slide.java
+++ b/transition/transition/src/main/java/androidx/transition/Slide.java
@@ -59,7 +59,6 @@
private CalculateSlide mSlideCalculator = sCalculateBottom;
private int mSlideEdge = Gravity.BOTTOM;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Retention(RetentionPolicy.SOURCE)
@IntDef({Gravity.LEFT, Gravity.TOP, Gravity.RIGHT, Gravity.BOTTOM, Gravity.START, Gravity.END})
diff --git a/transition/transition/src/main/java/androidx/transition/Transition.java b/transition/transition/src/main/java/androidx/transition/Transition.java
index 4bc389f..fc1a168 100644
--- a/transition/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/transition/src/main/java/androidx/transition/Transition.java
@@ -149,7 +149,6 @@
private static final int MATCH_LAST = MATCH_ITEM_ID;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@IntDef({MATCH_INSTANCE, MATCH_NAME, MATCH_ID, MATCH_ITEM_ID})
@Retention(RetentionPolicy.SOURCE)
@@ -864,7 +863,6 @@
* This is called internally once all animations have been set up by the
* transition hierarchy. \
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void runAnimators() {
@@ -1708,7 +1706,6 @@
* TransitionListener#onTransitionPause(Transition)} to all listeners
* and pausing all running animators started by this transition.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void pause(@Nullable View sceneRoot) {
@@ -1735,7 +1732,6 @@
* TransitionListener#onTransitionPause(Transition)} to all listeners
* and pausing all running animators started by this transition.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public void resume(@Nullable View sceneRoot) {
@@ -1877,7 +1873,6 @@
* animation, and, when the animator ends, calls {@link #end()}.
*
* @param animator The Animator to be run during this transition.
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void animate(@Nullable Animator animator) {
@@ -1910,7 +1905,6 @@
* TransitionSet classes prior to a Transition subclass starting;
* subclasses should not need to call it directly.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void start() {
@@ -1937,7 +1931,6 @@
* Animator and end() was called in the onAnimationEnd()
* callback of the AnimatorListener.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void end() {
@@ -1970,7 +1963,6 @@
/**
* Force the transition to move to its end state, ending all the animators.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
void forceToEnd(@Nullable ViewGroup sceneRoot) {
@@ -1996,7 +1988,6 @@
/**
* This method cancels a transition that is currently running.
*
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
protected void cancel() {
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionSet.java b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
index ef26884..b1a1a3e 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionSet.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionSet.java
@@ -480,7 +480,6 @@
}
/**
- * @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -549,7 +548,7 @@
}
}
- /** @hide
+ /**
* @param sceneRoot */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -561,7 +560,7 @@
}
}
- /** @hide
+ /**
* @param sceneRoot */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
@@ -573,7 +572,6 @@
}
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
protected void cancel() {
@@ -584,7 +582,6 @@
}
}
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Override
void forceToEnd(ViewGroup sceneRoot) {
diff --git a/transition/transition/src/main/java/androidx/transition/ViewOverlayApi14.java b/transition/transition/src/main/java/androidx/transition/ViewOverlayApi14.java
index 94f2618..e3ae7ba 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewOverlayApi14.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewOverlayApi14.java
@@ -279,7 +279,6 @@
}
/**
- * @hide
*/
@SuppressLint("BanUncheckedReflection") // This class is only used on APIs 14-17
@RestrictTo(LIBRARY_GROUP_PREFIX)
diff --git a/transition/transition/src/main/java/androidx/transition/Visibility.java b/transition/transition/src/main/java/androidx/transition/Visibility.java
index aa582fc..a9f85bd 100644
--- a/transition/transition/src/main/java/androidx/transition/Visibility.java
+++ b/transition/transition/src/main/java/androidx/transition/Visibility.java
@@ -70,7 +70,6 @@
*/
public static final int MODE_OUT = 0x2;
- /** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@SuppressLint("UniqueConstants") // because MODE_IN and Fade.IN are aliases.
@IntDef(flag = true, value = {MODE_IN, MODE_OUT, Fade.IN, Fade.OUT})
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
index 2d6def9..5058a26 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -35,7 +35,7 @@
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
-@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Suppress("ComposableModifierFactory")
@Composable
internal fun Modifier.lazyLayoutSemantics(
itemProvider: LazyLayoutItemProvider,
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
index 067b94a..fb8cb87 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
@@ -22,7 +22,7 @@
import androidx.tv.foundation.lazy.layout.LazyLayoutSemanticState
// TODO (b/233188423): Address IllegalExperimentalApiUsage before moving to beta
-@Suppress("ComposableModifierFactory", "ModifierInspectorInfo", "IllegalExperimentalApiUsage")
+@Suppress("ComposableModifierFactory", "IllegalExperimentalApiUsage")
@ExperimentalFoundationApi
@Composable
internal fun rememberLazyListSemanticState(
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 7b1c9eb..d32a680 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -189,6 +189,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.IndicationInstance rememberUpdatedInstance(androidx.compose.foundation.interaction.InteractionSource interactionSource);
}
+ public final class IconKt {
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Icon(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Icon(androidx.compose.ui.graphics.ImageBitmap bitmap, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Icon(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
+ }
+
@androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
method @androidx.compose.animation.ExperimentalAnimationApi @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentScope<java.lang.Integer>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedVisibilityScope,? super java.lang.Integer,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public void AnimatedVisibility(boolean visible, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label, kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedVisibilityScope,kotlin.Unit> content);
@@ -267,7 +273,8 @@
}
public final class SurfaceKt {
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ClickableSurfaceShape shape, optional androidx.tv.material3.ClickableSurfaceColor color, optional androidx.tv.material3.ClickableSurfaceColor contentColor, optional androidx.tv.material3.ClickableSurfaceScale scale, optional androidx.tv.material3.ClickableSurfaceBorder border, optional androidx.tv.material3.ClickableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ClickableSurfaceShape shape, optional androidx.tv.material3.ClickableSurfaceColor color, optional androidx.tv.material3.ClickableSurfaceColor contentColor, optional androidx.tv.material3.ClickableSurfaceScale scale, optional androidx.tv.material3.ClickableSurfaceBorder border, optional androidx.tv.material3.ClickableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ToggleableSurfaceShape shape, optional androidx.tv.material3.ToggleableSurfaceColor color, optional androidx.tv.material3.ToggleableSurfaceColor contentColor, optional androidx.tv.material3.ToggleableSurfaceScale scale, optional androidx.tv.material3.ToggleableSurfaceBorder border, optional androidx.tv.material3.ToggleableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
@@ -307,6 +314,31 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceBorder {
+ }
+
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceColor {
+ }
+
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceDefaults {
+ method public androidx.tv.material3.ToggleableSurfaceBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border selectedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedSelectedBorder, optional androidx.tv.material3.Border focusedDisabledBorder, optional androidx.tv.material3.Border pressedSelectedBorder, optional androidx.tv.material3.Border selectedDisabledBorder, optional androidx.tv.material3.Border focusedSelectedDisabledBorder);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceColor color(optional long color, optional long focusedColor, optional long pressedColor, optional long selectedColor, optional long disabledColor, optional long focusedSelectedColor, optional long pressedSelectedColor);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long selectedColor, optional long disabledColor, optional long focusedSelectedColor, optional long pressedSelectedColor);
+ method public androidx.tv.material3.ToggleableSurfaceGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow, optional androidx.tv.material3.Glow selectedGlow, optional androidx.tv.material3.Glow focusedSelectedGlow, optional androidx.tv.material3.Glow pressedSelectedGlow);
+ method public androidx.tv.material3.ToggleableSurfaceScale scale(optional float scale, optional float focusedScale, optional float pressedScale, optional float selectedScale, optional float disabledScale, optional float focusedSelectedScale, optional float focusedDisabledScale, optional float pressedSelectedScale, optional float selectedDisabledScale, optional float focusedSelectedDisabledScale);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape selectedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape, optional androidx.compose.ui.graphics.Shape pressedSelectedShape, optional androidx.compose.ui.graphics.Shape selectedDisabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedDisabledShape);
+ field public static final androidx.tv.material3.ToggleableSurfaceDefaults INSTANCE;
+ }
+
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceGlow {
+ }
+
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceScale {
+ }
+
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceShape {
+ }
+
@androidx.compose.runtime.Immutable public final class Typography {
ctor public Typography(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
method public androidx.tv.material3.Typography copy(optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle headlineLarge, optional androidx.compose.ui.text.TextStyle headlineMedium, optional androidx.compose.ui.text.TextStyle headlineSmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall);
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 11a2550..8fa48ba 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -36,6 +36,7 @@
api(project(":compose:ui:ui"))
api("androidx.compose.foundation:foundation:$composeVersion")
api("androidx.compose.foundation:foundation-layout:$composeVersion")
+ api("androidx.compose.material:material-icons-core:$composeVersion")
api("androidx.compose.ui:ui-graphics:$composeVersion")
api("androidx.compose.ui:ui-text:$composeVersion")
api("androidx.compose.ui:ui-util:$composeVersion")
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
new file mode 100644
index 0000000..68299e8
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@OptIn(ExperimentalTvMaterial3Api::class)
+@RunWith(AndroidJUnit4::class)
+class IconTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun vector_materialIconSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val vector = Icons.Filled.Menu
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ Icon(vector, null)
+ }
+ }
+
+ rule
+ .onNodeWithTag(testTag)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun vector_customIconSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+ val vector = ImageVector.Builder(
+ defaultWidth = width, defaultHeight = height,
+ viewportWidth = width.value, viewportHeight = height.value
+ ).build()
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ Icon(vector, null)
+ }
+ }
+
+ rule
+ .onNodeWithTag(testTag)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun image_noIntrinsicSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ Icon(image, null)
+ }
+ }
+
+ rule
+ .onNodeWithTag(testTag)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun image_withIntrinsicSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ Icon(image, null)
+ }
+ }
+
+ rule
+ .onNodeWithTag(testTag)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun painter_noIntrinsicSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val painter = ColorPainter(Color.Red)
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ Icon(painter, null)
+ }
+ }
+
+ rule
+ .onNodeWithTag(testTag)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun painter_withIntrinsicSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ val bitmapPainter = BitmapPainter(image)
+ Icon(bitmapPainter, null)
+ }
+ }
+
+ rule
+ .onNodeWithTag(testTag)
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconScalesToFitSize() {
+ // Image with intrinsic size of 24dp
+ val width = 24.dp
+ val height = 24.dp
+ val testTag = "testTag"
+ var expectedIntSize: IntSize? = null
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ Icon(
+ image,
+ null,
+ // Force Icon to be 50dp
+ modifier = Modifier.requiredSize(50.dp),
+ tint = Color.Unspecified
+ )
+ with(LocalDensity.current) {
+ val dimension = 50.dp.roundToPx()
+ expectedIntSize = IntSize(dimension, dimension)
+ }
+ }
+ }
+
+ rule.onNodeWithTag(testTag)
+ .captureToImage()
+ // The icon should be 50x50 and fill the whole size with red pixels
+ .assertPixels(expectedSize = expectedIntSize!!) {
+ Color.Red
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconUnspecifiedTintColorIgnored() {
+ val width = 35.dp
+ val height = 83.dp
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ Icon(image, null, tint = Color.Unspecified)
+ }
+ }
+
+ // With no color provided for a tint, the icon should render the original pixels
+ rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Red }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconSpecifiedTintColorApplied() {
+ val width = 35.dp
+ val height = 83.dp
+ val testTag = "testTag"
+
+ rule.setContent {
+ Box(Modifier.testTag(testTag)) {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ Icon(image, null, tint = Color.Blue)
+ }
+ }
+
+ // With a tint color provided, all pixels should be blue
+ rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+ }
+
+ @Test
+ fun defaultSemanticsWhenContentDescriptionProvided() {
+ val testTag = "TestTag"
+ rule.setContent {
+ Icon(
+ bitmap = ImageBitmap(100, 100),
+ contentDescription = "qwerty",
+ modifier = Modifier.testTag(testTag)
+ )
+ }
+
+ rule.onNodeWithTag(testTag)
+ .assertContentDescriptionEquals("qwerty")
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Image))
+ }
+
+ private fun createBitmapWithColor(
+ density: Density,
+ width: Int,
+ height: Int,
+ color: Color
+ ): ImageBitmap {
+ val size = Size(width.toFloat(), height.toFloat())
+ val image = ImageBitmap(width, height)
+ CanvasDrawScope().draw(
+ density,
+ LayoutDirection.Ltr,
+ Canvas(image),
+ size
+ ) {
+ drawRect(color)
+ }
+ return image
+ }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
index f9a2879..392192f 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -40,15 +40,22 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsFocused
@@ -85,6 +92,7 @@
)
@MediumTest
@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class SurfaceTest {
@get:Rule
@@ -214,6 +222,7 @@
rule.setContent {
Surface(
modifier = Modifier
+ .semantics { role = Role.Tab }
.testTag("surface"),
onClick = { count.value += 1 },
) {
@@ -224,6 +233,7 @@
rule.onNodeWithTag("surface")
.performSemanticsAction(SemanticsActions.RequestFocus)
.assertHasClickAction()
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("0")
@@ -370,7 +380,6 @@
Truth.assertThat(hitTested.value).isTrue()
}
- @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun clickableSurface_reactsToStateChange() {
val interactionSource = MutableInteractionSource()
@@ -491,4 +500,370 @@
surface.captureToImage().assertContainsColor(Color.Magenta)
}
+
+ @Test
+ fun toggleable_semantics() {
+ var isChecked by mutableStateOf(false)
+ rule.setContent {
+ Surface(
+ checked = isChecked,
+ modifier = Modifier
+ .testTag("surface"),
+ onCheckedChange = { isChecked = it }
+ ) {
+ Text("$isChecked")
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertHasClickAction()
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+ .assertIsEnabled()
+ // since we merge descendants we should have text on the same node
+ .assertTextEquals("false")
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ .assertTextEquals("true")
+ }
+
+ @Test
+ fun toggleable_customSemantics() {
+ var isChecked by mutableStateOf(false)
+ rule.setContent {
+ Surface(
+ checked = isChecked,
+ modifier = Modifier
+ .semantics { role = Role.Tab }
+ .testTag("surface"),
+ onCheckedChange = { isChecked = it }
+ ) {
+ Text("$isChecked")
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertHasClickAction()
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+ .assertIsEnabled()
+ // since we merge descendants we should have text on the same node
+ .assertTextEquals("false")
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ .assertTextEquals("true")
+ }
+
+ @Test
+ fun toggleable_toggleAction() {
+ var isChecked by mutableStateOf(false)
+
+ rule.setContent {
+ Surface(
+ checked = isChecked,
+ modifier = Modifier
+ .testTag("surface"),
+ onCheckedChange = { isChecked = it }
+ ) {
+ Spacer(modifier = Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(isChecked).isTrue()
+
+ rule.onNodeWithTag("surface").performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(isChecked).isFalse()
+ }
+
+ @Test
+ fun toggleable_enabled_disabled() {
+ var isChecked by mutableStateOf(false)
+ var enabled by mutableStateOf(true)
+
+ rule.setContent {
+ Surface(
+ checked = isChecked,
+ modifier = Modifier
+ .testTag("surface"),
+ onCheckedChange = { isChecked = it },
+ enabled = enabled
+ ) {
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertIsEnabled()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ Truth.assertThat(isChecked).isTrue()
+ rule.runOnIdle {
+ enabled = false
+ }
+
+ rule.onNodeWithTag("surface")
+ .assertIsNotEnabled()
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(isChecked).isTrue()
+ }
+
+ @Test
+ fun toggleable_interactionSource() {
+ val interactionSource = MutableInteractionSource()
+
+ lateinit var scope: CoroutineScope
+
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ Surface(
+ checked = false,
+ modifier = Modifier
+ .testTag("surface"),
+ onCheckedChange = {},
+ interactionSource = interactionSource
+ ) {
+ Spacer(Modifier.size(30.toDp()))
+ }
+ }
+
+ val interactions = mutableListOf<Interaction>()
+
+ scope.launch {
+ interactionSource.interactions.collect { interactions.add(it) }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(interactions).isEmpty()
+ }
+
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { keyDown(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(interactions).hasSize(2)
+ Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+ }
+
+ rule.onNodeWithTag("surface").performKeyInput { keyUp(Key.DirectionCenter) }
+
+ rule.runOnIdle {
+ Truth.assertThat(interactions).hasSize(3)
+ Truth.assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+ Truth.assertThat(interactions[1]).isInstanceOf(PressInteraction.Press::class.java)
+ Truth.assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
+ Truth.assertThat((interactions[2] as PressInteraction.Release).press)
+ .isEqualTo(interactions[1])
+ }
+ }
+
+ @Test
+ fun toggleableSurface_allowsFinalPassChildren() {
+ val hitTested = mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.fillMaxSize()) {
+ Surface(
+ checked = false,
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("surface"),
+ onCheckedChange = {}
+ ) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("pressable")
+ .pointerInput(Unit) {
+ awaitEachGesture {
+ hitTested.value = true
+ val event = awaitPointerEvent(PointerEventPass.Final)
+ Truth
+ .assertThat(event.changes[0].isConsumed)
+ .isFalse()
+ }
+ }
+ )
+ }
+ }
+ }
+ rule.onNodeWithTag("surface").performSemanticsAction(SemanticsActions.RequestFocus)
+ rule.onNodeWithTag("pressable", true)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+ Truth.assertThat(hitTested.value).isTrue()
+ }
+
+ @Test
+ fun toggleableSurface_reactsToStateChange() {
+ val interactionSource = MutableInteractionSource()
+ var isPressed by mutableStateOf(false)
+
+ rule.setContent {
+ isPressed = interactionSource.collectIsPressedAsState().value
+ Surface(
+ checked = false,
+ modifier = Modifier
+ .testTag("surface")
+ .size(100.toDp()),
+ onCheckedChange = {},
+ interactionSource = interactionSource
+ ) {}
+ }
+
+ with(rule.onNodeWithTag("surface")) {
+ performSemanticsAction(SemanticsActions.RequestFocus)
+ assertIsFocused()
+ performKeyInput { keyDown(Key.DirectionCenter) }
+ }
+
+ rule.waitUntil(condition = { isPressed })
+
+ Truth.assertThat(isPressed).isTrue()
+ }
+
+ @FlakyTest(bugId = 269229262)
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun toggleableSurface_onCheckedChange_changesGlowColor() {
+ var isChecked by mutableStateOf(false)
+ var focusManager: FocusManager? = null
+ rule.setContent {
+ focusManager = LocalFocusManager.current
+ Surface(
+ checked = isChecked,
+ modifier = Modifier
+ .testTag("surface")
+ .size(100.toDp()),
+ onCheckedChange = { isChecked = it },
+ color = ToggleableSurfaceDefaults.color(
+ color = Color.Transparent,
+ selectedColor = Color.Transparent
+ ),
+ glow = ToggleableSurfaceDefaults.glow(
+ glow = Glow(
+ elevationColor = Color.Magenta,
+ elevation = Elevation.Level5
+ ),
+ selectedGlow = Glow(
+ elevationColor = Color.Green,
+ elevation = Elevation.Level5
+ )
+ )
+ ) {}
+ }
+ rule.onNodeWithTag("surface")
+ .captureToImage()
+ .assertContainsColor(Color.Magenta)
+
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ // Remove focused state to reveal selected state
+ focusManager?.clearFocus()
+
+ rule.onNodeWithTag("surface")
+ .captureToImage()
+ .assertContainsColor(Color.Green)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun toggleableSurface_onCheckedChange_changesScaleFactor() {
+ var isChecked by mutableStateOf(false)
+ var focusManager: FocusManager? = null
+ rule.setContent {
+ focusManager = LocalFocusManager.current
+ Box(
+ modifier = Modifier
+ .background(Color.Blue)
+ .size(50.toDp())
+ )
+ Surface(
+ checked = isChecked,
+ onCheckedChange = { isChecked = it },
+ modifier = Modifier
+ .size(50.toDp())
+ .testTag("surface"),
+ scale = ToggleableSurfaceDefaults.scale(
+ selectedScale = 1.5f
+ )
+ ) {}
+ }
+ rule.onRoot().captureToImage().assertContainsColor(Color.Blue)
+
+ rule.onNodeWithTag("surface")
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ // Remove focused state to reveal selected state
+ focusManager?.clearFocus()
+
+ rule.onRoot().captureToImage().assertDoesNotContainColor(Color.Blue)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun toggleableSurface_onCheckedChange_showsOutline() {
+ var isChecked by mutableStateOf(false)
+ var focusManager: FocusManager? = null
+ rule.setContent {
+ focusManager = LocalFocusManager.current
+ Surface(
+ checked = isChecked,
+ onCheckedChange = { isChecked = it },
+ modifier = Modifier
+ .size(100.toDp())
+ .testTag("surface"),
+ border = ToggleableSurfaceDefaults.border(
+ selectedBorder = Border(
+ border = BorderStroke(width = 5.toDp(), color = Color.Magenta)
+ )
+ ),
+ color = ToggleableSurfaceDefaults.color(
+ color = Color.Transparent,
+ selectedColor = Color.Transparent
+ )
+ ) {}
+ }
+
+ val surface = rule.onNodeWithTag("surface")
+
+ surface.captureToImage().assertDoesNotContainColor(Color.Magenta)
+
+ surface
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .performKeyInput { pressKey(Key.DirectionCenter) }
+
+ // Remove focused state to reveal selected state
+ focusManager?.clearFocus()
+
+ surface.captureToImage().assertContainsColor(Color.Magenta)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun surfaceChangesStyleOnChangingEnabledState() {
+ var surfaceEnabled by mutableStateOf(true)
+
+ rule.setContent {
+ Surface(
+ modifier = Modifier
+ .size(100.toDp())
+ .testTag("surface"),
+ onClick = {},
+ enabled = surfaceEnabled,
+ color = ClickableSurfaceDefaults.color(
+ color = Color.Green,
+ disabledColor = Color.Red
+ )
+ ) {}
+ }
+
+ // Assert surface is enabled
+ rule.onNodeWithTag("surface").captureToImage().assertContainsColor(Color.Green)
+ surfaceEnabled = false
+ // Assert surface is disabled
+ rule.onNodeWithTag("surface").captureToImage().assertContainsColor(Color.Red)
+ }
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
new file mode 100644
index 0000000..2e8faf1
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.toolingGraphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+
+/**
+ * A Material Design icon component that draws [imageVector] using [tint], with a default value
+ * of [LocalContentColor]. If [imageVector] has no intrinsic size, this component will use the
+ * recommended default size. Icon is an opinionated component designed to be used with single-color
+ * icons so that they can be tinted correctly for the component they are placed in. For multicolored
+ * icons and icons that should not be tinted, use [Color.Unspecified] for [tint]. For generic images
+ * that should not be tinted, and do not follow the recommended icon size, use the generic
+ * [androidx.compose.foundation.Image] instead.
+ *
+ * To learn more about icons, see [Material Design icons](https://m3.material.io/styles/icons/overview)
+ *
+ * @param imageVector [ImageVector] to draw inside this icon
+ * @param contentDescription text used by accessibility services to describe what this icon
+ * represents. This should always be gprovided unless this icon is used for decorative purposes, and
+ * does not represent a meaningful action that a user can take. This text should be localized, such
+ * as by using [androidx.compose.ui.res.stringResource] or similar
+ * @param modifier the [Modifier] to be applied to this icon
+ * @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no tint
+ * is applied.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun Icon(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current
+) {
+ Icon(
+ painter = rememberVectorPainter(imageVector),
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
+ )
+}
+
+/**
+ * A Material Design icon component that draws [bitmap] using [tint], with a default value
+ * of [LocalContentColor]. If [bitmap] has no intrinsic size, this component will use the
+ * recommended default size. Icon is an opinionated component designed to be used with single-color
+ * icons so that they can be tinted correctly for the component they are placed in. For multicolored
+ * icons and icons that should not be tinted, use [Color.Unspecified] for [tint]. For generic images
+ * that should not be tinted, and do not follow the recommended icon size, use the generic
+ * [androidx.compose.foundation.Image] instead.
+ *
+ * To learn more about icons, see [Material Design icons](https://m3.material.io/styles/icons/overview)
+ *
+ * @param bitmap [ImageBitmap] to draw inside this icon
+ * @param contentDescription text used by accessibility services to describe what this icon
+ * represents. This should always be provided unless this icon is used for decorative purposes, and
+ * does not represent a meaningful action that a user can take. This text should be localized, such
+ * as by using [androidx.compose.ui.res.stringResource] or similar
+ * @param modifier the [Modifier] to be applied to this icon
+ * @param tint tint to be applied to [bitmap]. If [Color.Unspecified] is provided, then no tint is
+ * applied.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun Icon(
+ bitmap: ImageBitmap,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current
+) {
+ val painter = remember(bitmap) { BitmapPainter(bitmap) }
+ Icon(
+ painter = painter,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
+ )
+}
+
+/**
+ * A Material Design icon component that draws [painter] using [tint], with a default value
+ * of [LocalContentColor]. If [painter] has no intrinsic size, this component will use the
+ * recommended default size. Icon is an opinionated component designed to be used with single-color
+ * icons so that they can be tinted correctly for the component they are placed in. For multicolored
+ * icons and icons that should not be tinted, use [Color.Unspecified] for [tint]. For generic images
+ * that should not be tinted, and do not follow the recommended icon size, use the generic
+ * [androidx.compose.foundation.Image] instead.
+ *
+ * To learn more about icons, see [Material Design icons](https://m3.material.io/styles/icons/overview)
+ *
+ * @param painter [Painter] to draw inside this icon
+ * @param contentDescription text used by accessibility services to describe what this icon
+ * represents. This should always be provided unless this icon is used for decorative purposes, and
+ * does not represent a meaningful action that a user can take. This text should be localized, such
+ * as by using [androidx.compose.ui.res.stringResource] or similar
+ * @param modifier the [Modifier] to be applied to this icon
+ * @param tint tint to be applied to [painter]. If [Color.Unspecified] is provided, then no tint is
+ * applied.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun Icon(
+ painter: Painter,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current
+) {
+ val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
+ val semantics =
+ if (contentDescription != null) {
+ Modifier.semantics {
+ this.contentDescription = contentDescription
+ this.role = Role.Image
+ }
+ } else {
+ Modifier
+ }
+ Box(
+ modifier
+ .toolingGraphicsLayer()
+ .defaultSizeFor(painter)
+ .paint(painter, colorFilter = colorFilter, contentScale = ContentScale.Fit)
+ .then(semantics)
+ )
+}
+
+private fun Modifier.defaultSizeFor(painter: Painter) =
+ this.then(
+ if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) {
+ DefaultIconSizeModifier
+ } else {
+ Modifier
+ }
+ )
+
+private fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
+
+// Default icon size, for icons with no intrinsic size information
+// TODO(rvighnesh): change this to IconButtonTokens.IconSize when we introduce IconButton
+private val DefaultIconSizeModifier = Modifier.size(24.dp)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
index c446450..1ae2b6c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -28,7 +28,6 @@
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -53,6 +52,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.tv.material3.tokens.Elevation
import kotlinx.coroutines.launch
/**
@@ -82,7 +82,6 @@
* @param content defines the [Composable] content inside the surface
*/
@ExperimentalTvMaterial3Api
-@NonRestartableComposable
@Composable
fun Surface(
onClick: () -> Unit,
@@ -105,10 +104,8 @@
enabled = enabled,
onClick = onClick,
interactionSource = interactionSource,
- value = false,
- onValueChanged = null
),
- selected = false,
+ checked = false,
enabled = enabled,
tonalElevation = tonalElevation,
shape = ClickableSurfaceDefaults.shape(
@@ -152,11 +149,121 @@
)
}
+/**
+ * The Surface is a building block component that will be used for any focusable
+ * element on TV such as buttons, cards, navigation, etc.
+ *
+ * This version of Surface is responsible for a toggling its checked state as well as everything
+ * else that a regular Surface does:
+ *
+ * This version of surface will react to the check toggles, calling
+ * [onCheckedChange] lambda, updating the [interactionSource] when [PressInteraction] occurs, and
+ * showing ripple indication in response to press events. If you don't need check
+ * handling, consider using a Surface function that doesn't require [onCheckedChange] param.
+ *
+ * To manually retrieve the content color inside a surface, use [LocalContentColor].
+ *
+ * @param checked whether or not this Surface is toggled on or off
+ * @param onCheckedChange callback to be invoked when the toggleable Surface is clicked
+ * @param modifier Modifier to be applied to the layout corresponding to the surface
+ * @param enabled Controls the enabled state of the surface. When `false`, this Surface will not be
+ * clickable or focusable.
+ * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
+ * in a darker color in light theme and lighter color in dark theme.
+ * @param shape Defines the surface's shape.
+ * @param color Color to be used on background of the Surface
+ * @param contentColor The preferred content color provided by this Surface to its children.
+ * @param scale Defines size of the Surface relative to its original size.
+ * @param border Defines a border around the Surface.
+ * @param glow Diffused shadow to be shown behind the Surface.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
+ * you want to observe [Interaction]s and customize the appearance / behavior of this Surface in
+ * different [Interaction]s.
+ * @param content defines the [Composable] content inside the surface
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun Surface(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ tonalElevation: Dp = Elevation.Level0,
+ shape: ToggleableSurfaceShape = ToggleableSurfaceDefaults.shape(),
+ color: ToggleableSurfaceColor = ToggleableSurfaceDefaults.color(),
+ contentColor: ToggleableSurfaceColor = ToggleableSurfaceDefaults.contentColor(),
+ scale: ToggleableSurfaceScale = ToggleableSurfaceDefaults.scale(),
+ border: ToggleableSurfaceBorder = ToggleableSurfaceDefaults.border(),
+ glow: ToggleableSurfaceGlow = ToggleableSurfaceDefaults.glow(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable (BoxScope.() -> Unit)
+) {
+ val focused by interactionSource.collectIsFocusedAsState()
+ val pressed by interactionSource.collectIsPressedAsState()
+
+ SurfaceImpl(
+ modifier = modifier.tvToggleable(
+ enabled = enabled,
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ interactionSource = interactionSource,
+ ),
+ checked = checked,
+ enabled = enabled,
+ tonalElevation = tonalElevation,
+ shape = ToggleableSurfaceDefaults.shape(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ selected = checked,
+ shape = shape
+ ),
+ color = ToggleableSurfaceDefaults.color(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ selected = checked,
+ color = color
+ ),
+ contentColor = ToggleableSurfaceDefaults.color(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ selected = checked,
+ color = contentColor
+ ),
+ scale = ToggleableSurfaceDefaults.scale(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ selected = checked,
+ scale = scale
+ ),
+ border = ToggleableSurfaceDefaults.border(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ selected = checked,
+ border = border
+ ),
+ glow = ToggleableSurfaceDefaults.glow(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ selected = checked,
+ glow = glow
+ ),
+ interactionSource = interactionSource,
+ content = content
+ )
+}
+
@ExperimentalTvMaterial3Api
@Composable
private fun SurfaceImpl(
modifier: Modifier,
- selected: Boolean,
+ checked: Boolean,
enabled: Boolean,
shape: Shape,
color: Color,
@@ -175,7 +282,7 @@
enabled = enabled,
focused = focused,
pressed = pressed,
- selected = selected
+ selected = checked
)
val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
@@ -258,25 +365,19 @@
/**
* This modifier handles click, press, and focus events for a TV composable.
- * @param enabled decides whether [onClick] or [onValueChanged] is executed
+ * @param enabled decides whether [onClick] is executed
* @param onClick executes the provided lambda
- * @param value differentiates whether the current item is selected or unselected
- * @param onValueChanged executes the provided lambda while returning the inverse state of [value]
* @param interactionSource used to emit [PressInteraction] events
*/
private fun Modifier.tvClickable(
enabled: Boolean,
onClick: (() -> Unit)?,
- value: Boolean,
- onValueChanged: ((Boolean) -> Unit)?,
interactionSource: MutableInteractionSource
) = this
.handleDPadEnter(
enabled = enabled,
interactionSource = interactionSource,
- onClick = onClick,
- value = value,
- onValueChanged = onValueChanged
+ onClick = onClick
)
.focusable(interactionSource = interactionSource)
.semantics(mergeDescendants = true) {
@@ -284,9 +385,6 @@
onClick?.let { nnOnClick ->
nnOnClick()
return@onClick true
- } ?: onValueChanged?.let { nnOnValueChanged ->
- nnOnValueChanged(!value)
- return@onClick true
}
false
}
@@ -295,20 +393,59 @@
}
}
+/**
+ * This modifier handles click, press, and focus events for a TV composable.
+ * @param enabled decides whether [onCheckedChange] is executed
+ * @param checked differentiates whether the current item is checked or unchecked
+ * @param onCheckedChange executes the provided lambda while returning the inverse state of
+ * [checked]
+ */
+private fun Modifier.tvToggleable(
+ enabled: Boolean,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ interactionSource: MutableInteractionSource,
+) = handleDPadEnter(
+ enabled = enabled,
+ interactionSource = interactionSource,
+ checked = checked,
+ onCheckedChanged = onCheckedChange
+ )
+ .focusable(enabled = enabled, interactionSource = interactionSource)
+ .semantics(mergeDescendants = true) {
+ onClick {
+ onCheckedChange(!checked)
+ true
+ }
+ if (!enabled) {
+ disabled()
+ }
+ }
+
+/**
+ * This modifier is used to perform some actions when the user clicks the D-PAD enter button
+ *
+ * @param enabled if this is false, the D-PAD enter event is ignored
+ * @param interactionSource used to emit [PressInteraction] events
+ * @param onClick this lambda will be triggered on D-PAD enter event
+ * @param checked differentiates whether the current item is checked or unchecked
+ * @param onCheckedChanged executes the provided lambda while returning the inverse state of
+ * [checked]
+ */
private fun Modifier.handleDPadEnter(
enabled: Boolean,
interactionSource: MutableInteractionSource,
- onClick: (() -> Unit)?,
- value: Boolean,
- onValueChanged: ((Boolean) -> Unit)?
+ onClick: (() -> Unit)? = null,
+ checked: Boolean = false,
+ onCheckedChanged: ((Boolean) -> Unit)? = null
) = composed(
inspectorInfo = debugInspectorInfo {
name = "handleDPadEnter"
properties["enabled"] = enabled
properties["interactionSource"] = interactionSource
properties["onClick"] = onClick
- properties["onValueChanged"] = onValueChanged
- properties["value"] = value
+ properties["checked"] = checked
+ properties["onCheckedChanged"] = onCheckedChanged
}
) {
val coroutineScope = rememberCoroutineScope()
@@ -334,7 +471,7 @@
interactionSource.emit(PressInteraction.Release(pressInteraction))
}
onClick?.invoke()
- onValueChanged?.invoke(!value)
+ onCheckedChanged?.invoke(!checked)
}
}
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
index 35afd2f..2462a4f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
@@ -261,4 +261,336 @@
)
}
+/**
+ * Contains the default values used by Toggleable Surface.
+ */
+@ExperimentalTvMaterial3Api
+object ToggleableSurfaceDefaults {
+ /**
+ * Creates a [ToggleableSurfaceShape] that represents the default container shapes used in a
+ * toggleable Surface.
+ *
+ * @param shape the shape used when the Surface is enabled, and has no other
+ * [Interaction]s.
+ * @param focusedShape the shape used when the Surface is enabled and focused.
+ * @param pressedShape the shape used when the Surface is enabled and pressed.
+ * @param selectedShape the shape used when the Surface is enabled and selected.
+ * @param disabledShape the shape used when the Surface is not enabled.
+ * @param focusedSelectedShape the shape used when the Surface is enabled, focused and selected.
+ * @param focusedDisabledShape the shape used when the Surface is not enabled and focused.
+ * @param pressedSelectedShape the shape used when the Surface is enabled, pressed and selected.
+ * @param selectedDisabledShape the shape used when the Surface is not enabled and selected.
+ * @param focusedSelectedDisabledShape the shape used when the Surface is not enabled, focused
+ * and selected.
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun shape(
+ shape: Shape = MaterialTheme.shapes.medium,
+ focusedShape: Shape = shape,
+ pressedShape: Shape = shape,
+ selectedShape: Shape = shape,
+ disabledShape: Shape = shape,
+ focusedSelectedShape: Shape = shape,
+ focusedDisabledShape: Shape = disabledShape,
+ pressedSelectedShape: Shape = shape,
+ selectedDisabledShape: Shape = disabledShape,
+ focusedSelectedDisabledShape: Shape = disabledShape
+ ) = ToggleableSurfaceShape(
+ shape = shape,
+ focusedShape = focusedShape,
+ pressedShape = pressedShape,
+ selectedShape = selectedShape,
+ disabledShape = disabledShape,
+ focusedSelectedShape = focusedSelectedShape,
+ focusedDisabledShape = focusedDisabledShape,
+ pressedSelectedShape = pressedSelectedShape,
+ selectedDisabledShape = selectedDisabledShape,
+ focusedSelectedDisabledShape = focusedSelectedDisabledShape
+ )
+
+ /**
+ * Creates a [ToggleableSurfaceColor] that represents the default container colors used in a
+ * toggleable Surface.
+ *
+ * @param color the color used when the Surface is enabled, and has no other [Interaction]s.
+ * @param focusedColor the color used when the Surface is enabled and focused.
+ * @param pressedColor the color used when the Surface is enabled and pressed.
+ * @param selectedColor the color used when the Surface is enabled and selected.
+ * @param disabledColor the color used when the Surface is not enabled.
+ * @param focusedSelectedColor the color used when the Surface is enabled, focused and selected.
+ * @param pressedSelectedColor the color used when the Surface is enabled, pressed and selected.
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun color(
+ color: Color = MaterialTheme.colorScheme.surface,
+ focusedColor: Color = MaterialTheme.colorScheme.inverseSurface,
+ pressedColor: Color = MaterialTheme.colorScheme.inverseSurface,
+ selectedColor: Color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
+ disabledColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(
+ alpha = DisabledBackgroundAlpha
+ ),
+ focusedSelectedColor: Color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
+ pressedSelectedColor: Color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f)
+ ) = ToggleableSurfaceColor(
+ color = color,
+ focusedColor = focusedColor,
+ pressedColor = pressedColor,
+ selectedColor = selectedColor,
+ disabledColor = disabledColor,
+ focusedSelectedColor = focusedSelectedColor,
+ pressedSelectedColor = pressedSelectedColor
+ )
+
+ /**
+ * Creates a [ToggleableSurfaceColor] that represents the default content colors used in a
+ * toggleable Surface.
+ *
+ * @param color the color used when the Surface is enabled, and has no other [Interaction]s.
+ * @param focusedColor the color used when the Surface is enabled and focused.
+ * @param pressedColor the color used when the Surface is enabled and pressed.
+ * @param selectedColor the color used when the Surface is enabled and selected.
+ * @param disabledColor the color used when the Surface is not enabled.
+ * @param focusedSelectedColor the color used when the Surface is enabled, focused and selected.
+ * @param pressedSelectedColor the color used when the Surface is enabled, pressed and selected.
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun contentColor(
+ color: Color = MaterialTheme.colorScheme.onSurface,
+ focusedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ pressedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ selectedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ disabledColor: Color = MaterialTheme.colorScheme.onSurface,
+ focusedSelectedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ pressedSelectedColor: Color = MaterialTheme.colorScheme.inverseOnSurface
+ ) = ToggleableSurfaceColor(
+ color = color,
+ focusedColor = focusedColor,
+ pressedColor = pressedColor,
+ selectedColor = selectedColor,
+ disabledColor = disabledColor,
+ focusedSelectedColor = focusedSelectedColor,
+ pressedSelectedColor = pressedSelectedColor
+ )
+
+ /**
+ * Creates a [ToggleableSurfaceScale] that represents the default scales used in a
+ * toggleable Surface. scales are used to modify the size of a composable in different
+ * [Interaction] states e.g. 1f (original) in default state, 1.2f (scaled up) in focused state,
+ * 0.8f (scaled down) in pressed state, etc.
+ *
+ * @param scale the scale used when the Surface is enabled, and has no other
+ * [Interaction]s.
+ * @param focusedScale the scale used when the Surface is enabled and focused.
+ * @param pressedScale the scale used when the Surface is enabled and pressed.
+ * @param selectedScale the scale used when the Surface is enabled and selected.
+ * @param disabledScale the scale used when the Surface is not enabled.
+ * @param focusedSelectedScale the scale used when the Surface is enabled, focused and
+ * selected.
+ * @param focusedDisabledScale the scale used when the Surface is not enabled and
+ * focused.
+ * @param pressedSelectedScale the scale used when the Surface is enabled, pressed and
+ * selected.
+ * @param selectedDisabledScale the scale used when the Surface is not enabled and
+ * selected.
+ * @param focusedSelectedDisabledScale the scale used when the Surface is not enabled,
+ * focused and selected.
+ */
+ fun scale(
+ scale: Float = 1f,
+ focusedScale: Float = 1.1f,
+ pressedScale: Float = scale,
+ selectedScale: Float = scale,
+ disabledScale: Float = scale,
+ focusedSelectedScale: Float = focusedScale,
+ focusedDisabledScale: Float = disabledScale,
+ pressedSelectedScale: Float = scale,
+ selectedDisabledScale: Float = disabledScale,
+ focusedSelectedDisabledScale: Float = disabledScale
+ ) = ToggleableSurfaceScale(
+ scale = scale,
+ focusedScale = focusedScale,
+ pressedScale = pressedScale,
+ selectedScale = selectedScale,
+ disabledScale = disabledScale,
+ focusedSelectedScale = focusedSelectedScale,
+ focusedDisabledScale = focusedDisabledScale,
+ pressedSelectedScale = pressedSelectedScale,
+ selectedDisabledScale = selectedDisabledScale,
+ focusedSelectedDisabledScale = focusedSelectedDisabledScale
+ )
+
+ /**
+ * Creates a [ToggleableSurfaceBorder] that represents the default [Border]s applied on a
+ * toggleable Surface in different [Interaction] states.
+ *
+ * @param border the [Border] used when the Surface is enabled, and has no other
+ * [Interaction]s.
+ * @param focusedBorder the [Border] used when the Surface is enabled and focused.
+ * @param pressedBorder the [Border] used when the Surface is enabled and pressed.
+ * @param selectedBorder the [Border] used when the Surface is enabled and selected.
+ * @param disabledBorder the [Border] used when the Surface is not enabled.
+ * @param focusedSelectedBorder the [Border] used when the Surface is enabled, focused and
+ * selected.
+ * @param focusedDisabledBorder the [Border] used when the Surface is not enabled and focused.
+ * @param pressedSelectedBorder the [Border] used when the Surface is enabled, pressed and
+ * selected.
+ * @param selectedDisabledBorder the [Border] used when the Surface is not enabled and
+ * selected.
+ * @param focusedSelectedDisabledBorder the [Border] used when the Surface is not enabled,
+ * focused and selected.
+ */
+ fun border(
+ border: Border = Border.None,
+ focusedBorder: Border = border,
+ pressedBorder: Border = focusedBorder,
+ selectedBorder: Border = border,
+ disabledBorder: Border = border,
+ focusedSelectedBorder: Border = focusedBorder,
+ focusedDisabledBorder: Border = disabledBorder,
+ pressedSelectedBorder: Border = border,
+ selectedDisabledBorder: Border = disabledBorder,
+ focusedSelectedDisabledBorder: Border = disabledBorder
+ ) = ToggleableSurfaceBorder(
+ border = border,
+ focusedBorder = focusedBorder,
+ pressedBorder = pressedBorder,
+ selectedBorder = selectedBorder,
+ disabledBorder = disabledBorder,
+ focusedSelectedBorder = focusedSelectedBorder,
+ focusedDisabledBorder = focusedDisabledBorder,
+ pressedSelectedBorder = pressedSelectedBorder,
+ selectedDisabledBorder = selectedDisabledBorder,
+ focusedSelectedDisabledBorder = focusedSelectedDisabledBorder
+ )
+
+ /**
+ * Creates a [ToggleableSurfaceGlow] that represents the default [Glow]s used in a
+ * toggleable Surface.
+ *
+ * @param glow the [Glow] used when the Surface is enabled, and has no other [Interaction]s.
+ * @param focusedGlow the [Glow] used when the Surface is enabled and focused.
+ * @param pressedGlow the [Glow] used when the Surface is enabled and pressed.
+ * @param selectedGlow the [Glow] used when the Surface is enabled and selected.
+ * @param focusedSelectedGlow the [Glow] used when the Surface is enabled, focused and selected.
+ * @param pressedSelectedGlow the [Glow] used when the Surface is enabled, pressed and selected.
+ */
+ fun glow(
+ glow: Glow = Glow.None,
+ focusedGlow: Glow = glow,
+ pressedGlow: Glow = glow,
+ selectedGlow: Glow = glow,
+ focusedSelectedGlow: Glow = focusedGlow,
+ pressedSelectedGlow: Glow = glow
+ ) = ToggleableSurfaceGlow(
+ glow = glow,
+ focusedGlow = focusedGlow,
+ pressedGlow = pressedGlow,
+ selectedGlow = selectedGlow,
+ focusedSelectedGlow = focusedSelectedGlow,
+ pressedSelectedGlow = pressedSelectedGlow
+ )
+
+ internal fun shape(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ selected: Boolean,
+ shape: ToggleableSurfaceShape
+ ): Shape {
+ return when {
+ enabled && selected && pressed -> shape.pressedSelectedShape
+ enabled && selected && focused -> shape.focusedSelectedShape
+ enabled && selected -> shape.selectedShape
+ enabled && pressed -> shape.pressedShape
+ enabled && focused -> shape.focusedShape
+ enabled -> shape.shape
+ !enabled && selected && focused -> shape.focusedSelectedDisabledShape
+ !enabled && selected -> shape.selectedDisabledShape
+ !enabled && focused -> shape.focusedDisabledShape
+ else -> shape.disabledShape
+ }
+ }
+
+ internal fun color(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ selected: Boolean,
+ color: ToggleableSurfaceColor
+ ): Color {
+ return when {
+ enabled && selected && pressed -> color.pressedSelectedColor
+ enabled && selected && focused -> color.focusedSelectedColor
+ enabled && selected -> color.selectedColor
+ enabled && pressed -> color.pressedColor
+ enabled && focused -> color.focusedColor
+ enabled -> color.color
+ else -> color.disabledColor
+ }
+ }
+
+ internal fun scale(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ selected: Boolean,
+ scale: ToggleableSurfaceScale
+ ): Float {
+ return when {
+ enabled && selected && pressed -> scale.pressedSelectedScale
+ enabled && selected && focused -> scale.focusedSelectedScale
+ enabled && selected -> scale.selectedScale
+ enabled && pressed -> scale.pressedScale
+ enabled && focused -> scale.focusedScale
+ enabled -> scale.scale
+ !enabled && selected && focused -> scale.focusedSelectedDisabledScale
+ !enabled && selected -> scale.selectedDisabledScale
+ !enabled && focused -> scale.focusedDisabledScale
+ else -> scale.disabledScale
+ }
+ }
+
+ internal fun border(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ selected: Boolean,
+ border: ToggleableSurfaceBorder
+ ): Border {
+ return when {
+ enabled && selected && pressed -> border.pressedSelectedBorder
+ enabled && selected && focused -> border.focusedSelectedBorder
+ enabled && selected -> border.selectedBorder
+ enabled && pressed -> border.pressedBorder
+ enabled && focused -> border.focusedBorder
+ enabled -> border.border
+ !enabled && selected && focused -> border.focusedSelectedDisabledBorder
+ !enabled && selected -> border.selectedDisabledBorder
+ !enabled && focused -> border.focusedDisabledBorder
+ else -> border.disabledBorder
+ }
+ }
+
+ internal fun glow(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ selected: Boolean,
+ glow: ToggleableSurfaceGlow
+ ): Glow {
+ return when {
+ enabled && selected && pressed -> glow.pressedSelectedGlow
+ enabled && selected && focused -> glow.focusedSelectedGlow
+ enabled && selected -> glow.selectedGlow
+ enabled && pressed -> glow.pressedGlow
+ enabled && focused -> glow.focusedGlow
+ enabled -> glow.glow
+ else -> Glow.None
+ }
+ }
+}
+
private const val DisabledBackgroundAlpha = 0.4f
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
index 3ea0875..c8d929a 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
@@ -67,6 +67,69 @@
}
/**
+ * Defines [Shape] for all TV [Interaction] states of a toggleable Surface.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class ToggleableSurfaceShape internal constructor(
+ internal val shape: Shape,
+ internal val focusedShape: Shape,
+ internal val pressedShape: Shape,
+ internal val selectedShape: Shape,
+ internal val disabledShape: Shape,
+ internal val focusedSelectedShape: Shape,
+ internal val focusedDisabledShape: Shape,
+ internal val pressedSelectedShape: Shape,
+ internal val selectedDisabledShape: Shape,
+ internal val focusedSelectedDisabledShape: Shape
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ToggleableSurfaceShape
+
+ if (shape != other.shape) return false
+ if (focusedShape != other.focusedShape) return false
+ if (pressedShape != other.pressedShape) return false
+ if (selectedShape != other.selectedShape) return false
+ if (disabledShape != other.disabledShape) return false
+ if (focusedSelectedShape != other.focusedSelectedShape) return false
+ if (focusedDisabledShape != other.focusedDisabledShape) return false
+ if (pressedSelectedShape != other.pressedSelectedShape) return false
+ if (selectedDisabledShape != other.selectedDisabledShape) return false
+ if (focusedSelectedDisabledShape != other.focusedSelectedDisabledShape) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = shape.hashCode()
+ result = 31 * result + focusedShape.hashCode()
+ result = 31 * result + pressedShape.hashCode()
+ result = 31 * result + selectedShape.hashCode()
+ result = 31 * result + disabledShape.hashCode()
+ result = 31 * result + focusedSelectedShape.hashCode()
+ result = 31 * result + focusedDisabledShape.hashCode()
+ result = 31 * result + pressedSelectedShape.hashCode()
+ result = 31 * result + selectedDisabledShape.hashCode()
+ result = 31 * result + focusedSelectedDisabledShape.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "ToggleableSurfaceShape(shape=$shape, focusedShape=$focusedShape," +
+ "pressedShape=$pressedShape, selectedShape=$selectedShape," +
+ "disabledShape=$disabledShape, focusedSelectedShape=$focusedSelectedShape, " +
+ "focusedDisabledShape=$focusedDisabledShape," +
+ "pressedSelectedShape=$pressedSelectedShape, " +
+ "selectedDisabledShape=$selectedDisabledShape, " +
+ "focusedSelectedDisabledShape=$focusedSelectedDisabledShape)"
+ }
+}
+
+/**
* Defines [Color] for all TV [Interaction] states of a Clickable Surface.
*/
@ExperimentalTvMaterial3Api
@@ -107,6 +170,57 @@
}
/**
+ * Defines [Color] for all TV [Interaction] states of a toggleable Surface.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class ToggleableSurfaceColor internal constructor(
+ internal val color: Color,
+ internal val focusedColor: Color,
+ internal val pressedColor: Color,
+ internal val selectedColor: Color,
+ internal val disabledColor: Color,
+ internal val focusedSelectedColor: Color,
+ internal val pressedSelectedColor: Color
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ToggleableSurfaceColor
+
+ if (color != other.color) return false
+ if (focusedColor != other.focusedColor) return false
+ if (pressedColor != other.pressedColor) return false
+ if (selectedColor != other.selectedColor) return false
+ if (disabledColor != other.disabledColor) return false
+ if (focusedSelectedColor != other.focusedSelectedColor) return false
+ if (pressedSelectedColor != other.pressedSelectedColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = color.hashCode()
+ result = 31 * result + focusedColor.hashCode()
+ result = 31 * result + pressedColor.hashCode()
+ result = 31 * result + selectedColor.hashCode()
+ result = 31 * result + disabledColor.hashCode()
+ result = 31 * result + focusedSelectedColor.hashCode()
+ result = 31 * result + pressedSelectedColor.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "ToggleableSurfaceColor(color=$color, focusedColor=$focusedColor," +
+ "pressedColor=$pressedColor, selectedColor=$selectedColor," +
+ "disabledColor=$disabledColor, focusedSelectedColor=$focusedSelectedColor, " +
+ "pressedSelectedColor=$pressedSelectedColor)"
+ }
+}
+
+/**
* Defines the scale for all TV indication states of Surface. Note: This scale must be
* a non-negative float.
*/
@@ -166,6 +280,70 @@
}
/**
+ * Defines the scale for all TV [Interaction] states of toggleable Surface. Note: This
+ * scale must be a non-negative float.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class ToggleableSurfaceScale internal constructor(
+ @FloatRange(from = 0.0) internal val scale: Float,
+ @FloatRange(from = 0.0) internal val focusedScale: Float,
+ @FloatRange(from = 0.0) internal val pressedScale: Float,
+ @FloatRange(from = 0.0) internal val selectedScale: Float,
+ @FloatRange(from = 0.0) internal val disabledScale: Float,
+ @FloatRange(from = 0.0) internal val focusedSelectedScale: Float,
+ @FloatRange(from = 0.0) internal val focusedDisabledScale: Float,
+ @FloatRange(from = 0.0) internal val pressedSelectedScale: Float,
+ @FloatRange(from = 0.0) internal val selectedDisabledScale: Float,
+ @FloatRange(from = 0.0) internal val focusedSelectedDisabledScale: Float
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ToggleableSurfaceScale
+
+ if (scale != other.scale) return false
+ if (focusedScale != other.focusedScale) return false
+ if (pressedScale != other.pressedScale) return false
+ if (selectedScale != other.selectedScale) return false
+ if (disabledScale != other.disabledScale) return false
+ if (focusedSelectedScale != other.focusedSelectedScale) return false
+ if (focusedDisabledScale != other.focusedDisabledScale) return false
+ if (pressedSelectedScale != other.pressedSelectedScale) return false
+ if (selectedDisabledScale != other.selectedDisabledScale) return false
+ if (focusedSelectedDisabledScale != other.focusedSelectedDisabledScale) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = scale.hashCode()
+ result = 31 * result + focusedScale.hashCode()
+ result = 31 * result + pressedScale.hashCode()
+ result = 31 * result + selectedScale.hashCode()
+ result = 31 * result + disabledScale.hashCode()
+ result = 31 * result + focusedSelectedScale.hashCode()
+ result = 31 * result + focusedDisabledScale.hashCode()
+ result = 31 * result + pressedSelectedScale.hashCode()
+ result = 31 * result + selectedDisabledScale.hashCode()
+ result = 31 * result + focusedSelectedDisabledScale.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "ToggleableSurfaceScale(scale=$scale, focusedScale=$focusedScale," +
+ "pressedScale=$pressedScale, selectedScale=$selectedScale," +
+ "disabledScale=$disabledScale, focusedSelectedScale=$focusedSelectedScale, " +
+ "focusedDisabledScale=$focusedDisabledScale," +
+ "pressedSelectedScale=$pressedSelectedScale, " +
+ "selectedDisabledScale=$selectedDisabledScale, " +
+ "focusedSelectedDisabledScale=$focusedSelectedDisabledScale)"
+ }
+}
+
+/**
* Defines [Border] for all TV states of [Surface].
*/
@ExperimentalTvMaterial3Api
@@ -210,7 +388,70 @@
}
/**
- * Defines [Glow] for all TV states of [Surface].
+ * Defines [Border] for all TV states of a toggleable Surface.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class ToggleableSurfaceBorder internal constructor(
+ internal val border: Border,
+ internal val focusedBorder: Border,
+ internal val pressedBorder: Border,
+ internal val selectedBorder: Border,
+ internal val disabledBorder: Border,
+ internal val focusedSelectedBorder: Border,
+ internal val focusedDisabledBorder: Border,
+ internal val pressedSelectedBorder: Border,
+ internal val selectedDisabledBorder: Border,
+ internal val focusedSelectedDisabledBorder: Border
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ToggleableSurfaceBorder
+
+ if (border != other.border) return false
+ if (focusedBorder != other.focusedBorder) return false
+ if (pressedBorder != other.pressedBorder) return false
+ if (selectedBorder != other.selectedBorder) return false
+ if (disabledBorder != other.disabledBorder) return false
+ if (focusedSelectedBorder != other.focusedSelectedBorder) return false
+ if (focusedDisabledBorder != other.focusedDisabledBorder) return false
+ if (pressedSelectedBorder != other.pressedSelectedBorder) return false
+ if (selectedDisabledBorder != other.selectedDisabledBorder) return false
+ if (focusedSelectedDisabledBorder != other.focusedSelectedDisabledBorder) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = border.hashCode()
+ result = 31 * result + focusedBorder.hashCode()
+ result = 31 * result + pressedBorder.hashCode()
+ result = 31 * result + selectedBorder.hashCode()
+ result = 31 * result + disabledBorder.hashCode()
+ result = 31 * result + focusedSelectedBorder.hashCode()
+ result = 31 * result + focusedDisabledBorder.hashCode()
+ result = 31 * result + pressedSelectedBorder.hashCode()
+ result = 31 * result + selectedDisabledBorder.hashCode()
+ result = 31 * result + focusedSelectedDisabledBorder.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "ToggleableSurfaceBorder(border=$border, focusedBorder=$focusedBorder," +
+ "pressedBorder=$pressedBorder, selectedBorder=$selectedBorder," +
+ "disabledBorder=$disabledBorder, focusedSelectedBorder=$focusedSelectedBorder, " +
+ "focusedDisabledBorder=$focusedDisabledBorder," +
+ "pressedSelectedBorder=$pressedSelectedBorder, " +
+ "selectedDisabledBorder=$selectedDisabledBorder, " +
+ "focusedSelectedDisabledBorder=$focusedSelectedDisabledBorder)"
+ }
+}
+
+/**
+ * Defines [Glow] for all TV [Interaction] states of [Surface].
*/
@ExperimentalTvMaterial3Api
@Immutable
@@ -245,3 +486,50 @@
"pressedGlow=$pressedGlow)"
}
}
+
+/**
+ * Defines [Glow] for all TV [Interaction] states of a toggleable Surface.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class ToggleableSurfaceGlow internal constructor(
+ internal val glow: Glow,
+ internal val focusedGlow: Glow,
+ internal val pressedGlow: Glow,
+ internal val selectedGlow: Glow,
+ internal val focusedSelectedGlow: Glow,
+ internal val pressedSelectedGlow: Glow
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ToggleableSurfaceGlow
+
+ if (glow != other.glow) return false
+ if (focusedGlow != other.focusedGlow) return false
+ if (pressedGlow != other.pressedGlow) return false
+ if (selectedGlow != other.selectedGlow) return false
+ if (focusedSelectedGlow != other.focusedSelectedGlow) return false
+ if (pressedSelectedGlow != other.pressedSelectedGlow) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = glow.hashCode()
+ result = 31 * result + focusedGlow.hashCode()
+ result = 31 * result + pressedGlow.hashCode()
+ result = 31 * result + selectedGlow.hashCode()
+ result = 31 * result + focusedSelectedGlow.hashCode()
+ result = 31 * result + pressedSelectedGlow.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "ToggleableSurfaceGlow(glow=$glow, focusedGlow=$focusedGlow," +
+ "pressedGlow=$pressedGlow, selectedGlow=$selectedGlow," +
+ "focusedSelectedGlow=$focusedSelectedGlow, pressedSelectedGlow=$pressedSelectedGlow)"
+ }
+}
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AdapterDataSetChangeWhileSmoothScrollTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AdapterDataSetChangeWhileSmoothScrollTest.kt
index 643d854..227a1f0 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AdapterDataSetChangeWhileSmoothScrollTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AdapterDataSetChangeWhileSmoothScrollTest.kt
@@ -36,6 +36,7 @@
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.Matchers.greaterThan
import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -105,6 +106,7 @@
test.setAdapterSync(config.adapterProvider.provider(dataSet))
}
+ @Ignore // b/271634631
@Test
fun test() {
tryNTimes(3, resetBlock = { test.resetViewPagerTo(initialPage) }) {
diff --git a/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/IconTest.kt b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/IconTest.kt
new file mode 100644
index 0000000..ce31b5d
--- /dev/null
+++ b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/IconTest.kt
@@ -0,0 +1,323 @@
+/*
+ * 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.wear.compose.materialcore
+
+import android.os.Build
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class IconTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun vector_materialIconSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val vector = Icons.Filled.Menu
+ rule
+ .setContentForSizeAssertions {
+ IconWithDefaults(
+ painter = rememberVectorPainter(vector),
+ contentDescription = null,
+ )
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun vector_customIconSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+ val vector = ImageVector.Builder(
+ defaultWidth = width, defaultHeight = height,
+ viewportWidth = width.value, viewportHeight = height.value
+ ).build()
+ rule
+ .setContentForSizeAssertions {
+ IconWithDefaults(
+ painter = rememberVectorPainter(vector),
+ contentDescription = null,
+ )
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun image_noIntrinsicSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ rule
+ .setContentForSizeAssertions {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+ val painter = remember(image) { BitmapPainter(image) }
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = null
+ )
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun image_withIntrinsicSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+
+ rule
+ .setContentForSizeAssertions {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+ val painter = remember(image) { BitmapPainter(image) }
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = null
+ )
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun painter_noIntrinsicSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val painter = ColorPainter(Color.Red)
+ rule
+ .setContentForSizeAssertions {
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = null
+ )
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun painter_withIntrinsicSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+
+ rule
+ .setContentForSizeAssertions {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ val bitmapPainter = BitmapPainter(image)
+ IconWithDefaults(
+ painter = bitmapPainter,
+ contentDescription = null
+ )
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconScalesToFitSize() {
+ // Image with intrinsic size of 24dp
+ val width = 24.dp
+ val height = 24.dp
+ var expectedIntSize: IntSize? = null
+ rule.setContent {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ val painter = remember(image) { BitmapPainter(image) }
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = null,
+ modifier = Modifier
+ .requiredSize(50.dp)
+ .testTag(TEST_TAG),
+ tint = Color.Unspecified
+ )
+
+ with(LocalDensity.current) {
+ val dimension = 50.dp.roundToPx()
+ expectedIntSize = IntSize(dimension, dimension)
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ // The icon should be 50x50 and fill the whole size with red pixels
+ .assertPixels(expectedSize = expectedIntSize!!) {
+ Color.Red
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconUnspecifiedTintColorIgnored() {
+ val width = 35.dp
+ val height = 83.dp
+ rule.setContent {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ val painter = remember(image) { BitmapPainter(image) }
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = null,
+ modifier = Modifier.testTag(TEST_TAG),
+ tint = Color.Unspecified
+ )
+ }
+
+ // With no color provided for a tint, the icon should render the original pixels
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertPixels { Color.Red }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconSpecifiedTintColorApplied() {
+ val width = 35.dp
+ val height = 83.dp
+ rule.setContent {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ val painter = remember(image) { BitmapPainter(image) }
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = null,
+ modifier = Modifier.testTag(TEST_TAG),
+ tint = Color.Blue
+ )
+ }
+
+ // With a tint color provided, all pixels should be blue
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertPixels { Color.Blue }
+ }
+
+ @Test
+ fun defaultSemanticsWhenContentDescriptionProvided() {
+ rule.setContent {
+ val painter = remember(ImageBitmap(100, 100)) { BitmapPainter(ImageBitmap(100, 100)) }
+ IconWithDefaults(
+ painter = painter,
+ contentDescription = "qwerty",
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .assertContentDescriptionEquals("qwerty")
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Image))
+ }
+
+ private fun createBitmapWithColor(
+ density: Density,
+ width: Int,
+ height: Int,
+ color: Color
+ ): ImageBitmap {
+ val size = Size(width.toFloat(), height.toFloat())
+ val image = ImageBitmap(width, height)
+ CanvasDrawScope().draw(
+ density,
+ LayoutDirection.Ltr,
+ Canvas(image),
+ size
+ ) {
+ drawRect(color)
+ }
+ return image
+ }
+}
+
+@Composable
+internal fun IconWithDefaults(
+ painter: Painter,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = Color.Red
+) {
+ Icon(
+ painter = painter,
+ contentDescription = contentDescription,
+ tint = tint,
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt
index 4ff48c8..cee442c 100644
--- a/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt
+++ b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/MaterialCoreTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
@@ -31,6 +32,8 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithTag
@@ -45,6 +48,9 @@
internal const val TEST_TAG = "test-item"
+val BigTestMaxWidth = 5000.dp
+val BigTestMaxHeight = 5000.dp
+
internal enum class Status {
Enabled,
Disabled;
@@ -113,3 +119,27 @@
}
return histogram
}
+
+internal fun ComposeContentTestRule.setContentForSizeAssertions(
+ parentMaxWidth: Dp = BigTestMaxWidth,
+ parentMaxHeight: Dp = BigTestMaxHeight,
+ useUnmergedTree: Boolean = false,
+ content: @Composable () -> Unit
+): SemanticsNodeInteraction {
+ setContent {
+ Box {
+ Box(
+ Modifier
+ .sizeIn(
+ maxWidth = parentMaxWidth,
+ maxHeight = parentMaxHeight
+ )
+ .testTag("containerForSizeAssertion")
+ ) {
+ content()
+ }
+ }
+ }
+
+ return onNodeWithTag("containerForSizeAssertion", useUnmergedTree = useUnmergedTree)
+}
diff --git a/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/TextTest.kt b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/TextTest.kt
new file mode 100644
index 0000000..7215c15
--- /dev/null
+++ b/wear/compose/compose-material-core/src/androidAndroidTest/kotlin/androidx/wear/compose/materialcore/TextTest.kt
@@ -0,0 +1,305 @@
+/*
+ * 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.wear.compose.materialcore
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.lang.IllegalArgumentException
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TextTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val ExpectedTextStyle = TextStyle(
+ color = Color.Red,
+ fontSize = 32.sp,
+ fontStyle = FontStyle.Italic,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily.Default,
+ letterSpacing = 1.sp,
+ textDecoration = TextDecoration.Underline,
+ textAlign = TextAlign.End,
+ lineHeight = 10.sp,
+ )
+ private val TestText = "TestText"
+
+ @Test
+ fun supports_testtag_on_Text() {
+ rule.setContent {
+ TextWithDefaults(
+ text = AnnotatedString(TestText),
+ modifier = Modifier.testTag(TEST_TAG),
+ style = ExpectedTextStyle
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ fun validateGreaterMinLinesResultsGreaterSize() {
+ var size1 = 0
+ var size2 = 0
+
+ rule.setContent {
+ Column(Modifier.background(Color.White)) {
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ minLines = 1,
+ maxLines = 3,
+ onTextLayout = {
+ size1 = it.size.height
+ },
+ style = ExpectedTextStyle
+ )
+
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ minLines = 2,
+ maxLines = 3,
+ onTextLayout = {
+ size2 = it.size.height
+ },
+ style = ExpectedTextStyle
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(size2).isGreaterThan(size1)
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun validateMinLinesGreaterThanZero() {
+ rule.setContent {
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ minLines = 0,
+ maxLines = 1,
+ style = ExpectedTextStyle
+ )
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun validateMaxLinesGreaterThanMinLines() {
+ rule.setContent {
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ minLines = 2,
+ maxLines = 1,
+ style = ExpectedTextStyle
+ )
+ }
+ }
+
+ @Test
+ fun colorParameterOverridesStyleColor() {
+ verifyTextColor(
+ Color.Red,
+ ExpectedTextStyle,
+ Color.Red
+ )
+ }
+
+ @Test
+ fun styleColorOverridesUnspecifiedColor() {
+ verifyTextColor(
+ Color.Unspecified,
+ ExpectedTextStyle,
+ ExpectedTextStyle.color
+ )
+ }
+
+ @Test
+ fun inheritsTextStyle() {
+ var fontSize: TextUnit? = null
+ var fontStyle: FontStyle? = null
+ var fontWeight: FontWeight? = null
+ var fontFamily: FontFamily? = null
+ var letterSpacing: TextUnit? = null
+ var textDecoration: TextDecoration? = null
+ var textAlign: TextAlign? = null
+
+ rule.setContent {
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ onTextLayout = {
+ fontSize = it.layoutInput.style.fontSize
+ fontStyle = it.layoutInput.style.fontStyle
+ fontWeight = it.layoutInput.style.fontWeight
+ fontFamily = it.layoutInput.style.fontFamily
+ letterSpacing = it.layoutInput.style.letterSpacing
+ textDecoration = it.layoutInput.style.textDecoration
+ textAlign = it.layoutInput.style.textAlign
+ },
+ style = ExpectedTextStyle
+ )
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(fontSize).isEqualTo(ExpectedTextStyle.fontSize)
+ Truth.assertThat(fontStyle).isEqualTo(ExpectedTextStyle.fontStyle)
+ Truth.assertThat(fontWeight).isEqualTo(ExpectedTextStyle.fontWeight)
+ Truth.assertThat(fontFamily).isEqualTo(ExpectedTextStyle.fontFamily)
+ Truth.assertThat(letterSpacing).isEqualTo(ExpectedTextStyle.letterSpacing)
+ Truth.assertThat(textDecoration).isEqualTo(ExpectedTextStyle.textDecoration)
+ Truth.assertThat(textAlign).isEqualTo(ExpectedTextStyle.textAlign)
+ }
+ }
+
+ @Test
+ fun setsParametersExplicitly() {
+ // Test to ensure that when parameter is set explicitly, then this parameter will be used
+ var textAlign: TextAlign? = null
+ var fontSize: TextUnit? = null
+ var fontStyle: FontStyle? = null
+ var letterSpacing: TextUnit? = null
+ val expectedColor = Color.Blue
+ val expectedTextAlign = TextAlign.End
+ val expectedFontSize = 32.sp
+ val expectedFontStyle = FontStyle.Italic
+ val expectedLetterSpacing = 1.em
+
+ rule.setContent {
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ color = expectedColor,
+ textAlign = expectedTextAlign,
+ fontSize = expectedFontSize,
+ fontStyle = expectedFontStyle,
+ letterSpacing = expectedLetterSpacing,
+ onTextLayout = {
+ textAlign = it.layoutInput.style.textAlign
+ fontSize = it.layoutInput.style.fontSize
+ fontStyle = it.layoutInput.style.fontStyle
+ letterSpacing = it.layoutInput.style.letterSpacing
+ },
+ style = ExpectedTextStyle
+ )
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(textAlign).isEqualTo(expectedTextAlign)
+ Truth.assertThat(fontSize).isEqualTo(expectedFontSize)
+ Truth.assertThat(fontStyle).isEqualTo(expectedFontStyle)
+ Truth.assertThat(letterSpacing).isEqualTo(expectedLetterSpacing)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ private fun verifyTextColor(
+ color: Color,
+ style: TextStyle,
+ expectedColor: Color
+ ) {
+ var textColor: Color? = null
+ rule.setContent {
+ TextWithDefaults(
+ AnnotatedString(TestText),
+ color = color,
+ modifier = Modifier.testTag(TEST_TAG),
+ onTextLayout = {
+ textColor = it.layoutInput.style.color
+ },
+ style = style
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertContainsColor(expectedColor, 0.1f)
+
+ rule.runOnIdle {
+ Truth.assertThat(textColor).isEqualTo(expectedColor)
+ }
+ }
+}
+
+@Composable
+internal fun TextWithDefaults(
+ text: AnnotatedString,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontStyle: FontStyle? = null,
+ fontWeight: FontWeight? = null,
+ fontFamily: FontFamily? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ textDecoration: TextDecoration? = null,
+ textAlign: TextAlign? = null,
+ lineHeight: TextUnit = TextUnit.Unspecified,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = 1,
+ inlineContent: Map<String, InlineTextContent> = mapOf(),
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ style: TextStyle
+) {
+ Text(
+ text = text,
+ modifier = modifier,
+ color = color,
+ fontSize = fontSize,
+ fontStyle = fontStyle,
+ fontWeight = fontWeight,
+ fontFamily = fontFamily,
+ letterSpacing = letterSpacing,
+ textDecoration = textDecoration,
+ textAlign = textAlign,
+ lineHeight = lineHeight,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ minLines = minLines,
+ inlineContent = inlineContent,
+ onTextLayout = onTextLayout,
+ style = style
+ )
+}
diff --git a/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Icon.kt b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Icon.kt
new file mode 100644
index 0000000..33dcc31
--- /dev/null
+++ b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Icon.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.wear.compose.materialcore
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.toolingGraphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+
+/**
+ * Icon component that draws a [painter] using [tint].
+ * TODO:// Add link to Chip for a clickable icon
+ *
+ * @param painter [Painter] to draw inside this Icon
+ * @param contentDescription Text used by accessibility services to describe what this icon
+ * represents. This should always be provided unless this icon is used for decorative purposes,
+ * and does not represent a meaningful action that a user can take. This text should be
+ * localized, such as by using [androidx.compose.ui.res.stringResource] or similar
+ * @param modifier Optional [Modifier] for this Icon
+ * @param tint Tint to be applied to [painter]. If [Color.Unspecified] is provided, then no
+ * tint is applied
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Composable
+fun Icon(
+ painter: Painter,
+ contentDescription: String?,
+ modifier: Modifier,
+ tint: Color
+) {
+ // TODO: b/149735981 semantics for content description
+ val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
+ val semantics = if (contentDescription != null) {
+ Modifier.semantics {
+ this.contentDescription = contentDescription
+ this.role = Role.Image
+ }
+ } else {
+ Modifier
+ }
+ Box(
+ modifier.toolingGraphicsLayer().defaultSizeFor(painter)
+ .paint(
+ painter,
+ colorFilter = colorFilter,
+ contentScale = ContentScale.Fit
+ )
+ .then(semantics)
+ )
+}
+
+internal fun Modifier.defaultSizeFor(painter: Painter) =
+ this.then(
+ if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) {
+ DefaultIconSizeModifier
+ } else {
+ Modifier
+ }
+ )
+
+internal fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
+
+// Default icon size, for icons with no intrinsic size information
+internal val DefaultIconSizeModifier = Modifier.size(24.dp)
diff --git a/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Text.kt b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Text.kt
new file mode 100644
index 0000000..c258ffa
--- /dev/null
+++ b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Text.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.wear.compose.materialcore
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.TextUnit
+
+/**
+ * High level element that displays text and provides semantics / accessibility information.
+ *
+ * For ease of use, commonly used parameters from [TextStyle] are also present here. The order of
+ * precedence is as follows:
+ * - If a parameter is explicitly set here (i.e, it is _not_ `null` or [TextUnit.Unspecified]),
+ * then this parameter will always be used.
+ * - If a parameter is _not_ set, (`null` or [TextUnit.Unspecified]), then the corresponding value
+ * from [style] will be used instead.
+ *
+ * @param text The text to be displayed, where [AnnotatedString] allows multiple styles to be used.
+ * @param modifier [Modifier] to apply to this layout node.
+ * @param color [Color] to apply to the text.
+ * @param fontSize The size of glyphs to use when painting the text. See [TextStyle.fontSize].
+ * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
+ * See [TextStyle.fontStyle].
+ * @param fontWeight The typeface thickness to use when painting the text (e.g., [FontWeight.Bold]).
+ * @param fontFamily The font family to be used when rendering the text. See [TextStyle.fontFamily].
+ * @param letterSpacing The amount of space to add between each letter.
+ * See [TextStyle.letterSpacing].
+ * @param textDecoration The decorations to paint on the text (e.g., an underline).
+ * See [TextStyle.textDecoration].
+ * @param textAlign The alignment of the text within the lines of the paragraph.
+ * See [TextStyle.textAlign].
+ * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+ * See [TextStyle.lineHeight].
+ * @param overflow How visual overflow should be handled.
+ * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
+ * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
+ * [overflow] and TextAlign may have unexpected effects.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated according to
+ * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
+ * @param inlineContent A map store composables that replaces certain ranges of the text. It's
+ * used to insert composables into text layout. Check [InlineTextContent] for more information.
+ * @param onTextLayout Callback that is executed when a new text layout is calculated. A
+ * [TextLayoutResult] object that callback provides contains paragraph information, size of the
+ * text, baselines and other details. The callback can be used to add additional decoration or
+ * functionality to the text. For example, to draw selection around the text.
+ * @param style Style configuration for the text such as color, font, line height etc.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Composable
+public fun Text(
+ text: AnnotatedString,
+ modifier: Modifier,
+ color: Color,
+ fontSize: TextUnit,
+ fontStyle: FontStyle?,
+ fontWeight: FontWeight?,
+ fontFamily: FontFamily?,
+ letterSpacing: TextUnit,
+ textDecoration: TextDecoration?,
+ textAlign: TextAlign?,
+ lineHeight: TextUnit,
+ overflow: TextOverflow,
+ softWrap: Boolean,
+ maxLines: Int,
+ minLines: Int,
+ inlineContent: Map<String, InlineTextContent>,
+ onTextLayout: (TextLayoutResult) -> Unit,
+ style: TextStyle
+) {
+ val mergedStyle = style.merge(
+ TextStyle(
+ color = color,
+ fontSize = fontSize,
+ fontWeight = fontWeight,
+ textAlign = textAlign,
+ lineHeight = lineHeight,
+ fontFamily = fontFamily,
+ textDecoration = textDecoration,
+ fontStyle = fontStyle,
+ letterSpacing = letterSpacing
+ )
+ )
+ BasicText(
+ text = text,
+ modifier = modifier,
+ style = mergedStyle,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ minLines = minLines,
+ inlineContent = inlineContent
+ )
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 8643b92..1be733d 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -451,8 +451,8 @@
@Deprecated public static final class ScalingLazyListAnchorType.Companion {
method @Deprecated public int getItemCenter();
method @Deprecated public int getItemStart();
- property public final int ItemCenter;
- property public final int ItemStart;
+ property @Deprecated public final int ItemCenter;
+ property @Deprecated public final int ItemStart;
}
@Deprecated public sealed interface ScalingLazyListItemInfo {
@@ -464,14 +464,14 @@
method @Deprecated public int getSize();
method @Deprecated public int getUnadjustedOffset();
method @Deprecated public int getUnadjustedSize();
- property public abstract float alpha;
- property public abstract int index;
- property public abstract Object key;
- property public abstract int offset;
- property public abstract float scale;
- property public abstract int size;
- property public abstract int unadjustedOffset;
- property public abstract int unadjustedSize;
+ property @Deprecated public abstract float alpha;
+ property @Deprecated public abstract int index;
+ property @Deprecated public abstract Object key;
+ property @Deprecated public abstract int offset;
+ property @Deprecated public abstract float scale;
+ property @Deprecated public abstract int size;
+ property @Deprecated public abstract int unadjustedOffset;
+ property @Deprecated public abstract int unadjustedSize;
}
@Deprecated @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
@@ -492,17 +492,17 @@
method @Deprecated public long getViewportSize();
method @Deprecated public int getViewportStartOffset();
method @Deprecated public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
- property public abstract int afterAutoCenteringPadding;
- property public abstract int afterContentPadding;
- property public abstract int beforeAutoCenteringPadding;
- property public abstract int beforeContentPadding;
- property public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property public abstract boolean reverseLayout;
- property public abstract int totalItemsCount;
- property public abstract int viewportEndOffset;
- property public abstract long viewportSize;
- property public abstract int viewportStartOffset;
- property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
+ property @Deprecated public abstract int afterAutoCenteringPadding;
+ property @Deprecated public abstract int afterContentPadding;
+ property @Deprecated public abstract int beforeAutoCenteringPadding;
+ property @Deprecated public abstract int beforeContentPadding;
+ property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property @Deprecated public abstract boolean reverseLayout;
+ property @Deprecated public abstract int totalItemsCount;
+ property @Deprecated public abstract int viewportEndOffset;
+ property @Deprecated public abstract long viewportSize;
+ property @Deprecated public abstract int viewportStartOffset;
+ property @Deprecated public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
}
@Deprecated @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
@@ -520,18 +520,18 @@
method @Deprecated public boolean isScrollInProgress();
method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public boolean canScrollBackward;
- property public boolean canScrollForward;
- property public final int centerItemIndex;
- property public final int centerItemScrollOffset;
- property public boolean isScrollInProgress;
- property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
+ property @Deprecated public boolean canScrollBackward;
+ property @Deprecated public boolean canScrollForward;
+ property @Deprecated public final int centerItemIndex;
+ property @Deprecated public final int centerItemScrollOffset;
+ property @Deprecated public boolean isScrollInProgress;
+ property @Deprecated public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
}
@Deprecated public static final class ScalingLazyListState.Companion {
method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
- property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
+ property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
}
public final class ScalingLazyListStateKt {
@@ -550,13 +550,13 @@
method @Deprecated public float getMinTransitionArea();
method @Deprecated public androidx.compose.animation.core.Easing getScaleInterpolator();
method @Deprecated public int resolveViewportVerticalOffset(long viewportConstraints);
- property public abstract float edgeAlpha;
- property public abstract float edgeScale;
- property public abstract float maxElementHeight;
- property public abstract float maxTransitionArea;
- property public abstract float minElementHeight;
- property public abstract float minTransitionArea;
- property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+ property @Deprecated public abstract float edgeAlpha;
+ property @Deprecated public abstract float edgeScale;
+ property @Deprecated public abstract float maxElementHeight;
+ property @Deprecated public abstract float maxTransitionArea;
+ property @Deprecated public abstract float minElementHeight;
+ property @Deprecated public abstract float minTransitionArea;
+ property @Deprecated public abstract androidx.compose.animation.core.Easing scaleInterpolator;
}
public final class ScrollAwayKt {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index b5342b3..c1255f3 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -502,8 +502,8 @@
@Deprecated public static final class ScalingLazyListAnchorType.Companion {
method @Deprecated public int getItemCenter();
method @Deprecated public int getItemStart();
- property public final int ItemCenter;
- property public final int ItemStart;
+ property @Deprecated public final int ItemCenter;
+ property @Deprecated public final int ItemStart;
}
@Deprecated public sealed interface ScalingLazyListItemInfo {
@@ -515,14 +515,14 @@
method @Deprecated public int getSize();
method @Deprecated public int getUnadjustedOffset();
method @Deprecated public int getUnadjustedSize();
- property public abstract float alpha;
- property public abstract int index;
- property public abstract Object key;
- property public abstract int offset;
- property public abstract float scale;
- property public abstract int size;
- property public abstract int unadjustedOffset;
- property public abstract int unadjustedSize;
+ property @Deprecated public abstract float alpha;
+ property @Deprecated public abstract int index;
+ property @Deprecated public abstract Object key;
+ property @Deprecated public abstract int offset;
+ property @Deprecated public abstract float scale;
+ property @Deprecated public abstract int size;
+ property @Deprecated public abstract int unadjustedOffset;
+ property @Deprecated public abstract int unadjustedSize;
}
@Deprecated @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
@@ -543,17 +543,17 @@
method @Deprecated public long getViewportSize();
method @Deprecated public int getViewportStartOffset();
method @Deprecated public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
- property public abstract int afterAutoCenteringPadding;
- property public abstract int afterContentPadding;
- property public abstract int beforeAutoCenteringPadding;
- property public abstract int beforeContentPadding;
- property public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property public abstract boolean reverseLayout;
- property public abstract int totalItemsCount;
- property public abstract int viewportEndOffset;
- property public abstract long viewportSize;
- property public abstract int viewportStartOffset;
- property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
+ property @Deprecated public abstract int afterAutoCenteringPadding;
+ property @Deprecated public abstract int afterContentPadding;
+ property @Deprecated public abstract int beforeAutoCenteringPadding;
+ property @Deprecated public abstract int beforeContentPadding;
+ property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property @Deprecated public abstract boolean reverseLayout;
+ property @Deprecated public abstract int totalItemsCount;
+ property @Deprecated public abstract int viewportEndOffset;
+ property @Deprecated public abstract long viewportSize;
+ property @Deprecated public abstract int viewportStartOffset;
+ property @Deprecated public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
}
@Deprecated @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
@@ -571,18 +571,18 @@
method @Deprecated public boolean isScrollInProgress();
method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public boolean canScrollBackward;
- property public boolean canScrollForward;
- property public final int centerItemIndex;
- property public final int centerItemScrollOffset;
- property public boolean isScrollInProgress;
- property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
+ property @Deprecated public boolean canScrollBackward;
+ property @Deprecated public boolean canScrollForward;
+ property @Deprecated public final int centerItemIndex;
+ property @Deprecated public final int centerItemScrollOffset;
+ property @Deprecated public boolean isScrollInProgress;
+ property @Deprecated public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
}
@Deprecated public static final class ScalingLazyListState.Companion {
method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
- property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
+ property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
}
public final class ScalingLazyListStateKt {
@@ -601,13 +601,13 @@
method @Deprecated public float getMinTransitionArea();
method @Deprecated public androidx.compose.animation.core.Easing getScaleInterpolator();
method @Deprecated public int resolveViewportVerticalOffset(long viewportConstraints);
- property public abstract float edgeAlpha;
- property public abstract float edgeScale;
- property public abstract float maxElementHeight;
- property public abstract float maxTransitionArea;
- property public abstract float minElementHeight;
- property public abstract float minTransitionArea;
- property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+ property @Deprecated public abstract float edgeAlpha;
+ property @Deprecated public abstract float edgeScale;
+ property @Deprecated public abstract float maxElementHeight;
+ property @Deprecated public abstract float maxTransitionArea;
+ property @Deprecated public abstract float minElementHeight;
+ property @Deprecated public abstract float minTransitionArea;
+ property @Deprecated public abstract androidx.compose.animation.core.Easing scaleInterpolator;
}
public final class ScrollAwayKt {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 8643b92..1be733d 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -451,8 +451,8 @@
@Deprecated public static final class ScalingLazyListAnchorType.Companion {
method @Deprecated public int getItemCenter();
method @Deprecated public int getItemStart();
- property public final int ItemCenter;
- property public final int ItemStart;
+ property @Deprecated public final int ItemCenter;
+ property @Deprecated public final int ItemStart;
}
@Deprecated public sealed interface ScalingLazyListItemInfo {
@@ -464,14 +464,14 @@
method @Deprecated public int getSize();
method @Deprecated public int getUnadjustedOffset();
method @Deprecated public int getUnadjustedSize();
- property public abstract float alpha;
- property public abstract int index;
- property public abstract Object key;
- property public abstract int offset;
- property public abstract float scale;
- property public abstract int size;
- property public abstract int unadjustedOffset;
- property public abstract int unadjustedSize;
+ property @Deprecated public abstract float alpha;
+ property @Deprecated public abstract int index;
+ property @Deprecated public abstract Object key;
+ property @Deprecated public abstract int offset;
+ property @Deprecated public abstract float scale;
+ property @Deprecated public abstract int size;
+ property @Deprecated public abstract int unadjustedOffset;
+ property @Deprecated public abstract int unadjustedSize;
}
@Deprecated @androidx.compose.runtime.Stable @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListItemScope {
@@ -492,17 +492,17 @@
method @Deprecated public long getViewportSize();
method @Deprecated public int getViewportStartOffset();
method @Deprecated public java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> getVisibleItemsInfo();
- property public abstract int afterAutoCenteringPadding;
- property public abstract int afterContentPadding;
- property public abstract int beforeAutoCenteringPadding;
- property public abstract int beforeContentPadding;
- property public abstract androidx.compose.foundation.gestures.Orientation orientation;
- property public abstract boolean reverseLayout;
- property public abstract int totalItemsCount;
- property public abstract int viewportEndOffset;
- property public abstract long viewportSize;
- property public abstract int viewportStartOffset;
- property public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
+ property @Deprecated public abstract int afterAutoCenteringPadding;
+ property @Deprecated public abstract int afterContentPadding;
+ property @Deprecated public abstract int beforeAutoCenteringPadding;
+ property @Deprecated public abstract int beforeContentPadding;
+ property @Deprecated public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property @Deprecated public abstract boolean reverseLayout;
+ property @Deprecated public abstract int totalItemsCount;
+ property @Deprecated public abstract int viewportEndOffset;
+ property @Deprecated public abstract long viewportSize;
+ property @Deprecated public abstract int viewportStartOffset;
+ property @Deprecated public abstract java.util.List<androidx.wear.compose.material.ScalingLazyListItemInfo> visibleItemsInfo;
}
@Deprecated @androidx.wear.compose.material.ScalingLazyScopeMarker public sealed interface ScalingLazyListScope {
@@ -520,18 +520,18 @@
method @Deprecated public boolean isScrollInProgress();
method @Deprecated public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public boolean canScrollBackward;
- property public boolean canScrollForward;
- property public final int centerItemIndex;
- property public final int centerItemScrollOffset;
- property public boolean isScrollInProgress;
- property public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
+ property @Deprecated public boolean canScrollBackward;
+ property @Deprecated public boolean canScrollForward;
+ property @Deprecated public final int centerItemIndex;
+ property @Deprecated public final int centerItemScrollOffset;
+ property @Deprecated public boolean isScrollInProgress;
+ property @Deprecated public final androidx.wear.compose.material.ScalingLazyListLayoutInfo layoutInfo;
field @Deprecated public static final androidx.wear.compose.material.ScalingLazyListState.Companion Companion;
}
@Deprecated public static final class ScalingLazyListState.Companion {
method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> getSaver();
- property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
+ property @Deprecated public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyListState,java.lang.Object> Saver;
}
public final class ScalingLazyListStateKt {
@@ -550,13 +550,13 @@
method @Deprecated public float getMinTransitionArea();
method @Deprecated public androidx.compose.animation.core.Easing getScaleInterpolator();
method @Deprecated public int resolveViewportVerticalOffset(long viewportConstraints);
- property public abstract float edgeAlpha;
- property public abstract float edgeScale;
- property public abstract float maxElementHeight;
- property public abstract float maxTransitionArea;
- property public abstract float minElementHeight;
- property public abstract float minTransitionArea;
- property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+ property @Deprecated public abstract float edgeAlpha;
+ property @Deprecated public abstract float edgeScale;
+ property @Deprecated public abstract float maxElementHeight;
+ property @Deprecated public abstract float maxTransitionArea;
+ property @Deprecated public abstract float minElementHeight;
+ property @Deprecated public abstract float minTransitionArea;
+ property @Deprecated public abstract androidx.compose.animation.core.Easing scaleInterpolator;
}
public final class ScrollAwayKt {
diff --git a/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
index adcec0e..0965d74 100644
--- a/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/androidMain/kotlin/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
@@ -29,7 +30,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -161,34 +161,29 @@
positionIndicator: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
- // Transitions for background and 'dialog content' alpha.
- var alphaTransitionState by remember {
- mutableStateOf(MutableTransitionState(AlphaStage.IntroFadeOut))
+ // Transitions for dialog animation.
+ var transitionState by remember {
+ mutableStateOf(MutableTransitionState(DialogVisibility.Hide))
}
- val alphaTransition = updateTransition(alphaTransitionState)
+ val transition = updateTransition(transitionState)
- // Transitions for dialog content scaling.
- var scaleTransitionState by remember {
- mutableStateOf(MutableTransitionState(ScaleStage.Intro))
+ var pendingOnDismissCall by remember {
+ mutableStateOf(false)
}
- val scaleTransition = updateTransition(scaleTransitionState)
- if (showDialog ||
- alphaTransitionState.targetState != AlphaStage.IntroFadeOut ||
- scaleTransitionState.targetState != ScaleStage.Intro
- ) {
+ if (showDialog || transition.currentState == DialogVisibility.Display) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties,
) {
- val backgroundAlpha by animateBackgroundAlpha(alphaTransition, alphaTransitionState)
- val alpha by animateDialogAlpha(alphaTransition, alphaTransitionState)
- val scale by animateDialogScale(scaleTransition, scaleTransitionState)
+ val backgroundScrimAlpha by animateBackgroundScrimAlpha(transition)
+ val contentAlpha by animateContentAlpha(transition)
+ val scale by animateDialogScale(transition)
Scaffold(
vignette = {
AnimatedVisibility(
- visible = scaleTransitionState.targetState == ScaleStage.Display,
+ visible = transition.targetState == DialogVisibility.Display,
enter = fadeIn(
animationSpec =
TweenSpec(durationMillis = CASUAL, easing = STANDARD_IN)
@@ -207,58 +202,45 @@
SwipeToDismissBox(
state = rememberSwipeToDismissBoxState(),
modifier = Modifier.graphicsLayer(
- alpha = alpha,
+ alpha = backgroundScrimAlpha,
scaleX = scale,
scaleY = scale,
),
onDismissed = {
onDismissRequest()
// Reset state for the next time this dialog is shown.
- alphaTransitionState = MutableTransitionState(AlphaStage.IntroFadeOut)
- scaleTransitionState = MutableTransitionState(ScaleStage.Intro)
+ transitionState = MutableTransitionState(DialogVisibility.Hide)
}
) { isBackground ->
Box(
modifier = Modifier
.matchParentSize()
- .background(
- MaterialTheme.colors.background.copy(alpha = backgroundAlpha)
- )
- )
- if (!isBackground) content()
+ .graphicsLayer(alpha = contentAlpha)
+ .background(MaterialTheme.colors.background)
+ ) {
+ if (!isBackground) content()
+ }
}
}
-
- SideEffect {
- // Trigger initial Intro animation
- if (alphaTransitionState.currentState == AlphaStage.IntroFadeOut) {
- // a) Fade out previous screen contents b) Scale down dialog contents.
- alphaTransitionState.targetState = AlphaStage.IntroFadeIn
- scaleTransitionState.targetState = ScaleStage.Display
- } else if (alphaTransitionState.currentState == AlphaStage.IntroFadeIn) {
- // Now conclude the Intro animation by fading in the dialog contents.
- alphaTransitionState.targetState = AlphaStage.Display
- }
- }
-
- // Trigger Outro animation when the caller updates showDialog to false.
LaunchedEffect(showDialog) {
- if (!showDialog) {
+ if (showDialog) {
+ // a) Fade out previous screen contents b) Scale down dialog contents from 125%
+ transitionState.targetState = DialogVisibility.Display
+ pendingOnDismissCall = true
+ } else {
// a) Fade out dialog contents b) Scale up dialog contents.
- alphaTransitionState.targetState = AlphaStage.OutroFadeOut
- scaleTransitionState.targetState = ScaleStage.Outro
+ transitionState.targetState = DialogVisibility.Hide
}
}
- LaunchedEffect(alphaTransitionState.currentState) {
- if (alphaTransitionState.currentState == AlphaStage.OutroFadeOut) {
- // Conclude the Outro animation by fading in the background contents.
- alphaTransitionState.targetState = AlphaStage.OutroFadeIn
- } else if (alphaTransitionState.currentState == AlphaStage.OutroFadeIn) {
+ LaunchedEffect(transitionState.currentState) {
+ if (pendingOnDismissCall &&
+ transitionState.currentState == DialogVisibility.Hide &&
+ transitionState.isIdle
+ ) {
// After the outro animation, leave the dialog & reset alpha/scale transitions.
onDismissRequest()
- alphaTransitionState = MutableTransitionState(AlphaStage.IntroFadeOut)
- scaleTransitionState = MutableTransitionState(ScaleStage.Intro)
+ pendingOnDismissCall = false
}
}
}
@@ -266,79 +248,79 @@
}
@Composable
-private fun animateBackgroundAlpha(
- alphaTransition: Transition<AlphaStage>,
- alphaTransitionState: MutableTransitionState<AlphaStage>
-) = alphaTransition.animateFloat(
+private fun animateBackgroundScrimAlpha(
+ transition: Transition<DialogVisibility>
+) = transition.animateFloat(
transitionSpec = {
- if (alphaTransitionState.currentState == AlphaStage.IntroFadeOut)
- tween(durationMillis = RAPID, easing = STANDARD_OUT)
- else if (alphaTransitionState.targetState == AlphaStage.OutroFadeIn)
- tween(durationMillis = QUICK, easing = STANDARD_IN)
- else
- tween(durationMillis = 0)
+ when (transition.targetState) {
+ DialogVisibility.Display -> tween(
+ durationMillis = (RAPID / 0.9f).toInt(),
+ easing = STANDARD_OUT
+ )
+
+ DialogVisibility.Hide -> keyframes {
+ // Outro
+ durationMillis = QUICK + RAPID
+ 1f at 0
+ 0.9f at RAPID with STANDARD_IN
+ 0.0f at RAPID + QUICK
+ }
+ }
},
- label = "background-alpha"
+ label = "background-scrim-alpha"
) { stage ->
when (stage) {
- AlphaStage.IntroFadeOut -> 0.0f
- AlphaStage.IntroFadeIn -> 0.9f
- AlphaStage.Display -> 1.0f
- AlphaStage.OutroFadeOut -> 0.9f
- AlphaStage.OutroFadeIn -> 0.0f
+ DialogVisibility.Hide -> 0f
+ DialogVisibility.Display -> 1f
}
}
@Composable
-private fun animateDialogAlpha(
- alphaTransition: Transition<AlphaStage>,
- alphaTransitionState: MutableTransitionState<AlphaStage>
-) = alphaTransition.animateFloat(
+private fun animateContentAlpha(
+ transition: Transition<DialogVisibility>
+) = transition.animateFloat(
transitionSpec = {
- if (alphaTransitionState.currentState == AlphaStage.IntroFadeIn)
- tween(durationMillis = QUICK, easing = STANDARD_IN)
- else if (alphaTransitionState.targetState == AlphaStage.OutroFadeOut)
- tween(durationMillis = RAPID, easing = STANDARD_OUT)
- else
- tween(durationMillis = 0)
+ when (transition.targetState) {
+ DialogVisibility.Display -> keyframes {
+ // Intro
+ durationMillis = QUICK + RAPID
+ 0.0f at 0
+ 0.1f at RAPID with STANDARD_IN
+ 1f at RAPID + QUICK
+ }
+
+ DialogVisibility.Hide -> tween(
+ durationMillis = (RAPID / 0.9f).toInt(),
+ easing = STANDARD_OUT
+ )
+ }
},
- label = "alpha"
+ label = "content-alpha"
) { stage ->
when (stage) {
- AlphaStage.IntroFadeOut -> 0.0f
- AlphaStage.IntroFadeIn -> 0.1f
- AlphaStage.Display -> 1.0f
- AlphaStage.OutroFadeOut -> 0.1f
- AlphaStage.OutroFadeIn -> 0.0f
+ DialogVisibility.Hide -> 0f
+ DialogVisibility.Display -> 1f
}
}
@Composable
private fun animateDialogScale(
- scaleTransition: Transition<ScaleStage>,
- scaleTransitionState: MutableTransitionState<ScaleStage>
-) = scaleTransition.animateFloat(
+ transition: Transition<DialogVisibility>
+) = transition.animateFloat(
transitionSpec = {
- if (scaleTransitionState.currentState == ScaleStage.Intro)
- tween(durationMillis = CASUAL, easing = STANDARD_IN)
- else
- tween(durationMillis = CASUAL, easing = STANDARD_OUT)
+ when (transition.targetState) {
+ DialogVisibility.Display -> tween(durationMillis = CASUAL, easing = STANDARD_IN)
+ DialogVisibility.Hide -> tween(durationMillis = CASUAL, easing = STANDARD_OUT)
+ }
},
label = "scale"
) { stage ->
when (stage) {
- ScaleStage.Intro -> 1.25f
- ScaleStage.Display -> 1.0f
- ScaleStage.Outro -> 1.25f
+ DialogVisibility.Hide -> 1.25f
+ DialogVisibility.Display -> 1.0f
}
}
-// Alpha transition stages - Intro and Outro are split into FadeIn/FadeOut stages.
-private enum class AlphaStage {
- IntroFadeOut, IntroFadeIn, Display, OutroFadeOut, OutroFadeIn;
-}
-
-// Scale transition stages - scaling is applied as single Intro/Outro animations.
-private enum class ScaleStage {
- Intro, Display, Outro;
-}
+private enum class DialogVisibility {
+ Hide, Display;
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Icon.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Icon.kt
index f92d72d..181a2cd1 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Icon.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Icon.kt
@@ -16,27 +16,15 @@
package androidx.wear.compose.material
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.paint
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.graphics.toolingGraphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
/**
* Icon component that draws [imageVector] using [tint], defaulting to [LocalContentColor]. For a
@@ -116,36 +104,10 @@
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
) {
// TODO: b/149735981 semantics for content description
- val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
- val semantics = if (contentDescription != null) {
- Modifier.semantics {
- this.contentDescription = contentDescription
- this.role = Role.Image
- }
- } else {
- Modifier
- }
- Box(
- modifier.toolingGraphicsLayer().defaultSizeFor(painter)
- .paint(
- painter,
- colorFilter = colorFilter,
- contentScale = ContentScale.Fit
- )
- .then(semantics)
+ androidx.wear.compose.materialcore.Icon(
+ painter = painter,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint
)
}
-
-private fun Modifier.defaultSizeFor(painter: Painter) =
- this.then(
- if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) {
- DefaultIconSizeModifier
- } else {
- Modifier
- }
- )
-
-private fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
-
-// Default icon size, for icons with no intrinsic size information
-private val DefaultIconSizeModifier = Modifier.size(24.dp)
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PickerGroup.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PickerGroup.kt
index 114c40e..a9be263 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PickerGroup.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PickerGroup.kt
@@ -320,7 +320,6 @@
return maxChildrenHeight.coerceIn(constraints.minHeight, constraints.maxHeight)
}
-@Suppress("ModifierInspectorInfo")
internal fun Modifier.autoCenteringTarget() = this.then(
object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?) = AutoCenteringRowParentData()
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
index 184e824..424101c 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
@@ -1097,7 +1097,6 @@
// Sets the size of this element, but lets the child measure using the constraints
// of the element containing this.
private fun Modifier.transparentSizeModifier(size: Density.() -> IntSize): Modifier = this.then(
- @Suppress("ModifierInspectorInfo")
object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index b725fb2..d577f3e 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -133,7 +133,6 @@
private fun Modifier.scrollAway(scrollFn: Density.() -> ScrollParams): Modifier =
this.then(
- @Suppress("ModifierInspectorInfo")
object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
index 747b122..297bc4a 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
@@ -16,7 +16,6 @@
package androidx.wear.compose.material
-import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -304,29 +303,25 @@
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
- val mergedStyle = style.merge(
- TextStyle(
- color = textColor,
- fontSize = fontSize,
- fontWeight = fontWeight,
- textAlign = textAlign,
- lineHeight = lineHeight,
- fontFamily = fontFamily,
- textDecoration = textDecoration,
- fontStyle = fontStyle,
- letterSpacing = letterSpacing
- )
- )
- BasicText(
+ androidx.wear.compose.materialcore.Text(
text = text,
modifier = modifier,
- style = mergedStyle,
- onTextLayout = onTextLayout,
+ color = textColor,
+ fontSize = fontSize,
+ fontStyle = fontStyle,
+ fontWeight = fontWeight,
+ fontFamily = fontFamily,
+ letterSpacing = letterSpacing,
+ textDecoration = textDecoration,
+ textAlign = textAlign,
+ lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
- inlineContent = inlineContent
+ inlineContent = inlineContent,
+ onTextLayout = onTextLayout,
+ style = style
)
}
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 4f95b89..f1d47a0 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
targetSdk 30
- versionCode 12
- versionName "1.12"
+ versionCode 13
+ versionName "1.13"
// Change the APK name to match the *testapp regex we use to pick up APKs for testing as
// part of CI.
archivesBaseName = "wear-compose-demos-testapp"
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index c6be1c3..e0b1320 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -16,7 +16,6 @@
package androidx.wear.compose.integration.demos
-import android.annotation.SuppressLint
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.FlingBehavior
@@ -185,7 +184,6 @@
internal data class TimestampedDelta(val time: Long, val delta: Float)
-@SuppressLint("ModifierInspectorInfo")
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("ComposableModifierFactory")
@Composable
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt
index fd110fe..8d61b3f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DialogDemo.kt
@@ -40,6 +40,7 @@
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.Colors
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.dialog.Alert
@@ -139,49 +140,53 @@
@Composable
fun DialogBackground(background: Color) {
- // By setting the background of the launcher and the dialog,
+ // By setting a bespoke background theme color for the launcher and dialog,
// it is easier to see animation details such as the vignette.
var showDialog by remember { mutableStateOf(false) }
val scrollState = rememberScalingLazyListState()
- LaunchScreen(onClick = { showDialog = true }, background = background)
- Dialog(
- showDialog = showDialog,
- onDismissRequest = { showDialog = false },
- scrollState = scrollState,
- ) {
- Alert(
- backgroundColor = background,
+ MaterialTheme(colors = Colors(background = background)) {
+ LaunchScreen(onClick = { showDialog = true })
+ Dialog(
+ showDialog = showDialog,
+ onDismissRequest = { showDialog = false },
scrollState = scrollState,
- icon = {
- DemoIcon(
- resourceId = R.drawable.ic_baseline_location_on_24,
- contentDescription = "Location"
- )
- },
- title = {
- Text(
- text = "Allow Bikemap to access this device's location?",
- textAlign = TextAlign.Center,
- color = MaterialTheme.colors.onBackground
- )
- },
- negativeButton = {
- Button(
- onClick = { showDialog = false },
- colors = ButtonDefaults.secondaryButtonColors()
- ) {
- DemoIcon(resourceId = R.drawable.ic_clear_24px, contentDescription = "Cross")
- }
- },
- positiveButton = {
- Button(
- onClick = { showDialog = false },
- colors = ButtonDefaults.primaryButtonColors()
- ) {
- DemoIcon(resourceId = R.drawable.ic_check_24px, contentDescription = "Tick")
- }
- },
- )
+ ) {
+ Alert(
+ scrollState = scrollState,
+ icon = {
+ DemoIcon(
+ resourceId = R.drawable.ic_baseline_location_on_24,
+ contentDescription = "Location"
+ )
+ },
+ title = {
+ Text(
+ text = "Allow Bikemap to access this device's location?",
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onBackground
+ )
+ },
+ negativeButton = {
+ Button(
+ onClick = { showDialog = false },
+ colors = ButtonDefaults.secondaryButtonColors()
+ ) {
+ DemoIcon(
+ resourceId = R.drawable.ic_clear_24px,
+ contentDescription = "Cross"
+ )
+ }
+ },
+ positiveButton = {
+ Button(
+ onClick = { showDialog = false },
+ colors = ButtonDefaults.primaryButtonColors()
+ ) {
+ DemoIcon(resourceId = R.drawable.ic_check_24px, contentDescription = "Tick")
+ }
+ },
+ )
+ }
}
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 450087c..dbaa9c9 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -43,7 +43,7 @@
import androidx.wear.protolayout.expression.pipeline.FloatNodes.DynamicAnimatedFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.FixedFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.Int32ToFloatNode;
-import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatSourceNode;
import androidx.wear.protolayout.expression.pipeline.InstantNodes.FixedInstantNode;
import androidx.wear.protolayout.expression.pipeline.InstantNodes.PlatformTimeSourceNode;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.AnimatableFixedInt32Node;
@@ -939,9 +939,8 @@
node = new FixedFloatNode(floatSource.getFixed(), consumer);
break;
case STATE_SOURCE:
- node =
- new StateFloatNode(
- mStateStore, floatSource.getStateSource().getSourceKey(), consumer);
+ node = new StateFloatSourceNode(
+ mStateStore, floatSource.getStateSource(), consumer);
break;
case ARITHMETIC_OPERATION:
{
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
index ebfe128..ca8133c 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
@@ -24,6 +24,7 @@
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedFloat;
import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticFloatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateFloatSource;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
/** Dynamic data nodes which yield floats. */
@@ -59,12 +60,16 @@
}
/** Dynamic float node that gets value from the state. */
- static class StateFloatNode extends StateSourceNode<Float> {
- StateFloatNode(
+ static class StateFloatSourceNode extends StateSourceNode<Float> {
+ StateFloatSourceNode(
ObservableStateStore observableStateStore,
- String bindKey,
+ StateFloatSource protoNode,
DynamicTypeValueReceiver<Float> downstream) {
- super(observableStateStore, bindKey, se -> se.getFloatVal().getValue(), downstream);
+ super(
+ observableStateStore,
+ protoNode.getSourceKey(),
+ se -> se.getFloatVal().getValue(),
+ downstream);
}
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/ColorNodesTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/ColorNodesTest.java
index 007ea68..551afbb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/ColorNodesTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/ColorNodesTest.java
@@ -57,6 +57,7 @@
FixedColor protoNode = FixedColor.newBuilder().setArgb(FROM_COLOR).build();
FixedColorNode node = new FixedColorNode(protoNode, new AddToListCallback<>(results));
+ node.preInit();
node.init();
assertThat(results).containsExactly(FROM_COLOR);
@@ -152,6 +153,7 @@
protoNode, new AddToListCallback<>(results), quotaManager);
node.setVisibility(true);
+ node.preInit();
node.init();
shadowOf(Looper.getMainLooper()).idle();
@@ -174,6 +176,7 @@
protoNode, new AddToListCallback<>(results), quotaManager);
node.setVisibility(false);
+ node.preInit();
node.init();
shadowOf(Looper.getMainLooper()).idle();
@@ -195,6 +198,7 @@
protoNode, new AddToListCallback<>(results), quotaManager);
node.setVisibility(true);
+ node.preInit();
node.init();
shadowOf(Looper.getMainLooper()).idle();
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java
new file mode 100644
index 0000000..3eb5734
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.wear.protolayout.expression.pipeline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.lang.Integer.MAX_VALUE;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Looper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.AnimatableFixedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.ArithmeticFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.DynamicAnimatedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.FixedFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.Int32ToFloatNode;
+import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatSourceNode;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.StateInt32SourceNode;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticFloatOp;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticOpType;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateFloatSource;
+import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
+import androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class FloatNodeTest {
+
+ @Test
+ public void fixedFloatNodesTest() {
+ List<Float> results = new ArrayList<>();
+ float testValue = 6.6f;
+
+ FixedFloat protoNode = FixedFloat.newBuilder().setValue(testValue).build();
+ FixedFloatNode node = new FixedFloatNode(protoNode, new AddToListCallback<>(results));
+
+ node.preInit();
+ node.init();
+
+ assertThat(results).containsExactly(testValue);
+ }
+
+ @Test
+ public void stateFloatSourceNodeTest() {
+ List<Float> results = new ArrayList<>();
+ float testValue = 6.6f;
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(testValue))
+ .build()));
+
+ StateFloatSource protoNode = StateFloatSource.newBuilder().setSourceKey("foo").build();
+ StateFloatSourceNode node =
+ new StateFloatSourceNode(oss, protoNode, new AddToListCallback<>(results));
+
+ node.preInit();
+ node.init();
+
+ assertThat(results).containsExactly(testValue);
+ }
+
+ @Test
+ public void stateFloatSourceNode_updatesWithStateChanges() {
+ List<Float> results = new ArrayList<>();
+ float oldValue = 6.5f;
+ float newValue = 7.8f;
+
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(oldValue))
+ .build()));
+
+ StateFloatSource protoNode = StateFloatSource.newBuilder().setSourceKey("foo").build();
+ StateFloatSourceNode node =
+ new StateFloatSourceNode(oss, protoNode, new AddToListCallback<>(results));
+
+ node.preInit();
+ node.init();
+ assertThat(results).containsExactly(oldValue);
+
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(newValue))
+ .build()));
+
+ assertThat(results).containsExactly(oldValue, newValue).inOrder();
+ }
+
+ @Test
+ public void stateFloatSourceNode_noUpdatesAfterDestroy() {
+ List<Float> results = new ArrayList<>();
+ float oldValue = 6.5f;
+ float newValue = 7.8f;
+
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(oldValue))
+ .build()));
+
+ StateFloatSource protoNode = StateFloatSource.newBuilder().setSourceKey("foo").build();
+ StateFloatSourceNode node =
+ new StateFloatSourceNode(oss, protoNode, new AddToListCallback<>(results));
+
+ node.preInit();
+ node.init();
+ assertThat(results).containsExactly(oldValue);
+
+ results.clear();
+ node.destroy();
+
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(newValue))
+ .build()));
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void arithmeticFloat_add() {
+ List<Float> results = new ArrayList<>();
+ ArithmeticFloatOp protoNode =
+ ArithmeticFloatOp.newBuilder()
+ .setOperationType(ArithmeticOpType.ARITHMETIC_OP_TYPE_ADD)
+ .build();
+
+ ArithmeticFloatNode node = new ArithmeticFloatNode(protoNode,
+ new AddToListCallback<>(results));
+
+ float lhsValue = 6.6f;
+ FixedFloat lhsProtoNode = FixedFloat.newBuilder().setValue(lhsValue).build();
+ FixedFloatNode lhsNode = new FixedFloatNode(lhsProtoNode, node.getLhsIncomingCallback());
+ lhsNode.init();
+
+ float oldRhsValue = 6.5f;
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(oldRhsValue))
+ .build()));
+ StateFloatSource rhsProtoNode = StateFloatSource.newBuilder().setSourceKey("foo").build();
+ StateFloatSourceNode rhsNode =
+ new StateFloatSourceNode(oss, rhsProtoNode, node.getRhsIncomingCallback());
+
+ rhsNode.preInit();
+ rhsNode.init();
+
+ assertThat(results).containsExactly(lhsValue + oldRhsValue);
+
+ float newRhsValue = 7.8f;
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(newRhsValue))
+ .build()));
+ assertThat(results).containsExactly(lhsValue + oldRhsValue,
+ lhsValue + newRhsValue).inOrder();
+ }
+
+ @Test
+ public void int32ToFloatTest() {
+ List<Float> results = new ArrayList<>();
+ Int32ToFloatNode node = new Int32ToFloatNode(new AddToListCallback<>(results));
+
+ int oldIntValue = 65;
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setInt32Val(FixedInt32.newBuilder().setValue(oldIntValue))
+ .build()));
+
+ StateInt32Source protoNode = StateInt32Source.newBuilder().setSourceKey("foo").build();
+ StateInt32SourceNode intNode =
+ new StateInt32SourceNode(oss, protoNode, node.getIncomingCallback());
+
+ intNode.preInit();
+ intNode.init();
+
+ assertThat(results).containsExactly((float) oldIntValue);
+
+ int newIntValue = 12;
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setInt32Val(FixedInt32.newBuilder().setValue(newIntValue))
+ .build()));
+
+ assertThat(results).containsExactly((float) oldIntValue, (float) newIntValue).inOrder();
+ }
+
+ @Test
+ public void animatableFixedFloat_animates() {
+ float startValue = 3.0f;
+ float endValue = 33.0f;
+ List<Float> results = new ArrayList<>();
+ QuotaManager quotaManager = new FixedQuotaManagerImpl(MAX_VALUE);
+ AnimatableFixedFloat protoNode =
+ AnimatableFixedFloat.newBuilder().setFromValue(startValue).setToValue(
+ endValue).build();
+ AnimatableFixedFloatNode node =
+ new AnimatableFixedFloatNode(protoNode, new AddToListCallback<>(results),
+ quotaManager);
+ node.setVisibility(true);
+
+ node.preInit();
+ node.init();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(results.size()).isGreaterThan(2);
+ assertThat(results.get(0)).isEqualTo(startValue);
+ assertThat(Iterables.getLast(results)).isEqualTo(endValue);
+ }
+
+ @Test
+ public void animatableFixedFloat_whenInvisible_skipToEnd() {
+ float startValue = 3.0f;
+ float endValue = 33.0f;
+ List<Float> results = new ArrayList<>();
+ QuotaManager quotaManager = new FixedQuotaManagerImpl(MAX_VALUE);
+ AnimatableFixedFloat protoNode =
+ AnimatableFixedFloat.newBuilder().setFromValue(startValue).setToValue(
+ endValue).build();
+ AnimatableFixedFloatNode node =
+ new AnimatableFixedFloatNode(protoNode, new AddToListCallback<>(results),
+ quotaManager);
+ node.setVisibility(false);
+
+ node.preInit();
+ node.init();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(results).hasSize(1);
+ assertThat(results).containsExactly(endValue);
+ }
+
+ @Test
+ public void animatableFixedFloat_whenNoQuota_skip() {
+ float startValue = 3.0f;
+ float endValue = 33.0f;
+ List<Float> results = new ArrayList<>();
+ QuotaManager quotaManager = new FixedQuotaManagerImpl(0);
+ AnimatableFixedFloat protoNode =
+ AnimatableFixedFloat.newBuilder().setFromValue(startValue).setToValue(
+ endValue).build();
+ AnimatableFixedFloatNode node =
+ new AnimatableFixedFloatNode(protoNode, new AddToListCallback<>(results),
+ quotaManager);
+ node.setVisibility(true);
+
+ node.preInit();
+ node.init();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(results).hasSize(1);
+ assertThat(results).containsExactly(endValue);
+ }
+
+ @Test
+ public void dynamicAnimatedFloat_onlyAnimateWhenVisible() {
+ float value1 = 3.0f;
+ float value2 = 11.0f;
+ float value3 = 17.0f;
+ List<Float> results = new ArrayList<>();
+ QuotaManager quotaManager = new FixedQuotaManagerImpl(MAX_VALUE);
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(
+ FixedFloat.newBuilder().setValue(value1).build())
+ .build()));
+ DynamicAnimatedFloatNode floatNode =
+ new DynamicAnimatedFloatNode(
+ new AddToListCallback<>(results), AnimationSpec.getDefaultInstance(),
+ quotaManager);
+ floatNode.setVisibility(false);
+ StateFloatSourceNode stateNode =
+ new StateFloatSourceNode(
+ oss,
+ StateFloatSource.newBuilder().setSourceKey("foo").build(),
+ floatNode.getInputCallback());
+
+ stateNode.preInit();
+ stateNode.init();
+
+ results.clear();
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(value2))
+ .build()));
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Only contains last value.
+ assertThat(results).hasSize(1);
+ assertThat(results).containsExactly(value2);
+
+ floatNode.setVisibility(true);
+ results.clear();
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setFloatVal(FixedFloat.newBuilder().setValue(value3))
+ .build()));
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Contains intermediate values besides the initial and last.
+ assertThat(results.size()).isGreaterThan(2);
+ assertThat(results.get(0)).isEqualTo(value2);
+ assertThat(Iterables.getLast(results)).isEqualTo(value3);
+ assertThat(results).isInOrder();
+ }
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/InstantNodesTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/InstantNodesTest.java
index 14da1b7..5c4d32d 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/InstantNodesTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/InstantNodesTest.java
@@ -47,6 +47,7 @@
FixedInstant protoNode = FixedInstant.newBuilder().setEpochSeconds(1234567L).build();
FixedInstantNode node = new FixedInstantNode(protoNode, new AddToListCallback<>(results));
+ node.preInit();
node.init();
assertThat(results).containsExactly(Instant.ofEpochSecond(1234567L));
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
index 4f610b2..46819e5 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
@@ -57,6 +57,7 @@
FixedInt32 protoNode = FixedInt32.newBuilder().setValue(56).build();
FixedInt32Node node = new FixedInt32Node(protoNode, new AddToListCallback<>(results));
+ node.preInit();
node.init();
assertThat(results).containsExactly(56);
@@ -219,6 +220,7 @@
protoNode, new AddToListCallback<>(results), quotaManager);
node.setVisibility(true);
+ node.preInit();
node.init();
shadowOf(Looper.getMainLooper()).idle();
@@ -243,6 +245,7 @@
protoNode, new AddToListCallback<>(results), quotaManager);
node.setVisibility(false);
+ node.preInit();
node.init();
shadowOf(Looper.getMainLooper()).idle();
@@ -266,6 +269,7 @@
protoNode, new AddToListCallback<>(results), quotaManager);
node.setVisibility(true);
+ node.preInit();
node.init();
shadowOf(Looper.getMainLooper()).idle();
diff --git a/wear/protolayout/protolayout-renderer/api/restricted_current.txt b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
index a25d1ea..3c69696 100644
--- a/wear/protolayout/protolayout-renderer/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
@@ -17,5 +17,9 @@
method public com.google.common.util.concurrent.ListeningExecutorService getUiExecutorService();
}
+ public static interface ProtoLayoutViewInstance.LoadActionListener {
+ method public void onClick(androidx.wear.protolayout.proto.StateProto.State);
+ }
+
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 53b3089..884bdcb 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -39,12 +39,12 @@
import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.ResourceProto;
+import androidx.wear.protolayout.proto.StateProto;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
-import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.LoadActionListener;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewGroupMutation;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewMutationException;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutThemeImpl;
@@ -68,6 +68,20 @@
*/
@RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
public class ProtoLayoutViewInstance implements AutoCloseable {
+ /**
+ * Listener for clicks on Clickable objects that have an Action to (re)load the contents of a
+ * layout.
+ */
+ public interface LoadActionListener {
+
+ /**
+ * Called when a Clickable that has a LoadAction is clicked.
+ *
+ * @param nextState The state that the next layout should be in.
+ */
+ void onClick(@NonNull StateProto.State nextState);
+ }
+
private static final int DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS = 4;
@NonNull private static final String TAG = "ProtoLayoutViewInstance";
@@ -688,7 +702,7 @@
ProtoLayoutInflater.Config.Builder inflaterConfigBuilder =
new ProtoLayoutInflater.Config.Builder(mUiContext, layout, resolvers)
.setLoadActionExecutor(mUiExecutorService)
- .setLoadActionListener(mLoadActionListener)
+ .setLoadActionListener(mLoadActionListener::onClick)
.setRendererResources(mRendererResources)
.setProtoLayoutTheme(mProtoLayoutTheme)
.setAnimationEnabled(mAnimationEnabled)
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/DefaultInlineImageResourceResolver.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/DefaultInlineImageResourceResolver.java
index 118a260..dfd21df 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/DefaultInlineImageResourceResolver.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/DefaultInlineImageResourceResolver.java
@@ -22,6 +22,7 @@
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,6 +36,7 @@
/** Resource resolver for inline resources. */
public class DefaultInlineImageResourceResolver implements InlineImageResourceResolver {
private static final int RGB565_BYTES_PER_PX = 2;
+ private static final String TAG = "InlineImageResolver";
@NonNull private final Context mAppContext;
@@ -102,12 +104,15 @@
return bitmap;
}
- @NonNull
+ @Nullable
private Bitmap loadStructuredBitmap(@NonNull InlineImageResource inlineImage) {
Bitmap bitmap =
BitmapFactory.decodeByteArray(
inlineImage.getData().toByteArray(), 0, inlineImage.getData().size());
-
+ if (bitmap == null) {
+ Log.e(TAG, "Unable to load structured bitmap.");
+ return null;
+ }
return Bitmap.createScaledBitmap(
bitmap, inlineImage.getWidthPx(), inlineImage.getHeightPx(), /* filter= */ true);
}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
index bf29998..5277c40 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
@@ -26,6 +26,7 @@
import androidx.annotation.Dimension;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
import androidx.wear.tiles.DeviceParametersBuilders;
@@ -94,6 +95,7 @@
.collect(Collectors.toList());
}
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
@Test
public void test() {
runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
index f42d628..0130f11 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
@@ -28,6 +28,7 @@
import androidx.annotation.Dimension;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
import androidx.wear.tiles.DeviceParametersBuilders;
@@ -134,6 +135,7 @@
getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
}
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
@Test
public void test() {
runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java
index 067e846..72498b3 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenTest.java
@@ -26,6 +26,7 @@
import androidx.annotation.Dimension;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
import androidx.wear.tiles.DeviceParametersBuilders;
@@ -94,6 +95,7 @@
.collect(Collectors.toList());
}
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
@Test
public void test() {
runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
index 41ee4be..ff3b8d9 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/layouts/LayoutsGoldenXLTest.java
@@ -28,6 +28,7 @@
import androidx.annotation.Dimension;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.screenshot.AndroidXScreenshotTestRule;
import androidx.wear.tiles.DeviceParametersBuilders;
@@ -143,6 +144,7 @@
getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
}
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
@Test
public void test() {
runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 7666c84..1432c9e 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -28,6 +28,7 @@
import androidx.wear.protolayout.LayoutElementBuilders;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.protolayout.proto.ResourceProto;
+import androidx.wear.protolayout.proto.StateProto;
import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance;
import androidx.wear.tiles.ResourceBuilders;
import androidx.wear.tiles.StateBuilders;
@@ -104,15 +105,17 @@
@NonNull androidx.wear.tiles.ResourceBuilders.Resources resources,
@NonNull Executor loadActionExecutor,
@NonNull LoadActionListener loadActionListener) {
- // TODO(b/270678226): Use load action listener.
-
this.mLayout = fromTileLayout(layout);
this.mResources = fromTileResources(resources);
this.mUiExecutor = MoreExecutors.newDirectExecutorService();
+ ProtoLayoutViewInstance.LoadActionListener instanceListener =
+ nextState -> loadActionExecutor.execute(
+ () -> loadActionListener.onClick(fromProtoLayoutState(nextState)));
ProtoLayoutViewInstance.Config.Builder config =
new ProtoLayoutViewInstance.Config.Builder(uiContext, mUiExecutor, mUiExecutor,
- TileService.EXTRA_CLICKABLE_ID);
+ TileService.EXTRA_CLICKABLE_ID)
+ .setLoadActionListener(instanceListener);
this.mInstance = new ProtoLayoutViewInstance(config.build());
}
@@ -130,6 +133,10 @@
.fromByteArray(layout.toByteArray())).toProto();
}
+ @NonNull StateBuilders.State fromProtoLayoutState(@NonNull StateProto.State state) {
+ return StateBuilders.State.fromProto(state);
+ }
+
/**
* Inflates a Tile into {@code parent}.
*
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 2ce63f3..fe929f3 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -52,8 +52,8 @@
ctor @Deprecated public DefaultComplicationDataSourcePolicyAndType(androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy, androidx.wear.watchface.complications.data.ComplicationType type);
method @Deprecated public androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy getPolicy();
method @Deprecated public androidx.wear.watchface.complications.data.ComplicationType getType();
- property public final androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy;
- property public final androidx.wear.watchface.complications.data.ComplicationType type;
+ property @Deprecated public final androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy;
+ property @Deprecated public final androidx.wear.watchface.complications.data.ComplicationType type;
}
public final class DeviceConfig {
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index 2deaca3..ee9780b 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -56,8 +56,8 @@
ctor @Deprecated public DefaultComplicationDataSourcePolicyAndType(androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy, androidx.wear.watchface.complications.data.ComplicationType type);
method @Deprecated public androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy getPolicy();
method @Deprecated public androidx.wear.watchface.complications.data.ComplicationType getType();
- property public final androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy;
- property public final androidx.wear.watchface.complications.data.ComplicationType type;
+ property @Deprecated public final androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy;
+ property @Deprecated public final androidx.wear.watchface.complications.data.ComplicationType type;
}
public final class DeviceConfig {
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 2ce63f3..fe929f3 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -52,8 +52,8 @@
ctor @Deprecated public DefaultComplicationDataSourcePolicyAndType(androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy, androidx.wear.watchface.complications.data.ComplicationType type);
method @Deprecated public androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy getPolicy();
method @Deprecated public androidx.wear.watchface.complications.data.ComplicationType getType();
- property public final androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy;
- property public final androidx.wear.watchface.complications.data.ComplicationType type;
+ property @Deprecated public final androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy policy;
+ property @Deprecated public final androidx.wear.watchface.complications.data.ComplicationType type;
}
public final class DeviceConfig {
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt
index 398e80b..d8a518d 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt
@@ -15,7 +15,6 @@
*/
package androidx.wear.watchface.complications.datasource
-import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.content.Intent
import android.content.res.Resources
import android.os.Build
@@ -23,10 +22,10 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.RemoteException
+import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.IComplicationManager
import android.support.wearable.complications.IComplicationProvider
import android.util.Log
-import androidx.wear.protolayout.expression.DynamicBuilders
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
import androidx.wear.watchface.complications.data.ComplicationData
@@ -178,10 +177,12 @@
@Test
fun testOnComplicationRequest_isForSafeWatchFace() {
- mService.responseData = LongTextComplicationData.Builder(
- PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build()
+ mService.responseData =
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ )
+ .build()
val id = 123
@Suppress("NewApi") // onUpdate2
@@ -199,16 +200,17 @@
runUiThreadTasksWhileAwaitingDataLatch(1000)
@Suppress("NewApi") // isForSafeWatchFace
- assertThat(mService.lastRequest!!.isForSafeWatchFace)
- .isEqualTo(TargetWatchFaceSafety.SAFE)
+ assertThat(mService.lastRequest!!.isForSafeWatchFace).isEqualTo(TargetWatchFaceSafety.SAFE)
}
@Test
fun testOnComplicationRequest_isForSafeWatchFace_malformedBundle() {
- mService.responseData = LongTextComplicationData.Builder(
- PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build()
+ mService.responseData =
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ )
+ .build()
val id = 123
@Suppress("NewApi") // onUpdate2
@@ -232,8 +234,7 @@
mService.responseData =
LongTextComplicationData.Builder(
ComplicationTextExpression(
- DynamicBuilders.DynamicString.constant("hello")
- .concat(DynamicBuilders.DynamicString.constant(" world"))
+ DynamicString.constant("hello").concat(DynamicString.constant(" world"))
),
ComplicationText.EMPTY
)
@@ -251,8 +252,8 @@
eq(
LongTextComplicationData.Builder(
ComplicationTextExpression(
- DynamicBuilders.DynamicString.constant("hello")
- .concat(DynamicBuilders.DynamicString.constant(" world"))
+ DynamicString.constant("hello")
+ .concat(DynamicString.constant(" world"))
),
ComplicationText.EMPTY
)
@@ -269,8 +270,7 @@
mService.responseData =
LongTextComplicationData.Builder(
ComplicationTextExpression(
- DynamicBuilders.DynamicString.constant("hello")
- .concat(DynamicBuilders.DynamicString.constant(" world"))
+ DynamicString.constant("hello").concat(DynamicString.constant(" world"))
),
ComplicationText.EMPTY
)
@@ -287,7 +287,10 @@
.updateComplicationData(
eq(123),
argThat { data ->
- data.longText!!.getTextAt(Resources.getSystem(), 0) == "hello world"
+ data.longText ==
+ PlainComplicationText.Builder("hello world")
+ .build()
+ .toWireComplicationText()
}
)
}
@@ -516,10 +519,12 @@
@Suppress("NewApi") // onSynchronousComplicationRequest2
fun testImmediateRequest_isForSafeWatchFace() {
val id = 123
- mService.responseData = LongTextComplicationData.Builder(
- PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build()
+ mService.responseData =
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ )
+ .build()
val thread = HandlerThread("testThread")
try {
thread.start()
@@ -561,10 +566,12 @@
@Suppress("NewApi") // onSynchronousComplicationRequest2
fun testImmediateRequest_isForSafeWatchFace_malformedBundle() {
val id = 123
- mService.responseData = LongTextComplicationData.Builder(
- PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build()
+ mService.responseData =
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ )
+ .build()
val thread = HandlerThread("testThread")
try {
thread.start()
diff --git a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl b/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
index 404f8e7..51d0d9e 100644
--- a/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
+++ b/wear/watchface/watchface-complications-data/src/main/aidl/android/support/wearable/complications/IComplicationProvider.aidl
@@ -10,12 +10,12 @@
interface IComplicationProvider {
// IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
// in the future to remain binary backwards compatible.
- // Next Id: 8
+ // Next Id: 10
/**
* API version number. This should be incremented every time a new method is added.
*/
- const int API_VERSION = 3;
+ const int API_VERSION = 4;
/** See {@link TargetWatchFaceSafety}. This field has an integer value. */
const String BUNDLE_KEY_IS_SAFE_FOR_WATCHFACE = "IsSafeForWatchFace";
@@ -146,7 +146,7 @@
* @param manager The binder for IComplicationManager
* @param bundle A {@link Bundle} containing {@link #BUNDLE_KEY_IS_SAFE_FOR_WATCHFACE}.
*
- * @since API version 3.
+ * @since API version 4.
*/
void onUpdate2(int complicationInstanceId, int type, IBinder manager, in Bundle bundle) = 8;
@@ -159,7 +159,7 @@
* @param type The type of complication requested
* @param bundle A {@link Bundle} containing {@link #BUNDLE_KEY_IS_SAFE_FOR_WATCHFACE}.
*
- * @since API version 3.
+ * @since API version 4.
*/
ComplicationData onSynchronousComplicationRequest2(
int complicationInstanceId, int type, in Bundle bundle) = 9;
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index b1f0494..39310fd 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -48,13 +48,13 @@
* [androidx.wear.protolayout.expression.DynamicBuilders.DynamicType] within its fields.
*
* Due to [WireComplicationData]'s shallow copy strategy the input is modified in-place.
- *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ComplicationDataExpressionEvaluator(
val unevaluatedData: WireComplicationData,
private val stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
private val sensorGateway: SensorGateway? = null,
+ private val keepExpression: Boolean = false,
) : AutoCloseable {
/**
* Java compatibility class for [ComplicationDataExpressionEvaluator].
@@ -62,15 +62,37 @@
* Unlike [data], [listener] is not invoked until there is a value (until [data] is non-null).
*/
class Compat
- @JvmOverloads
- constructor(
+ internal constructor(
val unevaluatedData: WireComplicationData,
private val listener: Consumer<WireComplicationData>,
- stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
- sensorGateway: SensorGateway? = null,
+ stateStore: ObservableStateStore,
+ sensorGateway: SensorGateway?,
+ keepExpression: Boolean,
) : AutoCloseable {
private val evaluator =
- ComplicationDataExpressionEvaluator(unevaluatedData, stateStore, sensorGateway)
+ ComplicationDataExpressionEvaluator(
+ unevaluatedData,
+ stateStore,
+ sensorGateway,
+ keepExpression,
+ )
+
+ /** Builder for [ComplicationDataExpressionEvaluator.Compat]. */
+ class Builder(
+ private val unevaluatedData: WireComplicationData,
+ private val listener: Consumer<WireComplicationData>,
+ ) {
+ private var stateStore: ObservableStateStore = ObservableStateStore(emptyMap())
+ private var sensorGateway: SensorGateway? = null
+ private var keepExpression: Boolean = false
+
+ fun setStateStore(value: ObservableStateStore) = apply { stateStore = value }
+ fun setSensorGateway(value: SensorGateway?) = apply { sensorGateway = value }
+ fun setKeepExpression(value: Boolean) = apply { keepExpression = value }
+
+ fun build() =
+ Compat(unevaluatedData, listener, stateStore, sensorGateway, keepExpression)
+ }
/**
* @see ComplicationDataExpressionEvaluator.init, [executor] is used in place of
@@ -133,7 +155,11 @@
if (unevaluatedData.hasRangedValueExpression()) {
unevaluatedData.rangedValueExpression
- ?.buildReceiver(coroutineScope) { setRangedValue(it) }
+ ?.buildReceiver(
+ coroutineScope,
+ expressionTrimmer = { setRangedValueExpression(null) },
+ setter = { setRangedValue(it) },
+ )
?.let { receivers += it }
}
if (unevaluatedData.hasLongText()) {
@@ -167,10 +193,14 @@
private fun DynamicFloat.buildReceiver(
coroutineScope: CoroutineScope,
- setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder
+ expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
+ setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
) =
ComplicationEvaluationResultReceiver(
- setter,
+ setter = {
+ if (!keepExpression) expressionTrimmer(this)
+ setter(this, it)
+ },
binder = { receiver ->
evaluator.bind(
this@buildReceiver,
@@ -182,11 +212,19 @@
private fun WireComplicationText.buildReceiver(
coroutineScope: CoroutineScope,
- setter: WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder
+ setter: WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
) =
expression?.let { expression ->
ComplicationEvaluationResultReceiver<String>(
- setter = { setter(WireComplicationText(it, expression)) },
+ setter = {
+ setter(
+ if (keepExpression) {
+ WireComplicationText(it, expression)
+ } else {
+ WireComplicationText(it)
+ }
+ )
+ },
binder = { receiver ->
evaluator.bind(
expression,
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 5834c42..279a8b0 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -76,12 +76,12 @@
}
/**
- * Scenarios for testing expressions.
+ * Scenarios for testing data with expressions.
*
* Each scenario describes the expressed data, the flow of states, and the flow of the evaluated
* data output.
*/
- enum class DataExpressionScenario(
+ enum class DataWithExpressionScenario(
val expressed: WireComplicationData,
val states: List<Map<String, StateEntryValue>>,
val evaluated: List<WireComplicationData>,
@@ -103,28 +103,11 @@
listOf(
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setRangedValue(1f)
- .setRangedValueExpression(DynamicFloat.constant(1f))
- .setLongText(
- WireComplicationText("Long Text", DynamicString.constant("Long Text"))
- )
- .setLongTitle(
- WireComplicationText("Long Title", DynamicString.constant("Long Title"))
- )
- .setShortText(
- WireComplicationText("Short Text", DynamicString.constant("Short Text"))
- )
- .setShortTitle(
- WireComplicationText(
- "Short Title",
- DynamicString.constant("Short Title")
- )
- )
- .setContentDescription(
- WireComplicationText(
- "Description",
- DynamicString.constant("Description")
- )
- )
+ .setLongText(WireComplicationText("Long Text"))
+ .setLongTitle(WireComplicationText("Long Title"))
+ .setShortText(WireComplicationText("Short Text"))
+ .setShortTitle(WireComplicationText("Short Title"))
+ .setContentDescription(WireComplicationText("Description"))
.build()
),
),
@@ -156,34 +139,11 @@
INVALID_DATA, // Before state is available.
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setRangedValue(1f)
- .setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
- .setLongText(
- WireComplicationText("Long Text", DynamicString.fromState("long_text"))
- )
- .setLongTitle(
- WireComplicationText(
- "Long Title",
- DynamicString.fromState("long_title")
- )
- )
- .setShortText(
- WireComplicationText(
- "Short Text",
- DynamicString.fromState("short_text")
- )
- )
- .setShortTitle(
- WireComplicationText(
- "Short Title",
- DynamicString.fromState("short_title")
- )
- )
- .setContentDescription(
- WireComplicationText(
- "Description",
- DynamicString.fromState("description")
- )
- )
+ .setLongText(WireComplicationText("Long Text"))
+ .setLongTitle(WireComplicationText("Long Title"))
+ .setShortText(WireComplicationText("Short Text"))
+ .setShortTitle(WireComplicationText("Short Title"))
+ .setContentDescription(WireComplicationText("Description"))
.build()
),
),
@@ -201,12 +161,8 @@
listOf(
INVALID_DATA, // Before state is available.
WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortTitle(
- WireComplicationText("Valid", DynamicString.fromState("valid"))
- )
- .setShortText(
- WireComplicationText("Valid", DynamicString.fromState("valid"))
- )
+ .setShortTitle(WireComplicationText("Valid"))
+ .setShortText(WireComplicationText("Valid"))
.build(),
),
),
@@ -244,12 +200,8 @@
listOf(
INVALID_DATA, // Before state is available.
WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortTitle(
- WireComplicationText("Valid", DynamicString.fromState("valid"))
- )
- .setShortText(
- WireComplicationText("Valid", DynamicString.fromState("invalid"))
- )
+ .setShortTitle(WireComplicationText("Valid"))
+ .setShortText(WireComplicationText("Valid"))
.build(),
INVALID_DATA, // After it was invalidated.
),
@@ -257,8 +209,8 @@
}
@Test
- fun data_expression_setToEvaluated() {
- for (scenario in DataExpressionScenario.values()) {
+ fun data_withExpression_setToEvaluated() {
+ for (scenario in DataWithExpressionScenario.values()) {
// Defensive copy due to in-place evaluation.
val expressed = WireComplicationData.Builder(scenario.expressed).build()
val stateStore = ObservableStateStore(mapOf())
@@ -287,6 +239,52 @@
}
}
+ @Test
+ fun data_keepExpression_doesNotTrimUnevaluatedExpression() {
+ val expressed =
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
+ .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
+ .setShortText(WireComplicationText(DynamicString.constant("Short Text")))
+ .setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
+ .setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+ .build()
+ ComplicationDataExpressionEvaluator(expressed, keepExpression = true).use { evaluator ->
+ evaluator.init()
+ runUiThreadTasks()
+
+ assertThat(evaluator.data.value)
+ .isEqualTo(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValue(1f)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .setLongText(
+ WireComplicationText("Long Text", DynamicString.constant("Long Text"))
+ )
+ .setLongTitle(
+ WireComplicationText("Long Title", DynamicString.constant("Long Title"))
+ )
+ .setShortText(
+ WireComplicationText("Short Text", DynamicString.constant("Short Text"))
+ )
+ .setShortTitle(
+ WireComplicationText(
+ "Short Title",
+ DynamicString.constant("Short Title")
+ )
+ )
+ .setContentDescription(
+ WireComplicationText(
+ "Description",
+ DynamicString.constant("Description")
+ )
+ )
+ .build()
+ )
+ }
+ }
+
enum class HasExpressionDataWithExpressionScenario(val data: WireComplicationData) {
RANGED_VALUE(
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
@@ -334,22 +332,25 @@
@Test
fun compat_notInitialized_listenerNotInvoked() {
- ComplicationDataExpressionEvaluator.Compat(DATA_WITH_NO_EXPRESSION, listener).use {
- runUiThreadTasks()
+ ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
+ .build()
+ .use {
+ runUiThreadTasks()
- verify(listener, never()).accept(any())
- }
+ verify(listener, never()).accept(any())
+ }
}
@Test
fun compat_noExpression_listenerInvokedWithData() {
- ComplicationDataExpressionEvaluator.Compat(DATA_WITH_NO_EXPRESSION, listener).use {
- evaluator ->
- evaluator.init(ContextCompat.getMainExecutor(getApplicationContext()))
- runUiThreadTasks()
+ ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
+ .build()
+ .use { evaluator ->
+ evaluator.init(ContextCompat.getMainExecutor(getApplicationContext()))
+ runUiThreadTasks()
- verify(listener, times(1)).accept(DATA_WITH_NO_EXPRESSION)
- }
+ verify(listener, times(1)).accept(DATA_WITH_NO_EXPRESSION)
+ }
}
private companion object {
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
index f517baa..873ef14 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
@@ -47,6 +47,8 @@
import androidx.wear.watchface.complications.data.SmallImageType
import androidx.wear.watchface.complications.data.toApiComplicationData
import androidx.wear.watchface.utility.TraceEvent
+import androidx.wear.watchface.utility.iconEquals
+import androidx.wear.watchface.utility.iconHashCode
import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -380,7 +382,7 @@
if (appName != other.appName) return false
if (name != other.name) return false
if (type != other.type) return false
- if (icon != other.icon) return false
+ if (!(icon iconEquals other.icon)) return false
if (componentName != other.componentName) return false
return true
@@ -390,7 +392,7 @@
var result = appName.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + type.hashCode()
- result = 31 * result + icon.hashCode()
+ result = 31 * result + icon.iconHashCode()
result = 31 * result + componentName.hashCode()
return result
}
diff --git a/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt b/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
index ded8016..4108ebf 100644
--- a/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
+++ b/wear/watchface/watchface-complications/src/test/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetrieverTest.kt
@@ -34,8 +34,12 @@
import com.google.common.truth.Truth.assertThat
import kotlin.jvm.java
import org.mockito.Mockito
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
-@org.junit.runner.RunWith(SharedRobolectricTestRunner::class)
+@RunWith(SharedRobolectricTestRunner::class)
public class ComplicationDataSourceInfoRetrieverTest {
private val mockService = Mockito.mock(IProviderInfoService::class.java)
private val mockBinder = Mockito.mock(android.os.IBinder::class.java)
@@ -43,7 +47,7 @@
ComplicationDataSourceInfoRetriever(mockService)
private val resources = ApplicationProvider.getApplicationContext<Context>().resources
- @org.junit.Test
+ @Test
@Suppress("NewApi") // retrievePreviewComplicationData
public fun retrievePreviewComplicationData() {
kotlinx.coroutines.runBlocking {
@@ -54,21 +58,21 @@
val testData: ComplicationData =
LongTextComplicationData.Builder(
- PlainComplicationText.Builder("Test Text").build(),
- ComplicationText.Companion.EMPTY
- )
+ PlainComplicationText.Builder("Test Text").build(),
+ ComplicationText.Companion.EMPTY
+ )
.build()
Mockito.doAnswer {
- val callback = it.arguments[2] as IPreviewComplicationDataCallback
- callback.updateComplicationData(testData.asWireComplicationData())
- true
- }
+ val callback = it.arguments[2] as IPreviewComplicationDataCallback
+ callback.updateComplicationData(testData.asWireComplicationData())
+ true
+ }
.`when`(mockService)
.requestPreviewComplicationData(
- org.mockito.ArgumentMatchers.eq(component),
- org.mockito.ArgumentMatchers.eq(type.toWireComplicationType()),
- org.mockito.ArgumentMatchers.any()
+ eq(component),
+ eq(type.toWireComplicationType()),
+ any()
)
val previewData =
@@ -78,18 +82,18 @@
)!!
assertThat(previewData.type).isEqualTo(type)
assertThat(
- (previewData as LongTextComplicationData)
- .text
- .getTextAt(
- ApplicationProvider.getApplicationContext<Context>().resources,
- java.time.Instant.EPOCH
- )
- )
+ (previewData as LongTextComplicationData)
+ .text
+ .getTextAt(
+ ApplicationProvider.getApplicationContext<Context>().resources,
+ java.time.Instant.EPOCH
+ )
+ )
.isEqualTo("Test Text")
}
}
- @org.junit.Test
+ @Test
@Suppress("NewApi") // retrievePreviewComplicationData
public fun retrievePreviewComplicationData_DataSourceReturnsNull() {
kotlinx.coroutines.runBlocking {
@@ -99,28 +103,28 @@
Mockito.`when`(mockService.asBinder()).thenReturn(mockBinder)
Mockito.doAnswer {
- val callback = it.arguments[2] as IPreviewComplicationDataCallback
- callback.updateComplicationData(null)
- true
- }
+ val callback = it.arguments[2] as IPreviewComplicationDataCallback
+ callback.updateComplicationData(null)
+ true
+ }
.`when`(mockService)
.requestPreviewComplicationData(
- org.mockito.ArgumentMatchers.eq(component),
- org.mockito.ArgumentMatchers.eq(type.toWireComplicationType()),
- org.mockito.ArgumentMatchers.any()
+ eq(component),
+ eq(type.toWireComplicationType()),
+ any()
)
assertThat(
- complicationDataSourceInfoRetriever.retrievePreviewComplicationData(
- component,
- type
- )
+ complicationDataSourceInfoRetriever.retrievePreviewComplicationData(
+ component,
+ type
)
+ )
.isNull()
}
}
- @org.junit.Test
+ @Test
@Suppress("NewApi") // retrievePreviewComplicationData
public fun retrievePreviewComplicationDataApiNotSupported() {
kotlinx.coroutines.runBlocking {
@@ -130,16 +134,16 @@
Mockito.`when`(mockService.asBinder()).thenReturn(mockBinder)
assertThat(
- complicationDataSourceInfoRetriever.retrievePreviewComplicationData(
- component,
- type
- )
+ complicationDataSourceInfoRetriever.retrievePreviewComplicationData(
+ component,
+ type
)
+ )
.isNull()
}
}
- @org.junit.Test
+ @Test
@Suppress("NewApi") // retrievePreviewComplicationData
public fun retrievePreviewComplicationDataApiReturnsFalse() {
kotlinx.coroutines.runBlocking {
@@ -150,22 +154,22 @@
Mockito.doAnswer { false }
.`when`(mockService)
.requestPreviewComplicationData(
- org.mockito.ArgumentMatchers.eq(component),
- org.mockito.ArgumentMatchers.eq(type.toWireComplicationType()),
- org.mockito.ArgumentMatchers.any()
+ eq(component),
+ eq(type.toWireComplicationType()),
+ any()
)
assertThat(
- complicationDataSourceInfoRetriever.retrievePreviewComplicationData(
- component,
- type
- )
+ complicationDataSourceInfoRetriever.retrievePreviewComplicationData(
+ component,
+ type
)
+ )
.isNull()
}
}
- @org.junit.Test
+ @Test
public fun complicationDataSourceInfo_NullComponentName() {
val complicationDataSourceInfo =
ComplicationDataSourceInfo(
@@ -183,131 +187,131 @@
)
}
- @org.junit.Test
+ @Test
public fun createShortTextFallbackPreviewData() {
val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
val shortTextPreviewData =
ComplicationDataSourceInfo(
- "applicationName",
- "complicationName",
- icon,
- ComplicationType.SHORT_TEXT,
- componentName = null
- )
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.SHORT_TEXT,
+ componentName = null
+ )
.fallbackPreviewData as ShortTextComplicationData
assertThat(shortTextPreviewData.text.getTextAt(resources, java.time.Instant.EPOCH))
.isEqualTo("complic")
assertThat(
- shortTextPreviewData.contentDescription!!.getTextAt(
- resources,
- java.time.Instant.EPOCH
- )
+ shortTextPreviewData.contentDescription!!.getTextAt(
+ resources,
+ java.time.Instant.EPOCH
)
+ )
.isEqualTo("complicationName")
assertThat(shortTextPreviewData.monochromaticImage!!.image).isEqualTo(icon)
}
- @org.junit.Test
+ @Test
public fun createLongTextFallbackPreviewData() {
val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
val longTextPreviewData =
ComplicationDataSourceInfo(
- "applicationName",
- "complicationName",
- icon,
- ComplicationType.LONG_TEXT,
- componentName = null
- )
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.LONG_TEXT,
+ componentName = null
+ )
.fallbackPreviewData as LongTextComplicationData
assertThat(longTextPreviewData.text.getTextAt(resources, java.time.Instant.EPOCH))
.isEqualTo("complicationName")
assertThat(
- longTextPreviewData.contentDescription!!.getTextAt(
- resources,
- java.time.Instant.EPOCH
- )
+ longTextPreviewData.contentDescription!!.getTextAt(
+ resources,
+ java.time.Instant.EPOCH
)
+ )
.isEqualTo("complicationName")
assertThat(longTextPreviewData.monochromaticImage!!.image).isEqualTo(icon)
}
- @org.junit.Test
+ @Test
public fun createSmallImageFallbackPreviewData() {
val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
val smallImagePreviewData =
ComplicationDataSourceInfo(
- "applicationName",
- "complicationName",
- icon,
- ComplicationType.SMALL_IMAGE,
- componentName = null
- )
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.SMALL_IMAGE,
+ componentName = null
+ )
.fallbackPreviewData as SmallImageComplicationData
assertThat(smallImagePreviewData.smallImage.image).isEqualTo(icon)
assertThat(
- smallImagePreviewData.contentDescription!!.getTextAt(
- resources,
- java.time.Instant.EPOCH
- )
+ smallImagePreviewData.contentDescription!!.getTextAt(
+ resources,
+ java.time.Instant.EPOCH
)
+ )
.isEqualTo("complicationName")
}
- @org.junit.Test
+ @Test
public fun createPhotoImageFallbackPreviewData() {
val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
val photoImagePreviewData =
ComplicationDataSourceInfo(
- "applicationName",
- "complicationName",
- icon,
- ComplicationType.PHOTO_IMAGE,
- componentName = null
- )
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.PHOTO_IMAGE,
+ componentName = null
+ )
.fallbackPreviewData as PhotoImageComplicationData
assertThat(photoImagePreviewData.photoImage).isEqualTo(icon)
assertThat(
- photoImagePreviewData.contentDescription!!.getTextAt(
- resources,
- java.time.Instant.EPOCH
- )
+ photoImagePreviewData.contentDescription!!.getTextAt(
+ resources,
+ java.time.Instant.EPOCH
)
+ )
.isEqualTo("complicationName")
}
- @org.junit.Test
+ @Test
public fun createMonochromaticImageFallbackPreviewData() {
val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
val monochromaticImagePreviewData =
ComplicationDataSourceInfo(
- "applicationName",
- "complicationName",
- icon,
- ComplicationType.MONOCHROMATIC_IMAGE,
- componentName = null
- )
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.MONOCHROMATIC_IMAGE,
+ componentName = null
+ )
.fallbackPreviewData as MonochromaticImageComplicationData
assertThat(monochromaticImagePreviewData.monochromaticImage.image).isEqualTo(icon)
assertThat(
- monochromaticImagePreviewData.contentDescription!!.getTextAt(
- resources,
- java.time.Instant.EPOCH
- )
+ monochromaticImagePreviewData.contentDescription!!.getTextAt(
+ resources,
+ java.time.Instant.EPOCH
)
+ )
.isEqualTo("complicationName")
}
- @org.junit.Test
+ @Test
public fun createRangedValueFallbackPreviewData() {
val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
val rangedValuePreviewData =
ComplicationDataSourceInfo(
- "applicationName",
- "complicationName",
- icon,
- ComplicationType.RANGED_VALUE,
- componentName = null
- )
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.RANGED_VALUE,
+ componentName = null
+ )
.fallbackPreviewData as RangedValueComplicationData
assertThat(rangedValuePreviewData.min).isEqualTo(0.0f)
assertThat(rangedValuePreviewData.max).isEqualTo(100.0f)
@@ -316,11 +320,44 @@
.isEqualTo("complicationName")
assertThat(rangedValuePreviewData.monochromaticImage!!.image).isEqualTo(icon)
assertThat(
- rangedValuePreviewData.contentDescription!!.getTextAt(
- resources,
- java.time.Instant.EPOCH
- )
+ rangedValuePreviewData.contentDescription!!.getTextAt(
+ resources,
+ java.time.Instant.EPOCH
)
+ )
.isEqualTo("complicationName")
}
+
+ @Test
+ public fun complicationDataSourceInfo_equals() {
+ val icon = android.graphics.drawable.Icon.createWithContentUri("icon")
+ val icon2 = android.graphics.drawable.Icon.createWithContentUri("icon")
+ val a = ComplicationDataSourceInfo(
+ "applicationName",
+ "complicationName",
+ icon,
+ ComplicationType.RANGED_VALUE,
+ componentName = null
+ )
+ val b = ComplicationDataSourceInfo(
+ "applicationName",
+ "complicationName",
+ icon2,
+ ComplicationType.RANGED_VALUE,
+ componentName = null
+ )
+ val c = ComplicationDataSourceInfo(
+ "applicationName2",
+ "complicationName2",
+ icon,
+ ComplicationType.RANGED_VALUE,
+ componentName = null
+ )
+
+ // Test two identical ComplicationDataSourceInfo with different references.
+ assertThat(a).isEqualTo(b)
+
+ // Test two ComplicationDataSourceInfos with different contents.
+ assertThat(a).isNotEqualTo(c)
+ }
}
diff --git a/wear/watchface/watchface/api/current.txt b/wear/watchface/watchface/api/current.txt
index 329aaeb..7376e37 100644
--- a/wear/watchface/watchface/api/current.txt
+++ b/wear/watchface/watchface/api/current.txt
@@ -219,7 +219,7 @@
method @Deprecated public void onDump(java.io.PrintWriter writer);
method @Deprecated @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, java.time.ZonedDateTime zonedDateTime);
method @Deprecated @UiThread public abstract void renderHighlightLayer(android.graphics.Canvas canvas, android.graphics.Rect bounds, java.time.ZonedDateTime zonedDateTime);
- property public final boolean clearWithBackgroundTintBeforeRenderingHighlightLayer;
+ property @Deprecated public final boolean clearWithBackgroundTintBeforeRenderingHighlightLayer;
}
public abstract static class Renderer.CanvasRenderer2<SharedAssetsT extends androidx.wear.watchface.Renderer.SharedAssets> extends androidx.wear.watchface.Renderer.CanvasRenderer {
@@ -249,10 +249,10 @@
method @Deprecated public final suspend Object? runUiThreadGlCommands(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> commands, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public final void setEglConfig(android.opengl.EGLConfig);
method @Deprecated public final void setEglDisplay(android.opengl.EGLDisplay);
- property public final android.opengl.EGLContext eglBackgroundThreadContext;
- property public final android.opengl.EGLConfig eglConfig;
- property public final android.opengl.EGLDisplay eglDisplay;
- property public final android.opengl.EGLContext eglUiThreadContext;
+ property @Deprecated public final android.opengl.EGLContext eglBackgroundThreadContext;
+ property @Deprecated public final android.opengl.EGLConfig eglConfig;
+ property @Deprecated public final android.opengl.EGLDisplay eglDisplay;
+ property @Deprecated public final android.opengl.EGLContext eglUiThreadContext;
}
@Deprecated public static final class Renderer.GlesRenderer.GlesException extends java.lang.Exception {
diff --git a/wear/watchface/watchface/api/public_plus_experimental_current.txt b/wear/watchface/watchface/api/public_plus_experimental_current.txt
index 70c32d5..220208d 100644
--- a/wear/watchface/watchface/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface/api/public_plus_experimental_current.txt
@@ -238,7 +238,7 @@
method @Deprecated public void onDump(java.io.PrintWriter writer);
method @Deprecated @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, java.time.ZonedDateTime zonedDateTime);
method @Deprecated @UiThread public abstract void renderHighlightLayer(android.graphics.Canvas canvas, android.graphics.Rect bounds, java.time.ZonedDateTime zonedDateTime);
- property public final boolean clearWithBackgroundTintBeforeRenderingHighlightLayer;
+ property @Deprecated public final boolean clearWithBackgroundTintBeforeRenderingHighlightLayer;
}
public abstract static class Renderer.CanvasRenderer2<SharedAssetsT extends androidx.wear.watchface.Renderer.SharedAssets> extends androidx.wear.watchface.Renderer.CanvasRenderer {
@@ -268,10 +268,10 @@
method @Deprecated public final suspend Object? runUiThreadGlCommands(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> commands, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public final void setEglConfig(android.opengl.EGLConfig);
method @Deprecated public final void setEglDisplay(android.opengl.EGLDisplay);
- property public final android.opengl.EGLContext eglBackgroundThreadContext;
- property public final android.opengl.EGLConfig eglConfig;
- property public final android.opengl.EGLDisplay eglDisplay;
- property public final android.opengl.EGLContext eglUiThreadContext;
+ property @Deprecated public final android.opengl.EGLContext eglBackgroundThreadContext;
+ property @Deprecated public final android.opengl.EGLConfig eglConfig;
+ property @Deprecated public final android.opengl.EGLDisplay eglDisplay;
+ property @Deprecated public final android.opengl.EGLContext eglUiThreadContext;
}
@Deprecated public static final class Renderer.GlesRenderer.GlesException extends java.lang.Exception {
diff --git a/wear/watchface/watchface/api/restricted_current.txt b/wear/watchface/watchface/api/restricted_current.txt
index 329aaeb..7376e37 100644
--- a/wear/watchface/watchface/api/restricted_current.txt
+++ b/wear/watchface/watchface/api/restricted_current.txt
@@ -219,7 +219,7 @@
method @Deprecated public void onDump(java.io.PrintWriter writer);
method @Deprecated @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, java.time.ZonedDateTime zonedDateTime);
method @Deprecated @UiThread public abstract void renderHighlightLayer(android.graphics.Canvas canvas, android.graphics.Rect bounds, java.time.ZonedDateTime zonedDateTime);
- property public final boolean clearWithBackgroundTintBeforeRenderingHighlightLayer;
+ property @Deprecated public final boolean clearWithBackgroundTintBeforeRenderingHighlightLayer;
}
public abstract static class Renderer.CanvasRenderer2<SharedAssetsT extends androidx.wear.watchface.Renderer.SharedAssets> extends androidx.wear.watchface.Renderer.CanvasRenderer {
@@ -249,10 +249,10 @@
method @Deprecated public final suspend Object? runUiThreadGlCommands(kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> commands, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @Deprecated public final void setEglConfig(android.opengl.EGLConfig);
method @Deprecated public final void setEglDisplay(android.opengl.EGLDisplay);
- property public final android.opengl.EGLContext eglBackgroundThreadContext;
- property public final android.opengl.EGLConfig eglConfig;
- property public final android.opengl.EGLDisplay eglDisplay;
- property public final android.opengl.EGLContext eglUiThreadContext;
+ property @Deprecated public final android.opengl.EGLContext eglBackgroundThreadContext;
+ property @Deprecated public final android.opengl.EGLConfig eglConfig;
+ property @Deprecated public final android.opengl.EGLDisplay eglDisplay;
+ property @Deprecated public final android.opengl.EGLContext eglUiThreadContext;
}
@Deprecated public static final class Renderer.GlesRenderer.GlesException extends java.lang.Exception {
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index 68265ed..fb730b0 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -29,6 +29,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
import androidx.test.screenshot.assertAgainstGolden
import androidx.wear.watchface.CanvasType
@@ -121,6 +122,7 @@
/* digitalPreviewReferenceTimeMillis = */ 0
)
+@SdkSuppress(maxSdkVersion = 32) // b/271922712
@RunWith(AndroidJUnit4::class)
@RequiresApi(Build.VERSION_CODES.O_MR1)
@MediumTest
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
index 61e0e41..066ed00 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
@@ -20,7 +20,6 @@
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
-import android.provider.Settings
import androidx.annotation.RestrictTo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -103,30 +102,22 @@
as KeyguardManager
(watchState.isLocked as MutableStateFlow).value = keyguardManager.isDeviceLocked
+ }
- // Before SysUI has connected, we use ActionScreenOn/ActionScreenOff as a trigger to query
- // AMBIENT_ENABLED_PATH in order to determine if the device os ambient or not.
+ override fun onActionUserPresent() {
+ (watchState.isLocked as MutableStateFlow).value = false
+ }
+
+ override fun onActionAmbientStarted() {
if (sysUiHasSentWatchUiState) {
return
}
val isAmbient = watchState.isAmbient as MutableStateFlow
-
- // This is a backup signal for when SysUI is unable to deliver the ambient state (e.g. in
- // direct boot mode). We need to distinguish between ACTION_SCREEN_OFF for entering ambient
- // and the screen turning off. This is only possible from R.
- isAmbient.value =
- if (ambientSettingAvailable) {
- Settings.Global.getInt(contentResolver, AMBIENT_ENABLED_PATH, 0) == 1
- } else {
- // On P and below we just have to assume we're not ambient.
- false
- }
+ isAmbient.value = true
}
- override fun onActionScreenOn() {
- // Before SysUI has connected, we use ActionScreenOn/ActionScreenOff as a trigger to query
- // AMBIENT_ENABLED_PATH in order to determine if the device os ambient or not.
+ override fun onActionAmbientStopped() {
if (sysUiHasSentWatchUiState) {
return
}
@@ -134,8 +125,4 @@
val isAmbient = watchState.isAmbient as MutableStateFlow
isAmbient.value = false
}
-
- override fun onActionUserPresent() {
- (watchState.isLocked as MutableStateFlow).value = false
- }
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
index 4742681..644a68a 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
@@ -62,11 +62,14 @@
/** Called when we receive [Intent.ACTION_SCREEN_OFF] */
@UiThread public fun onActionScreenOff() {}
- /** Called when we receive [Intent.ACTION_SCREEN_ON] */
- @UiThread public fun onActionScreenOn() {}
-
/** Called when we receive [Intent.ACTION_USER_PRESENT] */
@UiThread public fun onActionUserPresent() {}
+
+ /** Called when we receive [ACTION_AMBIENT_STARTED] */
+ @UiThread public fun onActionAmbientStarted() {}
+
+ /** Called when we receive [ACTION_AMBIENT_STOPPED] */
+ @UiThread public fun onActionAmbientStopped() {}
}
companion object {
@@ -75,6 +78,12 @@
// available programmatically. The value below is the default but it could be overridden
// by OEMs.
internal const val INITIAL_LOW_BATTERY_THRESHOLD = 15f
+
+ internal const val ACTION_AMBIENT_STARTED =
+ "com.google.android.wearable.action.AMBIENT_STARTED"
+
+ internal const val ACTION_AMBIENT_STOPPED =
+ "com.google.android.wearable.action.AMBIENT_STOPPED"
}
internal val receiver: BroadcastReceiver =
@@ -90,9 +99,10 @@
Intent.ACTION_TIME_TICK -> observer.onActionTimeTick()
Intent.ACTION_TIMEZONE_CHANGED -> observer.onActionTimeZoneChanged()
Intent.ACTION_SCREEN_OFF -> observer.onActionScreenOff()
- Intent.ACTION_SCREEN_ON -> observer.onActionScreenOn()
Intent.ACTION_USER_PRESENT -> observer.onActionUserPresent()
WatchFaceImpl.MOCK_TIME_INTENT -> observer.onMockTime(intent)
+ ACTION_AMBIENT_STARTED -> observer.onActionAmbientStarted()
+ ACTION_AMBIENT_STOPPED -> observer.onActionAmbientStopped()
else -> System.err.println("<< IGNORING $intent")
}
}
@@ -112,6 +122,8 @@
addAction(Intent.ACTION_POWER_DISCONNECTED)
addAction(Intent.ACTION_USER_PRESENT)
addAction(WatchFaceImpl.MOCK_TIME_INTENT)
+ addAction(ACTION_AMBIENT_STARTED)
+ addAction(ACTION_AMBIENT_STOPPED)
}
)
}
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 59e942b..140549e 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -5658,9 +5658,7 @@
@Test
@Config(sdk = [Build.VERSION_CODES.R])
- public fun onActionScreenOff_onActionScreenOn_ambientEnabled() {
- Settings.Global.putInt(context.contentResolver, BroadcastsObserver.AMBIENT_ENABLED_PATH, 1)
-
+ public fun onActionAmbientStarted_onActionAmbientStopped_ambientEnabled() {
testWatchFaceService =
TestWatchFaceService(
WatchFaceType.DIGITAL,
@@ -5715,20 +5713,21 @@
watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
- watchFaceImpl.broadcastsObserver.onActionScreenOff()
+ watchFaceImpl.broadcastsObserver.onActionAmbientStarted()
assertThat(watchState.isAmbient.value).isTrue()
- watchFaceImpl.broadcastsObserver.onActionScreenOn()
+ watchFaceImpl.broadcastsObserver.onActionAmbientStopped()
assertThat(watchState.isAmbient.value).isFalse()
- // After SysUI has sent WatchUiState onActionScreenOff/onActionScreenOn should be ignored.
+ // After SysUI has sent WatchUiState onActionAmbientStarted/onActionAmbientStopped should be
+ // ignored.
engineWrapper.setWatchUiState(WatchUiState(false, 0), fromSysUi = true)
- watchFaceImpl.broadcastsObserver.onActionScreenOff()
+ watchFaceImpl.broadcastsObserver.onActionAmbientStarted()
assertThat(watchState.isAmbient.value).isFalse()
engineWrapper.setWatchUiState(WatchUiState(true, 0), fromSysUi = true)
- watchFaceImpl.broadcastsObserver.onActionScreenOn()
+ watchFaceImpl.broadcastsObserver.onActionAmbientStopped()
assertThat(watchState.isAmbient.value).isTrue()
}
diff --git a/window/extensions/core/core/proguard-rules.pro b/window/extensions/core/core/proguard-rules.pro
index 989f11d..9f547e9 100644
--- a/window/extensions/core/core/proguard-rules.pro
+++ b/window/extensions/core/core/proguard-rules.pro
@@ -11,12 +11,14 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
--keepclassmembers class ** implements androidx.window.extensions.core.util.function.Consumer {
+
+# These interfaces must be kept for the client library to invoke methods in extensions.
+-keep interface androidx.window.extensions.core.util.function.Consumer {
public void accept(***);
}
--keepclassmembers class ** implements androidx.window.extensions.core.util.function.Predicate {
+-keep interface androidx.window.extensions.core.util.function.Predicate {
public boolean test(***);
}
--keepclassmembers class ** implements androidx.window.extensions.core.util.function.Function {
+-keep interface androidx.window.extensions.core.util.function.Function {
public *** apply(***);
}
\ No newline at end of file
diff --git a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
index 3dd4813..7e14f39 100644
--- a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
+++ b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
@@ -25,6 +25,7 @@
import androidx.test.filters.MediumTest
import androidx.work.Configuration
import androidx.work.OneTimeWorkRequest
+import androidx.work.impl.TestWorkManagerImpl
import androidx.work.impl.model.WorkGenerationalId
import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.utils.SerialExecutorImpl
@@ -92,7 +93,7 @@
.setExecutor(mExecutor)
.build()
- mWorkManager = WorkManagerImpl(mContext, configuration, workTaskExecutor, true)
+ mWorkManager = TestWorkManagerImpl(mContext, configuration, workTaskExecutor)
WorkManagerImpl.setDelegate(mWorkManager)
mWorkTimer = spy(WorkTimer(configuration.runnableScheduler))
mDispatcher = WorkManagerGcmDispatcher(mWorkManager, mWorkTimer)
diff --git a/work/work-runtime/api/current.ignore b/work/work-runtime/api/current.ignore
index ecd7f87..7b196e5 100644
--- a/work/work-runtime/api/current.ignore
+++ b/work/work-runtime/api/current.ignore
@@ -1,4 +1,14 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfoByIdFlow(java.util.UUID):
+ Added method androidx.work.WorkManager.getWorkInfoByIdFlow(java.util.UUID)
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfosByTagFlow(String):
+ Added method androidx.work.WorkManager.getWorkInfosByTagFlow(String)
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfosFlow(androidx.work.WorkQuery):
+ Added method androidx.work.WorkManager.getWorkInfosFlow(androidx.work.WorkQuery)
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfosForUniqueWorkFlow(String):
+ Added method androidx.work.WorkManager.getWorkInfosForUniqueWorkFlow(String)
+
+
ChangedType: androidx.work.Configuration#getInitializationExceptionHandler():
Method androidx.work.Configuration.getInitializationExceptionHandler has changed return type from androidx.core.util.Consumer<java.lang.Throwable!> to androidx.core.util.Consumer<java.lang.Throwable>
ChangedType: androidx.work.Configuration#getSchedulingExceptionHandler():
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 4947c9e..3e5a864 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -66,19 +66,21 @@
}
public final class Constraints {
- ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+ ctor @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(23) @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(24) public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow, optional long contentTriggerUpdateDelayMillis, optional long contentTriggerMaxDelayMillis, optional java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
ctor public Constraints(androidx.work.Constraints other);
- method public long getContentTriggerMaxDelayMillis();
- method public long getContentTriggerUpdateDelayMillis();
- method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+ method @RequiresApi(24) public long getContentTriggerMaxDelayMillis();
+ method @RequiresApi(24) public long getContentTriggerUpdateDelayMillis();
+ method @RequiresApi(24) public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
method public androidx.work.NetworkType getRequiredNetworkType();
method public boolean requiresBatteryNotLow();
method public boolean requiresCharging();
method @RequiresApi(23) public boolean requiresDeviceIdle();
method public boolean requiresStorageNotLow();
- property public final long contentTriggerMaxDelayMillis;
- property public final long contentTriggerUpdateDelayMillis;
- property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+ property @RequiresApi(24) public final long contentTriggerMaxDelayMillis;
+ property @RequiresApi(24) public final long contentTriggerUpdateDelayMillis;
+ property @RequiresApi(24) public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
property public final androidx.work.NetworkType requiredNetworkType;
field public static final androidx.work.Constraints.Companion Companion;
field public static final androidx.work.Constraints NONE;
@@ -392,11 +394,15 @@
method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+ method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
method public static void initialize(android.content.Context, androidx.work.Configuration);
diff --git a/work/work-runtime/api/public_plus_experimental_current.txt b/work/work-runtime/api/public_plus_experimental_current.txt
index 4947c9e..3e5a864 100644
--- a/work/work-runtime/api/public_plus_experimental_current.txt
+++ b/work/work-runtime/api/public_plus_experimental_current.txt
@@ -66,19 +66,21 @@
}
public final class Constraints {
- ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+ ctor @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(23) @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(24) public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow, optional long contentTriggerUpdateDelayMillis, optional long contentTriggerMaxDelayMillis, optional java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
ctor public Constraints(androidx.work.Constraints other);
- method public long getContentTriggerMaxDelayMillis();
- method public long getContentTriggerUpdateDelayMillis();
- method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+ method @RequiresApi(24) public long getContentTriggerMaxDelayMillis();
+ method @RequiresApi(24) public long getContentTriggerUpdateDelayMillis();
+ method @RequiresApi(24) public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
method public androidx.work.NetworkType getRequiredNetworkType();
method public boolean requiresBatteryNotLow();
method public boolean requiresCharging();
method @RequiresApi(23) public boolean requiresDeviceIdle();
method public boolean requiresStorageNotLow();
- property public final long contentTriggerMaxDelayMillis;
- property public final long contentTriggerUpdateDelayMillis;
- property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+ property @RequiresApi(24) public final long contentTriggerMaxDelayMillis;
+ property @RequiresApi(24) public final long contentTriggerUpdateDelayMillis;
+ property @RequiresApi(24) public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
property public final androidx.work.NetworkType requiredNetworkType;
field public static final androidx.work.Constraints.Companion Companion;
field public static final androidx.work.Constraints NONE;
@@ -392,11 +394,15 @@
method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+ method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
method public static void initialize(android.content.Context, androidx.work.Configuration);
diff --git a/work/work-runtime/api/restricted_current.ignore b/work/work-runtime/api/restricted_current.ignore
index ecd7f87..7b196e5 100644
--- a/work/work-runtime/api/restricted_current.ignore
+++ b/work/work-runtime/api/restricted_current.ignore
@@ -1,4 +1,14 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfoByIdFlow(java.util.UUID):
+ Added method androidx.work.WorkManager.getWorkInfoByIdFlow(java.util.UUID)
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfosByTagFlow(String):
+ Added method androidx.work.WorkManager.getWorkInfosByTagFlow(String)
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfosFlow(androidx.work.WorkQuery):
+ Added method androidx.work.WorkManager.getWorkInfosFlow(androidx.work.WorkQuery)
+AddedAbstractMethod: androidx.work.WorkManager#getWorkInfosForUniqueWorkFlow(String):
+ Added method androidx.work.WorkManager.getWorkInfosForUniqueWorkFlow(String)
+
+
ChangedType: androidx.work.Configuration#getInitializationExceptionHandler():
Method androidx.work.Configuration.getInitializationExceptionHandler has changed return type from androidx.core.util.Consumer<java.lang.Throwable!> to androidx.core.util.Consumer<java.lang.Throwable>
ChangedType: androidx.work.Configuration#getSchedulingExceptionHandler():
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 4947c9e..3e5a864 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -66,19 +66,21 @@
}
public final class Constraints {
- ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+ ctor @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(23) @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(24) public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow, optional long contentTriggerUpdateDelayMillis, optional long contentTriggerMaxDelayMillis, optional java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
ctor public Constraints(androidx.work.Constraints other);
- method public long getContentTriggerMaxDelayMillis();
- method public long getContentTriggerUpdateDelayMillis();
- method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+ method @RequiresApi(24) public long getContentTriggerMaxDelayMillis();
+ method @RequiresApi(24) public long getContentTriggerUpdateDelayMillis();
+ method @RequiresApi(24) public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
method public androidx.work.NetworkType getRequiredNetworkType();
method public boolean requiresBatteryNotLow();
method public boolean requiresCharging();
method @RequiresApi(23) public boolean requiresDeviceIdle();
method public boolean requiresStorageNotLow();
- property public final long contentTriggerMaxDelayMillis;
- property public final long contentTriggerUpdateDelayMillis;
- property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+ property @RequiresApi(24) public final long contentTriggerMaxDelayMillis;
+ property @RequiresApi(24) public final long contentTriggerUpdateDelayMillis;
+ property @RequiresApi(24) public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
property public final androidx.work.NetworkType requiredNetworkType;
field public static final androidx.work.Constraints.Companion Companion;
field public static final androidx.work.Constraints NONE;
@@ -392,11 +394,15 @@
method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+ method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
method public static void initialize(android.content.Context, androidx.work.Configuration);
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index 4912a2d..ac57f69 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -58,7 +58,7 @@
dependencies {
implementation("androidx.core:core:1.9.0")
ksp("androidx.room:room-compiler:2.5.0")
- implementation("androidx.room:room-runtime:2.5.0")
+ implementation("androidx.room:room-ktx:2.5.0")
androidTestImplementation("androidx.room:room-testing:2.5.0")
implementation("androidx.sqlite:sqlite-framework:2.3.0")
api("androidx.annotation:annotation-experimental:1.0.0")
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/ContentUriTriggerWorkersTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/ContentUriTriggerWorkersTest.kt
index 3b3aeeb..c9bf3cb 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/ContentUriTriggerWorkersTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/ContentUriTriggerWorkersTest.kt
@@ -24,11 +24,11 @@
import androidx.test.filters.SdkSuppress
import androidx.work.Configuration.Companion.MIN_SCHEDULER_LIMIT
import androidx.work.Constraints.ContentUriTrigger
-import androidx.work.impl.Processor
import androidx.work.impl.Scheduler
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.schedulers
import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
import androidx.work.worker.TestWorker
import com.google.common.truth.Truth.assertThat
@@ -49,11 +49,14 @@
.build()
val executor = Executors.newSingleThreadExecutor()
val taskExecutor = WorkManagerTaskExecutor(executor)
- val db = WorkDatabase.create(context, executor, true)
internal val testScheduler = TestScheduler()
- val processor = Processor(context, configuration, taskExecutor, db)
- val workManager = WorkManagerImpl(context,
- configuration, taskExecutor, db, listOf<Scheduler>(testScheduler), processor)
+ val workManager = WorkManagerImpl(
+ context = context,
+ configuration = configuration,
+ workTaskExecutor = taskExecutor,
+ workDatabase = WorkDatabase.create(context, executor, true),
+ schedulersCreator = schedulers(testScheduler)
+ )
@Test
fun maxSchedulerLimitNotApplicable() {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
index 72fc7af..21a8de2 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
@@ -18,6 +18,8 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
import androidx.work.impl.Processor
import androidx.work.impl.Scheduler
import androidx.work.impl.StartStopTokens
@@ -28,6 +30,7 @@
import androidx.work.impl.constraints.trackers.Trackers
import androidx.work.impl.model.WorkSpec
import androidx.work.impl.testutils.TrackingWorkerFactory
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
import androidx.work.worker.FailureWorker
import androidx.work.worker.LatchWorker
@@ -37,7 +40,10 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Test
+import org.junit.runner.RunWith
+@MediumTest
+@RunWith(AndroidJUnit4::class)
class SchedulersTest {
val context = ApplicationProvider.getApplicationContext<Context>().applicationContext
val factory = TrackingWorkerFactory()
@@ -60,8 +66,20 @@
override fun hasLimitedSchedulingSlots() = false
}
- val wm = WorkManagerImpl(context, configuration, taskExecutor, db,
- listOf(trackingScheduler, greedyScheduler), processor, trackers)
+ val wm = WorkManagerImpl(
+ context, configuration, taskExecutor, db,
+ ) { context: Context,
+ configuration: Configuration,
+ taskExecutor: TaskExecutor,
+ _: WorkDatabase,
+ trackers: Trackers,
+ processor: Processor ->
+ listOf(
+ GreedyScheduler(context, configuration, trackers, processor,
+ WorkLauncherImpl(processor, taskExecutor)),
+ trackingScheduler
+ )
+ }
val workRequest = OneTimeWorkRequest.from(TestWorker::class.java)
val dependency = OneTimeWorkRequest.from(TestWorker::class.java)
@@ -87,8 +105,10 @@
override fun hasLimitedSchedulingSlots() = false
}
- val wm = WorkManagerImpl(context, configuration, taskExecutor, db,
- listOf(trackingScheduler, greedyScheduler), processor, trackers)
+ val wm = WorkManagerImpl(
+ context, configuration, taskExecutor, db,
+ listOf(trackingScheduler, greedyScheduler), processor, trackers
+ )
val workRequest = OneTimeWorkRequest.from(FailureWorker::class.java)
wm.enqueue(workRequest)
@@ -105,7 +125,9 @@
@Test
fun interruptionReschedules() {
val schedulers = mutableListOf<Scheduler>()
- val wm = WorkManagerImpl(context, configuration, taskExecutor, db, schedulers, processor)
+ val wm = WorkManagerImpl(
+ context, configuration, taskExecutor, db, schedulers, processor, trackers
+ )
val scheduledSpecs = mutableListOf<WorkSpec>()
val cancelledIds = mutableListOf<String>()
val scheduler = object : Scheduler {
@@ -154,7 +176,10 @@
@Test
fun periodicReschedules() {
val schedulers = mutableListOf<Scheduler>()
- val wm = WorkManagerImpl(context, configuration, taskExecutor, db, schedulers, processor)
+ val wm = WorkManagerImpl(
+ context, configuration, taskExecutor, db,
+ schedulers, processor, trackers
+ )
val scheduledSpecs = mutableListOf<WorkSpec>()
val cancelledIds = mutableListOf<String>()
val scheduler = object : Scheduler {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt
new file mode 100644
index 0000000..10b44d3
--- /dev/null
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkInfoFlowsTest.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.work
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import androidx.work.ExistingWorkPolicy.APPEND
+import androidx.work.ExistingWorkPolicy.KEEP
+import androidx.work.impl.Processor
+import androidx.work.impl.Scheduler
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.WorkLauncherImpl
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.background.greedy.GreedyScheduler
+import androidx.work.impl.constraints.trackers.Trackers
+import androidx.work.impl.testutils.TestConstraintTracker
+import androidx.work.impl.testutils.TrackingWorkerFactory
+import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
+import androidx.work.worker.LatchWorker
+import androidx.work.worker.TestWorker
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+
+@SmallTest
+class WorkInfoFlowsTest {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val workerFactory = TrackingWorkerFactory()
+ val configuration = Configuration.Builder().setWorkerFactory(workerFactory).build()
+ val executor = Executors.newSingleThreadExecutor()
+ val taskExecutor = WorkManagerTaskExecutor(executor)
+ val fakeChargingTracker = TestConstraintTracker(false, context, taskExecutor)
+ val trackers = Trackers(
+ context = context,
+ taskExecutor = taskExecutor,
+ batteryChargingTracker = fakeChargingTracker
+ )
+ val db = WorkDatabase.create(context, executor, true)
+
+ // ugly, ugly hack because of circular dependency:
+ // Schedulers need WorkManager, WorkManager needs schedulers
+ val schedulers = mutableListOf<Scheduler>()
+ val processor = Processor(context, configuration, taskExecutor, db)
+ val workManager = WorkManagerImpl(
+ context, configuration, taskExecutor, db, schedulers, processor, trackers
+ )
+ val greedyScheduler = GreedyScheduler(context, configuration, trackers,
+ processor, WorkLauncherImpl(processor, taskExecutor))
+
+ init {
+ schedulers.add(greedyScheduler)
+ WorkManagerImpl.setDelegate(workManager)
+ }
+
+ val unrelatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setInitialDelay(1, TimeUnit.DAYS)
+ .build()
+
+ @Test
+ fun flowById() = runBlocking {
+ val request = OneTimeWorkRequest.Builder(LatchWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .build()
+ val tester = launchTester(workManager.getWorkInfoByIdFlow(request.id))
+ assertThat(tester.awaitNext()).isNull()
+ workManager.enqueue(unrelatedRequest)
+ workManager.enqueue(request)
+ assertThat(tester.awaitNext().state).isEqualTo(WorkInfo.State.ENQUEUED)
+ fakeChargingTracker.state = true
+
+ assertThat(tester.awaitNext().state).isEqualTo(WorkInfo.State.RUNNING)
+ val worker = workerFactory.awaitWorker(request.id) as LatchWorker
+ worker.mLatch.countDown()
+ assertThat(tester.awaitNext().state).isEqualTo(WorkInfo.State.SUCCEEDED)
+ }
+
+ @Test
+ fun flowByName() = runBlocking<Unit> {
+ val request1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .build()
+ val request2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .build()
+ val tester = launchTester(workManager.getWorkInfosForUniqueWorkFlow("name"))
+ assertThat(tester.awaitNext()).isEmpty()
+ workManager.enqueue(unrelatedRequest)
+ workManager.enqueueUniqueWork("name", KEEP, request1)
+ val firstList = tester.awaitNext()
+ assertThat(firstList.size).isEqualTo(1)
+ assertThat(firstList.first().id).isEqualTo(request1.id)
+ workManager.enqueueUniqueWork("name", APPEND, request2)
+ val secondList = tester.awaitNext()
+ assertThat(secondList.size).isEqualTo(2)
+ assertThat(secondList.map { it.id }).containsExactly(request1.id, request2.id)
+ }
+
+ @Test
+ fun flowByQuery() = runBlocking<Unit> {
+ val request1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .build()
+ val request2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .build()
+ val query = WorkQuery.fromIds(request1.id, request2.id)
+ val tester = launchTester(workManager.getWorkInfosFlow(query))
+ assertThat(tester.awaitNext()).isEmpty()
+ workManager.enqueue(unrelatedRequest)
+ workManager.enqueue(request1)
+ val firstList = tester.awaitNext()
+ assertThat(firstList.size).isEqualTo(1)
+ assertThat(firstList.first().id).isEqualTo(request1.id)
+ workManager.enqueue(request2)
+ val secondList = tester.awaitNext()
+ assertThat(secondList.size).isEqualTo(2)
+ assertThat(secondList.map { it.id }).containsExactly(request1.id, request2.id)
+ }
+
+ @Test
+ fun flowByTag() = runBlocking<Unit> {
+ val request1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .addTag("tag")
+ .build()
+ val request2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ .setConstraints(Constraints(requiresCharging = true))
+ .addTag("tag")
+ .build()
+ val tester = launchTester(workManager.getWorkInfosByTagFlow("tag"))
+
+ assertThat(tester.awaitNext()).isEmpty()
+ workManager.enqueue(unrelatedRequest)
+ workManager.enqueue(request1)
+ val firstList = tester.awaitNext()
+ assertThat(firstList.size).isEqualTo(1)
+ assertThat(firstList.first().id).isEqualTo(request1.id)
+ workManager.enqueue(unrelatedRequest)
+ workManager.enqueue(request2)
+ val secondList = tester.awaitNext()
+ assertThat(secondList.size).isEqualTo(2)
+ assertThat(secondList.map { it.id }).containsExactly(request1.id, request2.id)
+ }
+}
+
+private fun <T> CoroutineScope.launchTester(flow: Flow<T>): FlowTester<T> {
+ val tester = FlowTester(flow)
+ // we don't block parent from completing and simply stop collecting once parent is done
+ val forked = Job()
+ coroutineContext.job.invokeOnCompletion { forked.cancel() }
+ launch(Job()) { tester.launch(this) }
+ return tester
+}
+
+private class FlowTester<T>(private val flow: Flow<T>) {
+ private val channel = Channel<T>(10)
+
+ suspend fun awaitNext(): T {
+ val result = try {
+ withTimeout(3000L) { channel.receive() }
+ } catch (e: TimeoutCancellationException) {
+ throw AssertionError("Didn't receive event")
+ }
+ val next = channel.tryReceive()
+ if (next.isSuccess || next.isClosed)
+ throw AssertionError(
+ "Two events received instead of one;\n" +
+ "first: $result;\nsecond: ${next.getOrNull()}"
+ )
+ return result
+ }
+
+ fun launch(scope: CoroutineScope) {
+ flow.onEach { channel.send(it) }.launchIn(scope)
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
index aef0621..1819c89 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTest.java
@@ -16,6 +16,8 @@
package androidx.work.impl;
+import static androidx.work.impl.WorkManagerImplExtKt.createWorkManager;
+
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -104,7 +106,7 @@
.build();
mWorkManagerImpl =
- spy(new WorkManagerImpl(context, mConfiguration, new InstantWorkTaskExecutor()));
+ spy(createWorkManager(context, mConfiguration, new InstantWorkTaskExecutor()));
when(mWorkManagerImpl.getSchedulers()).thenReturn(Collections.singletonList(mScheduler));
WorkManagerImpl.setDelegate(mWorkManagerImpl);
mDatabase = mWorkManagerImpl.getWorkDatabase();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
index da88dcf..60777ec 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
@@ -16,6 +16,7 @@
package androidx.work.impl;
+import static androidx.work.impl.WorkManagerImplExtKt.createTestWorkManager;
import static androidx.work.worker.RandomSleepTestWorker.MAX_SLEEP_DURATION_MS;
import static org.hamcrest.CoreMatchers.is;
@@ -111,8 +112,7 @@
.setMaxSchedulerLimit(TEST_SCHEDULER_LIMIT)
.build();
TaskExecutor taskExecutor = new InstantWorkTaskExecutor();
- mWorkManagerImplSpy = spy(
- new WorkManagerImpl(context, configuration, taskExecutor, true));
+ mWorkManagerImplSpy = spy(createTestWorkManager(context, configuration, taskExecutor));
TrackingScheduler trackingScheduler =
new TrackingScheduler(context, configuration, mWorkManagerImplSpy);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 700390a..d64b7d8 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -29,6 +29,8 @@
import static androidx.work.WorkInfo.State.FAILED;
import static androidx.work.WorkInfo.State.RUNNING;
import static androidx.work.WorkInfo.State.SUCCEEDED;
+import static androidx.work.impl.WorkManagerImplExtKt.createWorkManager;
+import static androidx.work.impl.WorkManagerImplExtKt.schedulers;
import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
import static androidx.work.impl.workers.ConstraintTrackingWorkerKt.ARGUMENT_CLASS_NAME;
@@ -96,6 +98,7 @@
import androidx.work.WorkRequest;
import androidx.work.impl.background.greedy.GreedyScheduler;
import androidx.work.impl.background.systemalarm.RescheduleReceiver;
+import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.model.Dependency;
import androidx.work.impl.model.DependencyDao;
import androidx.work.impl.model.WorkName;
@@ -168,8 +171,7 @@
.setMinimumLoggingLevel(Log.DEBUG)
.build();
InstantWorkTaskExecutor workTaskExecutor = new InstantWorkTaskExecutor();
- mWorkManagerImpl =
- spy(new WorkManagerImpl(mContext, mConfiguration, workTaskExecutor));
+ mWorkManagerImpl = spy(createWorkManager(mContext, mConfiguration, workTaskExecutor));
WorkLauncher workLauncher = new WorkLauncherImpl(mWorkManagerImpl.getProcessor(),
workTaskExecutor);
mScheduler =
@@ -1802,14 +1804,15 @@
Processor processor = new Processor(mContext, mConfiguration, workTaskExecutor, mDatabase);
WorkLauncherImpl launcher = new WorkLauncherImpl(processor, workTaskExecutor);
+ Trackers trackers = mWorkManagerImpl.getTrackers();
Scheduler scheduler =
new GreedyScheduler(
mContext,
mWorkManagerImpl.getConfiguration(),
- mWorkManagerImpl.getTrackers(),
+ trackers,
processor, launcher);
- mWorkManagerImpl = new WorkManagerImpl(mContext, mConfiguration, workTaskExecutor,
- mDatabase, Collections.singletonList(scheduler), processor);
+ mWorkManagerImpl = createWorkManager(mContext, mConfiguration, workTaskExecutor,
+ mDatabase, trackers, processor, schedulers(scheduler));
WorkManagerImpl.setDelegate(mWorkManagerImpl);
mDatabase = mWorkManagerImpl.getWorkDatabase();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
index fc84c9b..ee46004 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
@@ -42,7 +42,7 @@
@SdkSuppress(minSdkVersion = 24)
fun directBootTest() {
val context = DeviceProtectedStoreContext(true)
- WorkManagerImpl(context, configuration, taskExecutor, true)
+ TestWorkManagerImpl(context, configuration, taskExecutor)
}
@Test
@@ -50,7 +50,7 @@
@SdkSuppress(minSdkVersion = 24)
fun credentialBackedStorageTest() {
val context = DeviceProtectedStoreContext(false)
- val workManager = WorkManagerImpl(context, configuration, taskExecutor, true)
+ val workManager = TestWorkManagerImpl(context, configuration, taskExecutor)
assertNotNull(workManager)
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
index 78e9c98..393685e 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
@@ -85,6 +85,7 @@
public void testGreedyScheduler_startsUnconstrainedWork() {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
WorkSpec workSpec = work.getWorkSpec();
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
mGreedyScheduler.schedule(workSpec);
ArgumentCaptor<StartStopToken> captor = ArgumentCaptor.forClass(StartStopToken.class);
verify(mWorkLauncher).startWork(captor.capture());
@@ -110,6 +111,7 @@
@SmallTest
public void testGreedyScheduler_startsDelayedWork() {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class)
+ .setLastEnqueueTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
.build();
mGreedyScheduler.schedule(work.getWorkSpec());
@@ -170,6 +172,7 @@
.setConstraints(new Constraints.Builder().setRequiresCharging(true).build())
.build();
final WorkSpec workSpec = work.getWorkSpec();
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
Set<WorkSpec> expected = new HashSet<WorkSpec>();
expected.add(workSpec);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index a8f2985..2b97041 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -123,6 +123,7 @@
public void testConvert_initialDelay() {
final long expectedInitialDelay = 12123L;
WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
workSpec.initialDelay = expectedInitialDelay;
JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
assertCloseValues(jobInfo.getMinLatencyMillis(), expectedInitialDelay);
@@ -229,6 +230,7 @@
@SdkSuppress(minSdkVersion = 29)
public void testConvert_setImportantWhileForeground() {
WorkSpec workSpec = getTestWorkSpecWithConstraints(new Constraints.Builder().build());
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
assertThat(jobInfo.isImportantWhileForeground(), is(true));
}
@@ -252,6 +254,7 @@
}
WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
workSpec.expedited = true;
JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
assertThat(jobInfo.isExpedited(), is(true));
@@ -280,6 +283,7 @@
WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
workSpec.expedited = true;
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
workSpec.initialDelay = 1000L; // delay
JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
assertThat(jobInfo.isExpedited(), is(false));
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
index 7594a1e..8356427 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobServiceTest.java
@@ -17,6 +17,8 @@
package androidx.work.impl.background.systemjob;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
+import static androidx.work.impl.WorkManagerImplExtKt.createWorkManager;
+import static androidx.work.impl.WorkManagerImplExtKt.schedulers;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -54,6 +56,7 @@
import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.model.WorkSpecDao;
import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor;
import androidx.work.worker.InfiniteTestWorker;
@@ -63,7 +66,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
@@ -108,15 +110,14 @@
.setExecutor(Executors.newSingleThreadExecutor())
.build();
mScheduler = mock(Scheduler.class);
- List<Scheduler> schedulers = Collections.singletonList(mScheduler);
mProcessor = new Processor(
context,
configuration,
taskExecutor,
mDatabase);
- mWorkManagerImpl = new WorkManagerImpl(
- context, configuration, taskExecutor, mDatabase, schedulers, mProcessor);
+ mWorkManagerImpl = createWorkManager(context, configuration, taskExecutor,
+ mDatabase, new Trackers(context, taskExecutor), mProcessor, schedulers(mScheduler));
WorkManagerImpl.setDelegate(mWorkManagerImpl);
mSystemJobServiceSpy = spy(new SystemJobService());
doReturn(context).when(mSystemJobServiceSpy).getApplicationContext();
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
index 4ff0283..e13f7d1 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
@@ -45,6 +45,7 @@
import androidx.work.impl.foreground.SystemForegroundDispatcher.createStartForegroundIntent
import androidx.work.impl.foreground.SystemForegroundDispatcher.createStopForegroundIntent
import androidx.work.impl.model.WorkGenerationalId
+import androidx.work.impl.schedulers
import androidx.work.impl.utils.SynchronousExecutor
import androidx.work.impl.utils.futures.SettableFuture
import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
@@ -95,12 +96,12 @@
processor = spy(Processor(context, config, taskExecutor, workDatabase))
workManager = spy(
WorkManagerImpl(
- context,
- config,
- taskExecutor,
- workDatabase,
- listOf(scheduler),
- processor
+ context = context,
+ configuration = config,
+ workTaskExecutor = taskExecutor,
+ workDatabase = workDatabase,
+ processor = processor,
+ schedulersCreator = schedulers(scheduler),
)
)
workDatabase = workManager.workDatabase
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/WorkerWrapperForegroundTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/WorkerWrapperForegroundTest.kt
index 58b88ae..159a514 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/WorkerWrapperForegroundTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/WorkerWrapperForegroundTest.kt
@@ -27,16 +27,20 @@
import androidx.test.filters.LargeTest
import androidx.work.Configuration
import androidx.work.OneTimeWorkRequest
-import androidx.work.impl.Processor
import androidx.work.impl.Scheduler
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.WorkerWrapper
-import androidx.work.impl.utils.SerialExecutorImpl
+import androidx.work.impl.schedulers
import androidx.work.impl.utils.futures.SettableFuture
import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
import androidx.work.worker.StopAwareForegroundWorker
import androidx.work.worker.TestForegroundWorker
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
@@ -46,12 +50,6 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
-import java.util.Collections
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
@LargeTest
@@ -63,8 +61,6 @@
private lateinit var internalExecutor: ExecutorService
private lateinit var taskExecutor: TaskExecutor
private lateinit var workDatabase: WorkDatabase
- private lateinit var schedulers: List<Scheduler>
- private lateinit var processor: Processor
private lateinit var workManager: WorkManagerImpl
private lateinit var foregroundProcessor: ForegroundProcessor
@@ -84,26 +80,16 @@
.setMinimumLoggingLevel(Log.DEBUG)
.build()
- taskExecutor = object : TaskExecutor {
- val main = Executor { runnable ->
- handler.post(runnable)
- }
- val serialExecutor = SerialExecutorImpl(internalExecutor)
-
- override fun getMainThreadExecutor(): Executor {
- return main
- }
-
- override fun getSerialTaskExecutor() = serialExecutor
- }
+ taskExecutor = WorkManagerTaskExecutor(internalExecutor)
workDatabase = WorkDatabase.create(context, taskExecutor.serialTaskExecutor, true)
- val scheduler = mock(Scheduler::class.java)
- schedulers = Collections.singletonList(scheduler)
- processor = Processor(context, config, taskExecutor, workDatabase)
- workManager =
- spy(WorkManagerImpl(context, config, taskExecutor, workDatabase, schedulers, processor))
- workDatabase = workManager.workDatabase
+ workManager = WorkManagerImpl(
+ context = context,
+ configuration = config,
+ workTaskExecutor = taskExecutor,
+ workDatabase = workDatabase,
+ schedulersCreator = schedulers(mock(Scheduler::class.java))
+ )
WorkManagerImpl.setDelegate(workManager)
// Foreground processor
foregroundProcessor = mock(ForegroundProcessor::class.java)
diff --git a/work/work-runtime/src/main/java/androidx/work/Configuration.kt b/work/work-runtime/src/main/java/androidx/work/Configuration.kt
index c6d9b85..a9c2810 100644
--- a/work/work-runtime/src/main/java/androidx/work/Configuration.kt
+++ b/work/work-runtime/src/main/java/androidx/work/Configuration.kt
@@ -84,7 +84,6 @@
/**
* The minimum logging level, corresponding to the constants found in [android.util.Log]
- * @hide
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val minimumLoggingLevel: Int
@@ -122,7 +121,6 @@
/**
* The maximum number of system requests which can be enqueued by [WorkManager]
* when using [android.app.job.JobScheduler] or [android.app.AlarmManager]
- * @hide
*/
@get:IntRange(from = MIN_SCHEDULER_LIMIT.toLong(), to = Scheduler.MAX_SCHEDULER_LIMIT.toLong())
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -130,7 +128,6 @@
/**
* @return `true` If the default task [Executor] is being used
- * @hide
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val isUsingDefaultTaskExecutor: Boolean
@@ -188,7 +185,6 @@
* template.
*
* @param configuration An existing [Configuration] to use as a template
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
constructor(configuration: Configuration) {
diff --git a/work/work-runtime/src/main/java/androidx/work/Constraints.kt b/work/work-runtime/src/main/java/androidx/work/Constraints.kt
index 138d653..b59431c 100644
--- a/work/work-runtime/src/main/java/androidx/work/Constraints.kt
+++ b/work/work-runtime/src/main/java/androidx/work/Constraints.kt
@@ -15,11 +15,13 @@
*/
package androidx.work
+import android.annotation.SuppressLint
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.room.ColumnInfo
+import androidx.room.Ignore
import androidx.work.impl.utils.toMillisCompat
import java.time.Duration
import java.util.concurrent.TimeUnit
@@ -29,61 +31,177 @@
* default, WorkRequests do not have any requirements and can run immediately. By adding
* requirements, you can make sure that work only runs in certain situations - for example, when you
* have an unmetered network and are charging.
- *
- * @property requiredNetworkType The type of network required for the work to run.
- * The default value is [NetworkType.NOT_REQUIRED].
- * @param requiresCharging whether device should be charging for the [WorkRequest] to run. The
- * default value is `false`.
- * @param requiresDeviceIdle whether device should be idle for the [WorkRequest] to run. The
- * default value is `false`.
- * @param requiresBatteryNotLow whether device battery should be at an acceptable level for the
- * [WorkRequest] to run. The default value is `false`.
- * @param requiresStorageNotLow whether the device's available storage should be at an acceptable
- * level for the [WorkRequest] to run. The default value is `false`.
- * @property contentTriggerUpdateDelayMillis the delay in milliseconds that is allowed from the
- * time a `content:` [Uri] change is detected to the time when the [WorkRequest] is scheduled.
- * If there are more changes during this time, the delay will be reset to the start of the most
- * recent change. This functionality is identical to the one found in `JobScheduler` and
- * is described in [android.app.job.JobInfo.Builder.setTriggerContentUpdateDelay]
- * @property contentTriggerMaxDelayMillis the maximum delay in milliseconds that is allowed
- * from the first time a `content:` [Uri] change is detected to the time when the [WorkRequest]
- * is scheduled. This functionality is identical to the one found in `JobScheduler` and is described
- * in [android.app.job.JobInfo.Builder.setTriggerContentMaxDelay].
- * @property contentUriTriggers set of [ContentUriTrigger]. [WorkRequest] will run when a local
- * `content:` [Uri] of one of the triggers in the set is updated.
- * This functionality is identical to the one found in `JobScheduler` and is described in
- * [android.app.job.JobInfo.Builder.addTriggerContentUri].
*/
-class Constraints(
+class Constraints {
+ /**
+ * The type of network required for the work to run.
+ */
@ColumnInfo(name = "required_network_type")
- val requiredNetworkType: NetworkType = NetworkType.NOT_REQUIRED,
+ val requiredNetworkType: NetworkType
+
@ColumnInfo(name = "requires_charging")
- private val requiresCharging: Boolean = false,
+ private val requiresCharging: Boolean
+
@ColumnInfo(name = "requires_device_idle")
- private val requiresDeviceIdle: Boolean = false,
+ private val requiresDeviceIdle: Boolean
+
@ColumnInfo(name = "requires_battery_not_low")
- private val requiresBatteryNotLow: Boolean = false,
+ private val requiresBatteryNotLow: Boolean
+
@ColumnInfo(name = "requires_storage_not_low")
- private val requiresStorageNotLow: Boolean = false,
+ private val requiresStorageNotLow: Boolean
+
+ /**
+ * The delay in milliseconds that is allowed from the
+ * time a `content:` [Uri] change is detected to the time when the [WorkRequest] is scheduled.
+ * If there are more changes during this time, the delay will be reset to the start of the most
+ * recent change. This functionality is identical to the one found in `JobScheduler` and
+ * is described in [android.app.job.JobInfo.Builder.setTriggerContentUpdateDelay]
+ */
+ @get:RequiresApi(24)
@ColumnInfo(name = "trigger_content_update_delay")
- val contentTriggerUpdateDelayMillis: Long = -1,
+ val contentTriggerUpdateDelayMillis: Long
+
+ /**
+ * The maximum delay in milliseconds that is allowed
+ * from the first time a `content:` [Uri] change is detected to the time when the [WorkRequest]
+ * is scheduled. This functionality is identical to the one found in `JobScheduler` and is
+ * described in [android.app.job.JobInfo.Builder.setTriggerContentMaxDelay].
+ */
+ @get:RequiresApi(24)
@ColumnInfo(name = "trigger_max_content_delay")
- val contentTriggerMaxDelayMillis: Long = -1,
+ val contentTriggerMaxDelayMillis: Long
+
+ /**
+ * Set of [ContentUriTrigger]. [WorkRequest] will run when a local
+ * `content:` [Uri] of one of the triggers in the set is updated.
+ * This functionality is identical to the one found in `JobScheduler` and is described in
+ * [android.app.job.JobInfo.Builder.addTriggerContentUri].
+ */
@ColumnInfo(name = "content_uri_triggers")
- val contentUriTriggers: Set<ContentUriTrigger> = setOf(),
-) {
- constructor(other: Constraints) : this(
- requiresCharging = other.requiresCharging,
- requiresDeviceIdle = other.requiresDeviceIdle,
- requiredNetworkType = other.requiredNetworkType,
- requiresBatteryNotLow = other.requiresBatteryNotLow,
- requiresStorageNotLow = other.requiresStorageNotLow,
- contentUriTriggers = other.contentUriTriggers,
- contentTriggerUpdateDelayMillis = other.contentTriggerUpdateDelayMillis,
- contentTriggerMaxDelayMillis = other.contentTriggerMaxDelayMillis,
+ @get:RequiresApi(24)
+ val contentUriTriggers: Set<ContentUriTrigger>
+
+ /**
+ * Constructs [Constraints].
+ *
+ * @param requiredNetworkType The type of network required for the work to run.
+ * The default value is [NetworkType.NOT_REQUIRED].
+ * @param requiresCharging whether device should be charging for the [WorkRequest] to run. The
+ * default value is `false`.
+ * @param requiresBatteryNotLow whether device battery should be at an acceptable level for the
+ * [WorkRequest] to run. The default value is `false`.
+ * @param requiresStorageNotLow whether the device's available storage should be at an
+ * acceptable level for the [WorkRequest] to run. The default value is `false`.
+ */
+ @Ignore
+ @SuppressLint("NewApi")
+ constructor(
+ requiredNetworkType: NetworkType = NetworkType.NOT_REQUIRED,
+ requiresCharging: Boolean = false,
+ requiresBatteryNotLow: Boolean = false,
+ requiresStorageNotLow: Boolean = false,
+ ) : this(
+ requiredNetworkType = requiredNetworkType,
+ requiresCharging = requiresCharging,
+ requiresStorageNotLow = requiresStorageNotLow,
+ requiresBatteryNotLow = requiresBatteryNotLow,
+ requiresDeviceIdle = false
+ )
+
+ /**
+ * Constructs [Constraints].
+ *
+ * @param requiredNetworkType The type of network required for the work to run.
+ * The default value is [NetworkType.NOT_REQUIRED].
+ * @param requiresCharging whether device should be charging for the [WorkRequest] to run. The
+ * default value is `false`.
+ * @param requiresDeviceIdle whether device should be idle for the [WorkRequest] to run. The
+ * default value is `false`.
+ * @param requiresBatteryNotLow whether device battery should be at an acceptable level for the
+ * [WorkRequest] to run. The default value is `false`.
+ * @param requiresStorageNotLow whether the device's available storage should be at an
+ * acceptable level for the [WorkRequest] to run. The default value is `false`.
+ */
+ @Ignore
+ @SuppressLint("NewApi")
+ @RequiresApi(23) // requiresDeviceIdle is supported since API 23
+ constructor(
+ requiredNetworkType: NetworkType = NetworkType.NOT_REQUIRED,
+ requiresCharging: Boolean = false,
+ requiresDeviceIdle: Boolean = false,
+ requiresBatteryNotLow: Boolean = false,
+ requiresStorageNotLow: Boolean = false,
+ ) : this(
+ requiredNetworkType = requiredNetworkType,
+ requiresCharging = requiresCharging,
+ requiresDeviceIdle = requiresDeviceIdle,
+ requiresBatteryNotLow = requiresBatteryNotLow,
+ requiresStorageNotLow = requiresStorageNotLow,
+ contentTriggerUpdateDelayMillis = -1,
)
/**
+ * Constructs [Constraints].
+ *
+ * @param requiredNetworkType The type of network required for the work to run.
+ * The default value is [NetworkType.NOT_REQUIRED].
+ * @param requiresCharging whether device should be charging for the [WorkRequest] to run. The
+ * default value is `false`.
+ * @param requiresDeviceIdle whether device should be idle for the [WorkRequest] to run. The
+ * default value is `false`.
+ * @param requiresBatteryNotLow whether device battery should be at an acceptable level for the
+ * [WorkRequest] to run. The default value is `false`.
+ * @param requiresStorageNotLow whether the device's available storage should be at an
+ * acceptable level for the [WorkRequest] to run. The default value is `false`.
+ * @param contentTriggerUpdateDelayMillis the delay in milliseconds that is allowed from the
+ * time a `content:` [Uri] change is detected to the time when the [WorkRequest] is scheduled.
+ * If there are more changes during this time, the delay will be reset to the start of the most
+ * recent change. This functionality is identical to the one found in `JobScheduler` and
+ * is described in [android.app.job.JobInfo.Builder.setTriggerContentUpdateDelay]
+ * @param contentTriggerMaxDelayMillis the maximum delay in milliseconds that is allowed
+ * from the first time a `content:` [Uri] change is detected to the time when the [WorkRequest]
+ * is scheduled. This functionality is identical to the one found in `JobScheduler` and is
+ * described in [android.app.job.JobInfo.Builder.setTriggerContentMaxDelay].
+ * @param contentUriTriggers set of [ContentUriTrigger]. [WorkRequest] will run when a local
+ * `content:` [Uri] of one of the triggers in the set is updated.
+ * This functionality is identical to the one found in `JobScheduler` and is described in
+ * [android.app.job.JobInfo.Builder.addTriggerContentUri].
+ */
+ @RequiresApi(24)
+ constructor(
+ requiredNetworkType: NetworkType = NetworkType.NOT_REQUIRED,
+ requiresCharging: Boolean = false,
+ requiresDeviceIdle: Boolean = false,
+ requiresBatteryNotLow: Boolean = false,
+ requiresStorageNotLow: Boolean = false,
+ contentTriggerUpdateDelayMillis: Long = -1,
+ contentTriggerMaxDelayMillis: Long = -1,
+ contentUriTriggers: Set<ContentUriTrigger> = setOf(),
+ ) {
+ this.requiredNetworkType = requiredNetworkType
+ this.requiresCharging = requiresCharging
+ this.requiresDeviceIdle = requiresDeviceIdle
+ this.requiresBatteryNotLow = requiresBatteryNotLow
+ this.requiresStorageNotLow = requiresStorageNotLow
+ this.contentTriggerUpdateDelayMillis = contentTriggerUpdateDelayMillis
+ this.contentTriggerMaxDelayMillis = contentTriggerMaxDelayMillis
+ this.contentUriTriggers = contentUriTriggers
+ }
+
+ @SuppressLint("NewApi") // just copy everything
+ constructor(other: Constraints) {
+ requiresCharging = other.requiresCharging
+ requiresDeviceIdle = other.requiresDeviceIdle
+ requiredNetworkType = other.requiredNetworkType
+ requiresBatteryNotLow = other.requiresBatteryNotLow
+ requiresStorageNotLow = other.requiresStorageNotLow
+ contentUriTriggers = other.contentUriTriggers
+ contentTriggerUpdateDelayMillis = other.contentTriggerUpdateDelayMillis
+ contentTriggerMaxDelayMillis = other.contentTriggerMaxDelayMillis
+ }
+
+ /**
* @return `true` if the work should only execute while the device is charging
*/
fun requiresCharging(): Boolean {
@@ -117,9 +235,12 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun hasContentUriTriggers(): Boolean {
- return contentUriTriggers.isNotEmpty()
+ return Build.VERSION.SDK_INT < 24 || contentUriTriggers.isNotEmpty()
}
+ // just use all properties in equals, no actual harm in accessing properties annotated by
+ // RequiresApi(...)
+ @SuppressLint("NewApi")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
@@ -134,6 +255,9 @@
else contentUriTriggers == that.contentUriTriggers
}
+ // just use all properties in hashCode, no actual harm in accessing properties annotated by
+ // RequiresApi(...)
+ @SuppressLint("NewApi")
override fun hashCode(): Int {
var result = requiredNetworkType.hashCode()
result = 31 * result + if (requiresCharging) 1 else 0
@@ -345,6 +469,7 @@
triggerMaxContentDelay = -1
}
+ @Suppress("NewApi")
return Constraints(
requiresCharging = requiresCharging,
requiresDeviceIdle = Build.VERSION.SDK_INT >= 23 && requiresDeviceIdle,
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkManager.java b/work/work-runtime/src/main/java/androidx/work/WorkManager.java
index a01f374..ddfb9ade 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkManager.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkManager.java
@@ -32,6 +32,8 @@
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import kotlinx.coroutines.flow.Flow;
+
/**
* WorkManager is the recommended library for persistent work.
* Scheduled work is guaranteed to execute sometime after its {@link Constraints} are met.
@@ -523,6 +525,16 @@
public abstract @NonNull LiveData<WorkInfo> getWorkInfoByIdLiveData(@NonNull UUID id);
/**
+ * Gets a {@link Flow} of the {@link WorkInfo} for a given work id.
+ *
+ * @param id The id of the work
+ * @return A {@link Flow} of the {@link WorkInfo} associated with {@code id}; note that
+ * this {@link WorkInfo} may be {@code null} if {@code id} is not known to
+ * WorkManager.
+ */
+ public abstract @NonNull Flow<WorkInfo> getWorkInfoByIdFlow(@NonNull UUID id);
+
+ /**
* Gets a {@link ListenableFuture} of the {@link WorkInfo} for a given work id.
*
* @param id The id of the work
@@ -542,6 +554,14 @@
@NonNull String tag);
/**
+ * Gets a {@link Flow} of the {@link WorkInfo} for all work for a given tag.
+ *
+ * @param tag The tag of the work
+ * @return A {@link Flow} list of {@link WorkInfo} for work tagged with {@code tag}
+ */
+ public abstract @NonNull Flow<List<WorkInfo>> getWorkInfosByTagFlow(@NonNull String tag);
+
+ /**
* Gets a {@link ListenableFuture} of the {@link WorkInfo} for all work for a given tag.
*
* @param tag The tag of the work
@@ -563,6 +583,17 @@
@NonNull String uniqueWorkName);
/**
+ * Gets a {@link Flow} of the {@link WorkInfo} for all work in a work chain with a given
+ * unique name.
+ *
+ * @param uniqueWorkName The unique name used to identify the chain of work
+ * @return A {@link Flow} of the {@link WorkInfo} for work in the chain named
+ * {@code uniqueWorkName}
+ */
+ public abstract @NonNull Flow<List<WorkInfo>> getWorkInfosForUniqueWorkFlow(
+ @NonNull String uniqueWorkName);
+
+ /**
* Gets a {@link ListenableFuture} of the {@link WorkInfo} for all work in a work chain
* with a given unique name.
*
@@ -585,6 +616,17 @@
@NonNull WorkQuery workQuery);
/**
+ * Gets the {@link Flow} of the {@link List} of {@link WorkInfo} for all work
+ * referenced by the {@link WorkQuery} specification.
+ *
+ * @param workQuery The work query specification
+ * @return A {@link Flow} of the {@link List} of {@link WorkInfo} for work
+ * referenced by this {@link WorkQuery}.
+ */
+ public abstract @NonNull Flow<List<WorkInfo>> getWorkInfosFlow(
+ @NonNull WorkQuery workQuery);
+
+ /**
* Gets the {@link ListenableFuture} of the {@link List} of {@link WorkInfo} for all work
* referenced by the {@link WorkQuery} specification.
*
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 7656327..6d8bb95 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -20,9 +20,13 @@
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.text.TextUtils.isEmpty;
-import static androidx.work.impl.Schedulers.createBestAvailableBackgroundScheduler;
+import static androidx.work.impl.WorkManagerImplExtKt.createWorkManager;
import static androidx.work.impl.WorkerUpdater.enqueueUniquelyNamedPeriodic;
import static androidx.work.impl.foreground.SystemForegroundDispatcher.createCancelWorkIntent;
+import static androidx.work.impl.model.RawWorkInfoDaoKt.getWorkInfoPojosFlow;
+import static androidx.work.impl.model.WorkSpecDaoKt.getWorkStatusPojoFlowDataForIds;
+import static androidx.work.impl.model.WorkSpecDaoKt.getWorkStatusPojoFlowForName;
+import static androidx.work.impl.model.WorkSpecDaoKt.getWorkStatusPojoFlowForTag;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -44,13 +48,11 @@
import androidx.work.OneTimeWorkRequest;
import androidx.work.Operation;
import androidx.work.PeriodicWorkRequest;
-import androidx.work.R;
import androidx.work.WorkContinuation;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkQuery;
import androidx.work.WorkRequest;
-import androidx.work.impl.background.greedy.GreedyScheduler;
import androidx.work.impl.background.systemalarm.RescheduleReceiver;
import androidx.work.impl.background.systemjob.SystemJobScheduler;
import androidx.work.impl.constraints.trackers.Trackers;
@@ -68,16 +70,16 @@
import androidx.work.impl.utils.StopWorkRunnable;
import androidx.work.impl.utils.futures.SettableFuture;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor;
import androidx.work.multiprocess.RemoteWorkManager;
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
+import kotlinx.coroutines.flow.Flow;
+
/**
* A concrete implementation of {@link WorkManager}.
*
@@ -99,11 +101,10 @@
private List<Scheduler> mSchedulers;
private Processor mProcessor;
private PreferenceUtils mPreferenceUtils;
- private boolean mForceStopRunnableCompleted;
+ private boolean mForceStopRunnableCompleted = false;
private BroadcastReceiver.PendingResult mRescheduleReceiverResult;
private volatile RemoteWorkManager mRemoteWorkManager;
private final Trackers mTrackers;
- private final WorkLauncher mWorkLauncher;
private static WorkManagerImpl sDelegatedInstance = null;
private static WorkManagerImpl sDefaultInstance = null;
private static final Object sLock = new Object();
@@ -202,10 +203,7 @@
if (sDelegatedInstance == null) {
context = context.getApplicationContext();
if (sDefaultInstance == null) {
- sDefaultInstance = new WorkManagerImpl(
- context,
- configuration,
- new WorkManagerTaskExecutor(configuration.getTaskExecutor()));
+ sDefaultInstance = createWorkManager(context, configuration);
}
sDelegatedInstance = sDefaultInstance;
}
@@ -215,103 +213,6 @@
/**
* Create an instance of {@link WorkManagerImpl}.
*
- * @param context The application {@link Context}
- * @param configuration The {@link Configuration} configuration
- * @param workTaskExecutor The {@link TaskExecutor} for running "processing" jobs, such as
- * enqueueing, scheduling, cancellation, etc.
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public WorkManagerImpl(
- @NonNull Context context,
- @NonNull Configuration configuration,
- @NonNull TaskExecutor workTaskExecutor) {
- this(context,
- configuration,
- workTaskExecutor,
- context.getResources().getBoolean(R.bool.workmanager_test_configuration));
- }
-
- /**
- * Create an instance of {@link WorkManagerImpl}.
- *
- * @param context The application {@link Context}
- * @param configuration The {@link Configuration} configuration
- * @param workTaskExecutor The {@link TaskExecutor} for running "processing" jobs, such as
- * enqueueing, scheduling, cancellation, etc.
- * @param useTestDatabase {@code true} If using an in-memory test database
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public WorkManagerImpl(
- @NonNull Context context,
- @NonNull Configuration configuration,
- @NonNull TaskExecutor workTaskExecutor,
- boolean useTestDatabase) {
- this(context,
- configuration,
- workTaskExecutor,
- WorkDatabase.create(
- context.getApplicationContext(),
- workTaskExecutor.getSerialTaskExecutor(),
- useTestDatabase)
- );
- }
-
- /**
- * Create an instance of {@link WorkManagerImpl}.
- *
- * @param context The application {@link Context}
- * @param configuration The {@link Configuration} configuration
- * @param workTaskExecutor The {@link TaskExecutor} for running "processing" jobs, such as
- * enqueueing, scheduling, cancellation, etc.
- * @param database The {@link WorkDatabase}
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public WorkManagerImpl(
- @NonNull Context context,
- @NonNull Configuration configuration,
- @NonNull TaskExecutor workTaskExecutor,
- @NonNull WorkDatabase database) {
- Context applicationContext = context.getApplicationContext();
- Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
- mTrackers = new Trackers(applicationContext, workTaskExecutor);
- mProcessor = new Processor(
- context,
- configuration,
- workTaskExecutor,
- database);
- mWorkLauncher = new WorkLauncherImpl(mProcessor, workTaskExecutor);
- mWorkTaskExecutor = workTaskExecutor;
- mWorkDatabase = database;
- List<Scheduler> schedulers =
- createSchedulers(applicationContext, configuration, mTrackers);
- internalInit(context, configuration, workTaskExecutor, schedulers);
- }
-
- /**
- * Create an instance of {@link WorkManagerImpl}.
- *
- * @param context The application {@link Context}
- * @param configuration The {@link Configuration} configuration
- * @param workTaskExecutor The {@link TaskExecutor} for running "processing" jobs, such as
- * enqueueing, scheduling, cancellation, etc.
- * @param workDatabase The {@link WorkDatabase} instance
- * @param processor The {@link Processor} instance
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public WorkManagerImpl(
- @NonNull Context context,
- @NonNull Configuration configuration,
- @NonNull TaskExecutor workTaskExecutor,
- @NonNull WorkDatabase workDatabase,
- @NonNull List<Scheduler> schedulers,
- @NonNull Processor processor) {
- this(context, configuration, workTaskExecutor, workDatabase, schedulers, processor,
- new Trackers(context.getApplicationContext(), workTaskExecutor));
- }
-
- /**
- * Create an instance of {@link WorkManagerImpl}.
- *
* @param context The application {@link Context}
* @param configuration The {@link Configuration} configuration
* @param workTaskExecutor The {@link TaskExecutor} for running "processing" jobs, such as
@@ -329,12 +230,26 @@
@NonNull List<Scheduler> schedulers,
@NonNull Processor processor,
@NonNull Trackers trackers) {
- mTrackers = trackers;
- mWorkLauncher = new WorkLauncherImpl(processor, workTaskExecutor);
- mProcessor = processor;
+ context = context.getApplicationContext();
+ // Check for direct boot mode
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Api24Impl.isDeviceProtectedStorage(
+ context)) {
+ throw new IllegalStateException("Cannot initialize WorkManager in direct boot mode");
+ }
+ Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
+ mContext = context;
mWorkTaskExecutor = workTaskExecutor;
mWorkDatabase = workDatabase;
- internalInit(context, configuration, workTaskExecutor, schedulers);
+ mProcessor = processor;
+ mTrackers = trackers;
+ mConfiguration = configuration;
+ mSchedulers = schedulers;
+ mPreferenceUtils = new PreferenceUtils(mWorkDatabase);
+ Schedulers.registerRescheduling(schedulers, mProcessor,
+ workTaskExecutor.getSerialTaskExecutor(), mWorkDatabase, configuration);
+
+ // Checks for app force stops.
+ mWorkTaskExecutor.executeOnTaskThread(new ForceStopRunnable(context, this));
}
/**
@@ -575,6 +490,12 @@
mWorkTaskExecutor);
}
+ @NonNull
+ @Override
+ public Flow<WorkInfo> getWorkInfoByIdFlow(@NonNull UUID id) {
+ return getWorkStatusPojoFlowDataForIds(getWorkDatabase().workSpecDao(), id);
+ }
+
@Override
public @NonNull ListenableFuture<WorkInfo> getWorkInfoById(@NonNull UUID id) {
StatusRunnable<WorkInfo> runnable = StatusRunnable.forUUID(this, id);
@@ -582,6 +503,14 @@
return runnable.getFuture();
}
+ @NonNull
+ @Override
+ public Flow<List<WorkInfo>> getWorkInfosByTagFlow(@NonNull String tag) {
+ WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
+ return getWorkStatusPojoFlowForTag(workSpecDao,
+ mWorkTaskExecutor.getTaskCoroutineDispatcher(), tag);
+ }
+
@Override
public @NonNull LiveData<List<WorkInfo>> getWorkInfosByTagLiveData(@NonNull String tag) {
WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
@@ -613,6 +542,14 @@
mWorkTaskExecutor);
}
+ @NonNull
+ @Override
+ public Flow<List<WorkInfo>> getWorkInfosForUniqueWorkFlow(@NonNull String uniqueWorkName) {
+ WorkSpecDao workSpecDao = mWorkDatabase.workSpecDao();
+ return getWorkStatusPojoFlowForName(workSpecDao,
+ mWorkTaskExecutor.getTaskCoroutineDispatcher(), uniqueWorkName);
+ }
+
@Override
@NonNull
public ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(
@@ -639,6 +576,14 @@
@NonNull
@Override
+ public Flow<List<WorkInfo>> getWorkInfosFlow(@NonNull WorkQuery workQuery) {
+ RawWorkInfoDao rawWorkInfoDao = mWorkDatabase.rawWorkInfoDao();
+ return getWorkInfoPojosFlow(rawWorkInfoDao, mWorkTaskExecutor.getTaskCoroutineDispatcher(),
+ RawQueries.toRawQuery(workQuery));
+ }
+
+ @NonNull
+ @Override
public ListenableFuture<List<WorkInfo>> getWorkInfos(
@NonNull WorkQuery workQuery) {
StatusRunnable<List<WorkInfo>> runnable =
@@ -750,53 +695,6 @@
}
/**
- * Initializes an instance of {@link WorkManagerImpl}.
- *
- * @param context The application {@link Context}
- * @param configuration The {@link Configuration} configuration
- * @param schedulers The {@link List} of {@link Scheduler}s to use
- */
- private void internalInit(@NonNull Context context,
- @NonNull Configuration configuration,
- @NonNull TaskExecutor workTaskExecutor,
- @NonNull List<Scheduler> schedulers) {
-
- context = context.getApplicationContext();
- mContext = context;
- mConfiguration = configuration;
- mSchedulers = schedulers;
- mPreferenceUtils = new PreferenceUtils(mWorkDatabase);
- mForceStopRunnableCompleted = false;
- Schedulers.registerRescheduling(schedulers, mProcessor,
- workTaskExecutor.getSerialTaskExecutor(), mWorkDatabase, configuration);
- // Check for direct boot mode
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Api24Impl.isDeviceProtectedStorage(
- context)) {
- throw new IllegalStateException("Cannot initialize WorkManager in direct boot mode");
- }
-
- // Checks for app force stops.
- mWorkTaskExecutor.executeOnTaskThread(new ForceStopRunnable(context, this));
- }
-
- /**
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public List<Scheduler> createSchedulers(
- @NonNull Context context,
- @NonNull Configuration configuration,
- @NonNull Trackers trackers
- ) {
-
- return Arrays.asList(
- createBestAvailableBackgroundScheduler(context, mWorkDatabase, configuration),
- // Specify the task executor directly here as this happens before internalInit.
- // GreedyScheduler creates ConstraintTrackers and controllers eagerly.
- new GreedyScheduler(context, configuration, trackers, mProcessor, mWorkLauncher));
- }
-
- /**
* Tries to find a multi-process safe implementation for {@link WorkManager}.
*/
private void tryInitializeMultiProcessSupport() {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImplExt.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImplExt.kt
new file mode 100644
index 0000000..d27ca51
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImplExt.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.work.impl
+
+import android.content.Context
+import androidx.work.Configuration
+import androidx.work.R
+import androidx.work.impl.background.greedy.GreedyScheduler
+import androidx.work.impl.constraints.trackers.Trackers
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
+
+@JvmName("createWorkManager")
+@JvmOverloads
+fun WorkManagerImpl(
+ context: Context,
+ configuration: Configuration,
+ workTaskExecutor: TaskExecutor = WorkManagerTaskExecutor(configuration.taskExecutor),
+ workDatabase: WorkDatabase =
+ WorkDatabase.create(
+ context.applicationContext, workTaskExecutor.serialTaskExecutor,
+ context.resources.getBoolean(R.bool.workmanager_test_configuration)
+ ),
+ trackers: Trackers = Trackers(context.applicationContext, workTaskExecutor),
+ processor: Processor = Processor(
+ context.applicationContext, configuration, workTaskExecutor, workDatabase
+ ),
+ schedulersCreator: SchedulersCreator = ::createSchedulers
+): WorkManagerImpl {
+ val schedulers = schedulersCreator(
+ context, configuration,
+ workTaskExecutor, workDatabase, trackers, processor
+ )
+ return WorkManagerImpl(
+ context.applicationContext, configuration, workTaskExecutor, workDatabase,
+ schedulers, processor, trackers
+ )
+}
+
+@JvmName("createTestWorkManager")
+fun TestWorkManagerImpl(
+ context: Context,
+ configuration: Configuration,
+ workTaskExecutor: TaskExecutor,
+) = WorkManagerImpl(
+ context, configuration, workTaskExecutor,
+ WorkDatabase.create(context, workTaskExecutor.serialTaskExecutor, true)
+)
+
+internal typealias SchedulersCreator = (
+ context: Context,
+ configuration: Configuration,
+ workTaskExecutor: TaskExecutor,
+ workDatabase: WorkDatabase,
+ trackers: Trackers,
+ processor: Processor
+) -> List<Scheduler>
+
+fun schedulers(vararg schedulers: Scheduler): SchedulersCreator =
+ { _, _, _, _, _, _ -> schedulers.toList() }
+
+private fun createSchedulers(
+ context: Context,
+ configuration: Configuration,
+ workTaskExecutor: TaskExecutor,
+ workDatabase: WorkDatabase,
+ trackers: Trackers,
+ processor: Processor,
+): List<Scheduler> =
+ listOf(
+ Schedulers.createBestAvailableBackgroundScheduler(context, workDatabase, configuration),
+ GreedyScheduler(
+ context, configuration, trackers, processor,
+ WorkLauncherImpl(processor, workTaskExecutor)
+ ),
+ )
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/RawWorkInfoDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/RawWorkInfoDao.kt
index 80588f6..47d39d9 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/RawWorkInfoDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/RawWorkInfoDao.kt
@@ -19,6 +19,9 @@
import androidx.room.Dao
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery
+import androidx.work.WorkInfo
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
/**
* A Data Access Object for accessing [androidx.work.WorkInfo]s that uses raw SQL queries.
@@ -41,4 +44,17 @@
fun getWorkInfoPojosLiveData(
query: SupportSQLiteQuery
): LiveData<List<WorkSpec.WorkInfoPojo>>
-}
\ No newline at end of file
+
+ /**
+ * @param query The raw query obtained using [androidx.work.WorkQuery]
+ * @return A [Flow] of a [List] of [WorkSpec.WorkInfoPojo]s using the
+ * raw query.
+ */
+ @RawQuery(observedEntities = [WorkSpec::class])
+ fun getWorkInfoPojosFlow(query: SupportSQLiteQuery): Flow<List<WorkSpec.WorkInfoPojo>>
+}
+
+fun RawWorkInfoDao.getWorkInfoPojosFlow(
+ dispatcher: CoroutineDispatcher,
+ query: SupportSQLiteQuery
+): Flow<List<WorkInfo>> = getWorkInfoPojosFlow(query).dedup(dispatcher)
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpec.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpec.kt
index c2bf555..1d6a6b8 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpec.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpec.kt
@@ -304,11 +304,11 @@
val offset = if (periodCount == 0) 0 else intervalDuration
start + offset
}
+ } else if (lastEnqueueTime == 0L) {
+ // If never enqueued, we aren't scheduled to run.
+ Long.MAX_VALUE / 2 // 100 million years.
} else {
- // We are checking for (periodStartTime == 0) to support our testing use case.
- // For newly created WorkSpecs periodStartTime will always be 0.
- val start = if (lastEnqueueTime == 0L) System.currentTimeMillis() else lastEnqueueTime
- start + initialDelay
+ lastEnqueueTime + initialDelay
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt
index cfd36f4..ac78f50 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkSpecDao.kt
@@ -28,6 +28,13 @@
import androidx.work.WorkInfo
import androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES
import androidx.work.impl.model.WorkTypeConverters.StateIds.ENQUEUED
+import java.util.UUID
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import org.intellij.lang.annotations.Language
/**
* The Data Access Object for [WorkSpec]s.
@@ -171,10 +178,21 @@
* @return A [LiveData] list of [WorkSpec.WorkInfoPojo]
*/
@Transaction
- @Query("SELECT $WORK_INFO_COLUMNS FROM workspec WHERE id IN (:ids)")
+ @Query(WORK_INFO_BY_IDS)
fun getWorkStatusPojoLiveDataForIds(ids: List<String>): LiveData<List<WorkSpec.WorkInfoPojo>>
/**
+ * For a list of [WorkSpec] identifiers, retrieves a [LiveData] list of their
+ * [WorkSpec.WorkInfoPojo].
+ *
+ * @param ids The identifier of the [WorkSpec]s
+ * @return A [Flow] list of [WorkSpec.WorkInfoPojo]
+ */
+ @Transaction
+ @Query(WORK_INFO_BY_IDS)
+ fun getWorkStatusPojoFlowDataForIds(ids: List<String>): Flow<List<WorkSpec.WorkInfoPojo>>
+
+ /**
* Retrieves a list of [WorkSpec.WorkInfoPojo] for all work with a given tag.
*
* @param tag The tag for the [WorkSpec]s
@@ -195,10 +213,18 @@
* @return A [LiveData] list of [WorkSpec.WorkInfoPojo]
*/
@Transaction
- @Query(
- """SELECT $WORK_INFO_COLUMNS FROM workspec WHERE id IN
- (SELECT work_spec_id FROM worktag WHERE tag=:tag)"""
- )
+ @Query(WORK_INFO_BY_TAG)
+ fun getWorkStatusPojoFlowForTag(tag: String): Flow<List<WorkSpec.WorkInfoPojo>>
+
+ /**
+ * Retrieves a [LiveData] list of [WorkSpec.WorkInfoPojo] for all work with a
+ * given tag.
+ *
+ * @param tag The tag for the [WorkSpec]s
+ * @return A [LiveData] list of [WorkSpec.WorkInfoPojo]
+ */
+ @Transaction
+ @Query(WORK_INFO_BY_TAG)
fun getWorkStatusPojoLiveDataForTag(tag: String): LiveData<List<WorkSpec.WorkInfoPojo>>
/**
@@ -222,13 +248,20 @@
* @return A [LiveData] list of [WorkSpec.WorkInfoPojo]
*/
@Transaction
- @Query(
- "SELECT $WORK_INFO_COLUMNS FROM workspec WHERE id IN " +
- "(SELECT work_spec_id FROM workname WHERE name=:name)"
- )
+ @Query(WORK_INFO_BY_NAME)
fun getWorkStatusPojoLiveDataForName(name: String): LiveData<List<WorkSpec.WorkInfoPojo>>
/**
+ * Retrieves a [Flow] list of [WorkSpec.WorkInfoPojo] for all work with a given name.
+ *
+ * @param name The name for the [WorkSpec]s
+ * @return A [LiveData] list of [WorkSpec.WorkInfoPojo]
+ */
+ @Transaction
+ @Query(WORK_INFO_BY_NAME)
+ fun getWorkStatusPojoFlowForName(name: String): Flow<List<WorkSpec.WorkInfoPojo>>
+
+ /**
* Gets all inputs coming from prerequisites for a particular [WorkSpec]. These are
* [Data] set via `Worker#setOutputData()`.
*
@@ -409,5 +442,36 @@
fun countNonFinishedContentUriTriggerWorkers(): Int
}
+fun WorkSpecDao.getWorkStatusPojoFlowDataForIds(id: UUID): Flow<WorkInfo?> =
+ getWorkStatusPojoFlowDataForIds(listOf("$id"))
+ .map { it.firstOrNull()?.toWorkInfo() }.distinctUntilChanged()
+
+fun WorkSpecDao.getWorkStatusPojoFlowForName(
+ dispatcher: CoroutineDispatcher,
+ name: String
+): Flow<List<WorkInfo>> = getWorkStatusPojoFlowForName(name).dedup(dispatcher)
+
+fun WorkSpecDao.getWorkStatusPojoFlowForTag(
+ dispatcher: CoroutineDispatcher,
+ tag: String
+): Flow<List<WorkInfo>> = getWorkStatusPojoFlowForTag(tag).dedup(dispatcher)
+
+internal fun Flow<List<WorkSpec.WorkInfoPojo>>.dedup(
+ dispatcher: CoroutineDispatcher
+): Flow<List<WorkInfo>> = map { list -> list.map { pojo -> pojo.toWorkInfo() } }
+ .distinctUntilChanged()
+ .flowOn(dispatcher)
+
private const val WORK_INFO_COLUMNS = "id, state, output, run_attempt_count, generation" +
- ", $CONSTRAINTS_COLUMNS"
\ No newline at end of file
+ ", $CONSTRAINTS_COLUMNS"
+
+@Language("sql")
+private const val WORK_INFO_BY_IDS = "SELECT $WORK_INFO_COLUMNS FROM workspec WHERE id IN (:ids)"
+
+@Language("sql")
+private const val WORK_INFO_BY_TAG = """SELECT $WORK_INFO_COLUMNS FROM workspec WHERE id IN
+ (SELECT work_spec_id FROM worktag WHERE tag=:tag)"""
+
+@Language("sql")
+private const val WORK_INFO_BY_NAME = "SELECT $WORK_INFO_COLUMNS FROM workspec WHERE id IN " +
+ "(SELECT work_spec_id FROM workname WHERE name=:name)"
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.kt
index f37354a..55734d1 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/StartWorkRunnable.kt
@@ -23,7 +23,6 @@
/**
* A [Runnable] that can start work on the
* [androidx.work.impl.Processor].
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class StartWorkRunnable(
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/StopWorkRunnable.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/StopWorkRunnable.kt
index c5af619..cc9ee9d 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/StopWorkRunnable.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/StopWorkRunnable.kt
@@ -22,7 +22,6 @@
/**
* A [Runnable] that requests [androidx.work.impl.Processor] to stop the work
- * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class StopWorkRunnable(
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
index 3c2e331..e61932a 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/TaskExecutor.java
@@ -22,6 +22,9 @@
import java.util.concurrent.Executor;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.ExecutorsKt;
+
/**
* Interface for executing common tasks in WorkManager.
*
@@ -52,4 +55,9 @@
*/
@NonNull
SerialExecutor getSerialTaskExecutor();
+
+ @NonNull
+ default CoroutineDispatcher getTaskCoroutineDispatcher() {
+ return ExecutorsKt.from(getSerialTaskExecutor());
+ }
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
index 551cd60..9c4f92c 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/taskexecutor/WorkManagerTaskExecutor.java
@@ -25,6 +25,9 @@
import java.util.concurrent.Executor;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.ExecutorsKt;
+
/**
* Default Task Executor for executing common tasks in WorkManager
*/
@@ -32,11 +35,13 @@
public class WorkManagerTaskExecutor implements TaskExecutor {
private final SerialExecutorImpl mBackgroundExecutor;
+ private final CoroutineDispatcher mTaskDispatcher;
public WorkManagerTaskExecutor(@NonNull Executor backgroundExecutor) {
// Wrap it with a serial executor so we have ordering guarantees on commands
// being executed.
mBackgroundExecutor = new SerialExecutorImpl(backgroundExecutor);
+ mTaskDispatcher = ExecutorsKt.from(mBackgroundExecutor);
}
final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
@@ -59,4 +64,10 @@
public SerialExecutorImpl getSerialTaskExecutor() {
return mBackgroundExecutor;
}
+
+ @NonNull
+ @Override
+ public CoroutineDispatcher getTaskCoroutineDispatcher() {
+ return mTaskDispatcher;
+ }
}
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt b/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt
index 42940b8..fd6d17b 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt
@@ -38,7 +38,7 @@
class TestScheduler(
private val workDatabase: WorkDatabase,
private val launcher: WorkLauncher
-) : Scheduler {
+) : Scheduler, TestDriver {
@GuardedBy("lock")
private val pendingWorkStates = mutableMapOf<String, InternalWorkState>()
private val lock = Any()
@@ -85,7 +85,7 @@
* @param workSpecId The [Worker]'s id
* @throws IllegalArgumentException if `workSpecId` is not enqueued
*/
- fun setAllConstraintsMet(workSpecId: UUID) {
+ override fun setAllConstraintsMet(workSpecId: UUID) {
val id = workSpecId.toString()
val spec = loadSpec(id)
val state: InternalWorkState
@@ -104,7 +104,7 @@
* @param workSpecId The [Worker]'s id
* @throws IllegalArgumentException if `workSpecId` is not enqueued
*/
- fun setInitialDelayMet(workSpecId: UUID) {
+ override fun setInitialDelayMet(workSpecId: UUID) {
val id = workSpecId.toString()
val state: InternalWorkState
val spec = loadSpec(id)
@@ -123,7 +123,7 @@
* @param workSpecId The [Worker]'s id
* @throws IllegalArgumentException if `workSpecId` is not enqueued
*/
- fun setPeriodDelayMet(workSpecId: UUID) {
+ override fun setPeriodDelayMet(workSpecId: UUID) {
val id = workSpecId.toString()
val spec = loadSpec(id)
if (!spec.isPeriodic) throw IllegalArgumentException("Work with id $id isn't periodic!")
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
deleted file mode 100644
index 3358f9a..0000000
--- a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.work.testing;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.work.Configuration;
-import androidx.work.WorkManager;
-import androidx.work.impl.Scheduler;
-import androidx.work.impl.WorkLauncherImpl;
-import androidx.work.impl.WorkManagerImpl;
-import androidx.work.impl.constraints.trackers.Trackers;
-import androidx.work.impl.utils.taskexecutor.SerialExecutor;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
-import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-
-/**
- * A concrete implementation of {@link WorkManager} which can be used for testing. This
- * implementation makes it easy to swap Schedulers.
- *
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class TestWorkManagerImpl extends WorkManagerImpl implements TestDriver {
-
- private TestScheduler mScheduler;
-
- TestWorkManagerImpl(
- @NonNull final Context context,
- @NonNull final Configuration configuration
- ) {
- super(context, configuration,
- new WorkManagerTaskExecutor(configuration.getTaskExecutor()), true);
- }
-
- TestWorkManagerImpl(
- @NonNull final Context context,
- @NonNull final Configuration configuration,
- @NonNull final SerialExecutor serialExecutor
- ) {
-
- // Note: This implies that the call to ForceStopRunnable() actually does nothing.
- // This is okay when testing.
-
- // IMPORTANT: Leave the main thread executor as a Direct executor. This is very important.
- // Otherwise we subtly change the order of callbacks. onExecuted() will execute after
- // a call to StopWorkRunnable(). StopWorkRunnable() removes the pending WorkSpec and
- // therefore the call to onExecuted() does not add the workSpecId to the list of
- // terminated WorkSpecs. This is because internalWorkState == null.
- // Also for PeriodicWorkRequests, Schedulers.schedule() will run before the call to
- // onExecuted() and therefore PeriodicWorkRequests will always run twice.
- super(
- context,
- configuration,
- new TaskExecutor() {
- Executor mSynchronousExecutor = new SynchronousExecutor();
-
- @NonNull
- @Override
- public Executor getMainThreadExecutor() {
- return mSynchronousExecutor;
- }
-
- @NonNull
- @Override
- public SerialExecutor getSerialTaskExecutor() {
- return serialExecutor;
- }
- },
- true);
- }
-
- @Override
- @NonNull
- public List<Scheduler> createSchedulers(@NonNull Context context,
- @NonNull Configuration configuration, @NonNull Trackers trackers) {
- WorkLauncherImpl launcher = new WorkLauncherImpl(getProcessor(), getWorkTaskExecutor());
- mScheduler = new TestScheduler(getWorkDatabase(), launcher);
- return Collections.singletonList((Scheduler) mScheduler);
- }
-
- @Override
- public void setAllConstraintsMet(@NonNull UUID workSpecId) {
- mScheduler.setAllConstraintsMet(workSpecId);
- }
-
- @Override
- public void setInitialDelayMet(@NonNull UUID workSpecId) {
- mScheduler.setInitialDelayMet(workSpecId);
- }
-
- @Override
- public void setPeriodDelayMet(@NonNull UUID workSpecId) {
- mScheduler.setPeriodDelayMet(workSpecId);
- }
-}
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.kt b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.kt
new file mode 100644
index 0000000..a2b86d3
--- /dev/null
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.work.testing
+
+import android.content.Context
+
+import androidx.work.Configuration
+import androidx.work.impl.Processor
+import androidx.work.impl.Scheduler
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.WorkLauncherImpl
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.constraints.trackers.Trackers
+import androidx.work.impl.utils.taskexecutor.SerialExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
+
+internal fun createTestWorkManagerImpl(
+ context: Context,
+ configuration: Configuration,
+ serialExecutor: SerialExecutor,
+): WorkManagerImpl {
+ val taskExecutor = object : TaskExecutor {
+ val synchronousExecutor = SynchronousExecutor()
+ override fun getMainThreadExecutor() = synchronousExecutor
+
+ override fun getSerialTaskExecutor() = serialExecutor
+ }
+ return WorkManagerImpl(
+ context = context,
+ configuration = configuration,
+ workTaskExecutor = taskExecutor,
+ workDatabase = WorkDatabase.create(context, taskExecutor.serialTaskExecutor, true),
+ schedulersCreator = ::createTestSchedulers
+ )
+}
+
+internal fun createTestWorkManagerImpl(
+ context: Context,
+ configuration: Configuration,
+): WorkManagerImpl {
+ val taskExecutor = WorkManagerTaskExecutor(configuration.taskExecutor)
+ return WorkManagerImpl(
+ context = context,
+ configuration = configuration,
+ workTaskExecutor = taskExecutor,
+ workDatabase = WorkDatabase.create(context, taskExecutor.serialTaskExecutor, true),
+ schedulersCreator = ::createTestSchedulers
+ )
+}
+
+internal val WorkManagerImpl.testDriver: TestDriver
+ get() {
+ return schedulers.find { it is TestScheduler } as? TestScheduler
+ ?: throw IllegalStateException(
+ "WorkManager is incorrectly initialized. " +
+ "Was WorkManagerTestInitHelper.initializeTestWorkManager* method called?"
+ )
+ }
+
+@Suppress("UNUSED_PARAMETER")
+private fun createTestSchedulers(
+ context: Context,
+ configuration: Configuration,
+ workTaskExecutor: TaskExecutor,
+ workDatabase: WorkDatabase,
+ trackers: Trackers,
+ processor: Processor,
+): List<Scheduler> {
+ val launcher = WorkLauncherImpl(processor, workTaskExecutor)
+ return listOf<Scheduler>(TestScheduler(workDatabase, launcher))
+}
\ No newline at end of file
diff --git a/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java b/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
index cb3f365..22155ed 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
@@ -16,6 +16,8 @@
package androidx.work.testing;
+import static androidx.work.testing.TestWorkManagerImplKt.createTestWorkManagerImpl;
+
import android.content.Context;
import androidx.annotation.NonNull;
@@ -69,7 +71,7 @@
}
WorkManagerImpl.setDelegate(
- new TestWorkManagerImpl(context, configuration, serialExecutor)
+ createTestWorkManagerImpl(context, configuration, serialExecutor)
);
}
@@ -83,7 +85,7 @@
*/
public static void initializeTestWorkManagerWithRealExecutors(@NonNull Context context) {
Configuration configuration = new Configuration.Builder().build();
- WorkManagerImpl.setDelegate(new TestWorkManagerImpl(context, configuration));
+ WorkManagerImpl.setDelegate(createTestWorkManagerImpl(context, configuration));
}
/**
@@ -96,7 +98,7 @@
*/
public static void initializeTestWorkManagerWithRealExecutors(
@NonNull Context context, @NonNull Configuration configuration) {
- WorkManagerImpl.setDelegate(new TestWorkManagerImpl(context, configuration));
+ WorkManagerImpl.setDelegate(createTestWorkManagerImpl(context, configuration));
}
/**
@@ -110,7 +112,7 @@
if (workManager == null) {
return null;
} else {
- return (TestWorkManagerImpl) workManager;
+ return TestWorkManagerImplKt.getTestDriver(workManager);
}
}
@@ -120,7 +122,7 @@
*/
public static @Nullable TestDriver getTestDriver(@NonNull Context context) {
try {
- return (TestWorkManagerImpl) WorkManagerImpl.getInstance(context);
+ return TestWorkManagerImplKt.getTestDriver(WorkManagerImpl.getInstance(context));
} catch (IllegalStateException e) {
return null;
}